effect-gql-spanner
v0.1.0
Published
Cloud Spanner Graph driver for GQL (Graph Query Language) with Effect.ts
Maintainers
Readme
effect-gql-spanner
Cloud Spanner Graph driver implementing the GQL (Graph Query Language) abstraction with Effect.ts.
Features
- 🎯 Complete GQL implementation for Cloud Spanner Graph
- 📊 Type-safe graph queries with schema validation
- 🔄 Schemaless node/edge operations with runtime type checking
- 🧪 Production-tested in Lever monorepo
- ⚡ Built on effect-gql, effect-sql-spanner, and effect-sql-googlesql
Installation
pnpm add effect-gql-spanner effect-gql effect-sql-spanner effect @effect/sqlQuick Start
import * as GqlClient from "effect-gql-spanner/GqlClient"
import * as Effect from "effect/Effect"
const program = Effect.gen(function* () {
const gql = yield* GqlClient.GqlClient
// Execute GQL query
const users = yield* gql.execute(
`MATCH (u:User)-[:FOLLOWS]->(f:User)
WHERE u.id = @userId
RETURN f`,
["user-123"]
)
return users
})Core Modules
GqlClient
Spanner Graph client implementing the GqlClient interface:
import * as GqlClient from "effect-gql-spanner/GqlClient"
import * as Layer from "effect/Layer"
const SpannerGqlLayer = GqlClient.layer({
projectId: "my-project",
instanceId: "my-instance",
databaseId: "my-database"
})
const program = Effect.provide(myProgram, SpannerGqlLayer)GraphSchema
Type-safe graph query constructors:
import * as GraphSchema from "effect-gql-spanner/GraphSchema"
import * as Schema from "effect/Schema"
const findFollowers = GraphSchema.gqlAll({
Request: Schema.Struct({ userId: Schema.String }),
Result: Schema.Struct({
id: Schema.String,
name: Schema.String
}),
execute: (req) => gql.execute(
`MATCH (u:User {id: @userId})-[:FOLLOWS]->(f) RETURN f`,
[req.userId]
)
})SchemalessSchema
Runtime-validated graph operations without compile-time schemas:
import * as SchemalessSchema from "effect-gql-spanner/SchemalessSchema"
// Insert node with runtime validation
yield* SchemalessSchema.insertNode({
label: "User",
properties: {
id: "user-123",
email: "[email protected]"
}
})
// Query with runtime schema
const users = yield* SchemalessSchema.findNodes({
label: "User",
filter: { role: "admin" }
})GraphNodes
High-level node operations:
import * as GraphNodes from "effect-gql-spanner/GraphNodes"
// Insert typed node
yield* GraphNodes.insert({
label: "User",
key: "user-123",
properties: { name: "Alice", verified: true }
})
// Update node
yield* GraphNodes.update({
label: "User",
key: "user-123",
properties: { lastLogin: new Date() }
})GraphEdges
High-level edge operations:
import * as GraphEdges from "effect-gql-spanner/GraphEdges"
// Create relationship
yield* GraphEdges.insert({
label: "FOLLOWS",
sourceKey: "user-123",
targetKey: "user-456",
properties: { since: new Date() }
})GraphPaths
Path traversal helpers:
import * as GraphPaths from "effect-gql-spanner/GraphPaths"
// Find paths between nodes
const paths = yield* GraphPaths.findPaths({
startLabel: "User",
startKey: "user-123",
endLabel: "User",
endKey: "user-456",
maxDepth: 5
})GqlResolver
Query result resolution with schema validation:
import * as GqlResolver from "effect-gql-spanner/GqlResolver"
const resolver = GqlResolver.make({
"User": Schema.Struct({ id: Schema.String, email: Schema.String }),
"Post": Schema.Struct({ id: Schema.String, title: Schema.String })
})
// Automatically validates and decodes based on node labels
const results = yield* resolver.resolve(queryResults)Configuration
interface SpannerGqlClientConfig {
projectId: string
instanceId: string
databaseId: string
emulatorHost?: string
transformQueryNames?: (name: string) => string
transformResultNames?: (name: string) => string
}Testing
Emulator Testing (Default)
Tests run against Spanner emulator by default:
# Run tests with emulator (default)
pnpm test
# Watch mode with emulator
pnpm test:watchThe emulator is automatically configured via SPANNER_EMULATOR_HOST=localhost:9010.
Live Database Testing
To test against a live Spanner instance:
# Run tests against live Spanner
pnpm test:liveMake sure to configure credentials before running live tests.
Test Layer Setup
import * as Testing from "effect-sql-spanner/testing"
// Emulator layer (creates isolated instance per test)
const testLayer = Testing.EmulatorPerTestLayer
it.effect("should query graph", () =>
Effect.gen(function* () {
const gql = yield* GqlClient.GqlClient
yield* gql.execute(
`CREATE (u:User {id: @id, name: @name})`,
["user-123", "Alice"]
)
const users = yield* gql.execute(
`MATCH (u:User {id: @id}) RETURN u`,
["user-123"]
)
assert.strictEqual(users.length, 1)
}).pipe(Effect.provide(testLayer))
)Architecture
effect-gql-spanner bridges three layers:
- effect-gql: Abstract GQL client interface
- effect-sql-spanner: Spanner SQL driver with GoogleSQL compilation
- effect-sql-googlesql: GoogleSQL query compilation and parameter typing
This creates a type-safe, Effect-native way to work with Spanner Graph using standard GQL syntax.
License
Apache-2.0
Author
Ryan Hunter (@artimath)
