Effect-native client for GQL (Graph Query Language) with type-safe query construction and schema-based node decoding.
- 🎯 Type-safe query construction with request/result schemas
- 🔄 Schema registry for heterogeneous node types
- 🔌 Driver abstraction for portable graph database access
- 📊 Scoped execution options (request tags, commit stats)
- đź”’ Transaction management with connection pooling
- ⚡ Built for production use with Effect.ts
GQL (Graph Query Language) is the ISO standard for querying property graph databases. Similar to how SQL is the standard for relational databases, GQL provides a standardized way to query graph data across different vendors.
Cloud Spanner Graph is the first GA product implementing this standard. effect-gql provides an Effect-native abstraction layer following the same patterns as @effect/sql.
pnpm add effect-gql effect @effect/sql @effect/platform @effect/experimentaleffect-gql mirrors @effect/sql patterns but for graph queries:
- GqlClient: Abstract client interface (implement per driver)
- GqlSchema: Type-safe query constructors with schema validation
- GqlRegistry: Runtime label-to-schema mapping for heterogeneous nodes
- GqlCapabilities: Feature detection system for driver capabilities
- GqlStatement: Parameter type metadata for driver optimization
import * as GqlSchema from "effect-gql/GqlSchema"
import * as Schema from "effect/Schema"
import * as Effect from "effect/Effect"
// Define request and result schemas
const findTasks = GqlSchema.findAll({
Request: Schema.Struct({
status: Schema.String
}),
Result: Schema.Struct({
id: Schema.String,
title: Schema.String,
priority: Schema.Number
}),
execute: (req) => client.execute(
`MATCH (t:Task {status: @status}) RETURN t`,
[req.status]
)
})
// Use with full type inference
const program = Effect.gen(function* () {
const tasks = yield* findTasks({ status: "open" })
// tasks: { id: string; title: string; priority: number }[]
return tasks
})import * as GqlRegistry from "effect-gql/GqlRegistry"
import * as Schema from "effect/Schema"
const program = Effect.gen(function* () {
// Create registry
const registry = GqlRegistry.make()
// Register node label schemas
yield* registry.register("Task", Schema.Struct({
id: Schema.String,
title: Schema.String,
completed: Schema.Boolean
}))
yield* registry.register("User", Schema.Struct({
id: Schema.String,
email: Schema.String,
name: Schema.String
}))
// Decode nodes by label
const task = yield* registry.decode("Task", rawNodeData)
// Typed as: { id: string; title: string; completed: boolean }
})import * as GqlRegistry from "effect-gql/GqlRegistry"
import * as Schema from "effect/Schema"
const registry = GqlRegistry.makeWithSchemas([
["Task", Schema.Struct({ title: Schema.String })],
["User", Schema.Struct({ email: Schema.String })],
["Project", Schema.Struct({ name: Schema.String })]
])import * as GqlClient from "effect-gql/GqlClient"
import * as Effect from "effect/Effect"
const program = Effect.gen(function* () {
const client = yield* GqlClient.GqlClient
// Set execution options for nested queries
yield* GqlClient.withExecutionOptions(
{
requestOptions: {
priority: "high",
tag: "critical-path"
},
returnCommitStats: true,
onCommitStats: (stats) => console.log("Commit stats:", stats)
},
Effect.gen(function* () {
// All queries in this scope inherit options
const tasks = yield* findTasks({ status: "open" })
const users = yield* findUsers({ role: "admin" })
return { tasks, users }
})
)
})import * as GqlClient from "effect-gql/GqlClient"
import * as Effect from "effect/Effect"
const program = Effect.gen(function* () {
const client = yield* GqlClient.GqlClient
// Atomic transaction
yield* client.withTransaction(
Effect.gen(function* () {
yield* createTask({ title: "Deploy" })
yield* assignTask({ taskId: "123", userId: "456" })
yield* updateStatus({ taskId: "123", status: "in-progress" })
})
)
})effect-gql provides four query constructor patterns:
import * as GqlSchema from "effect-gql/GqlSchema"
// Return all matching rows
const findAll = GqlSchema.findAll({
Request: Schema.Struct({ filter: Schema.String }),
Result: Schema.Struct({ id: Schema.String, value: Schema.Number }),
execute: (req) => client.execute(query, [req.filter])
})
// Effect<A[], GqlError | ParseError>
// Return first match or None
const findOne = GqlSchema.findOne({
Request: Schema.Struct({ id: Schema.String }),
Result: Schema.Struct({ value: Schema.Number }),
execute: (req) => client.execute(query, [req.id])
})
// Effect<Option<A>, GqlError | ParseError>
// Return exactly one match or fail
const single = GqlSchema.single({
Request: Schema.Struct({ id: Schema.String }),
Result: Schema.Struct({ value: Schema.Number }),
execute: (req) => client.execute(query, [req.id])
})
// Effect<A, GqlError | ParseError | NoSuchElementException>
// Execute without result
const voidQuery = GqlSchema.void({
Request: Schema.Struct({ id: Schema.String }),
execute: (req) => client.execute(mutation, [req.id])
})
// Effect<void, GqlError | ParseError>Drivers advertise supported features via capabilities:
import * as GqlCapabilities from "effect-gql/GqlCapabilities"
import * as Effect from "effect/Effect"
const program = Effect.gen(function* () {
const caps = yield* GqlCapabilities.GqlCapabilities
if (caps.variableLengthPaths) {
// Use -[*1..5]-> patterns
yield* findConnected({ depth: "variable" })
} else {
// Degrade to fixed depth traversal
yield* findConnected({ depth: 5 })
}
})Available Capabilities:
variableLengthPaths: Support for-[*1..5]->patternsstreaming: Stream large result setsmutations: Support INSERT/UPDATE/DELETEtransactions: ACID transaction supportparameters: Parameterized query supportmaxTraversalDepth: Maximum graph traversal depth
Predefined Profiles:
import * as GqlCapabilities from "effect-gql/GqlCapabilities"
// Conservative defaults
GqlCapabilities.defaults
// Cloud Spanner Graph GA capabilities
GqlCapabilities.spannereffect-gql provides a typed error hierarchy:
import * as GqlError from "effect-gql/GqlError"
// Base error class
class GqlError extends Data.TaggedError("GqlError")
// Result count mismatch
class ResultLengthMismatch extends Data.TaggedError("ResultLengthMismatch")
// Unknown node label in registry
class UnknownLabel extends Data.TaggedError("UnknownLabel")
// Schema validation failure
class SchemaError extends Data.TaggedError("SchemaError")To implement a GQL driver, provide:
- GqlClient implementation:
import * as GqlClient from "effect-gql/GqlClient"
import * as Context from "effect/Context"
import * as Layer from "effect/Layer"
class MyGqlClient implements GqlClient.GqlClient {
execute<A>(query: string, params?: GqlPrimitive[]) {
// Execute query against your backend
}
withTransaction<R, E, A>(effect: Effect.Effect<A, E, R>) {
// Manage transaction lifecycle
}
reserve: Effect.Effect<void, GqlError, Scope> {
// Acquire connection from pool
}
}
export const layer = Layer.succeed(
GqlClient.GqlClient,
new MyGqlClient()
)- GqlCapabilities layer:
import * as GqlCapabilities from "effect-gql/GqlCapabilities"
export const capabilitiesLayer = Layer.succeed(
GqlCapabilities.GqlCapabilities,
{
variableLengthPaths: true,
streaming: false,
mutations: true,
transactions: true,
parameters: true,
maxTraversalDepth: undefined
}
)effect-gql is an abstract interface. Use with a concrete driver:
- effect-gql-spanner: Cloud Spanner Graph driver with full GQL support
import * as SpannerGql from "effect-gql-spanner/Client"
import * as Effect from "effect/Effect"
const program = Effect.gen(function* () {
const client = yield* SpannerGql.ClientTag
// GQL queries automatically compiled and executed
const nodes = yield* client.execute(
`MATCH (n:Task) WHERE n.status = @status RETURN n`,
["open"]
)
return nodes
})execute
execute<A>(query: string, params?: GqlPrimitive[]): Effect<A[], GqlError>Execute a GQL query with optional parameters.
withTransaction
withTransaction<R, E, A>(effect: Effect<A, E, R>): Effect<A, E | GqlError, R>Execute an effect within a transaction boundary.
reserve
reserve: Effect<void, GqlError, Scope>Acquire a connection from the pool (scoped).
withExecutionOptions
withExecutionOptions<R, E, A>(
options: GqlExecutionOptions,
effect: Effect<A, E, R>
): Effect<A, E, R>Set scoped execution options (merges with parent scope).
make
make(): GqlRegistryShapeCreate an empty registry.
makeWithSchemas
makeWithSchemas(entries: [label: string, schema: Schema][]): GqlRegistryShapeCreate a pre-configured registry.
register
register<A, I, R>(label: string, schema: Schema<A, I, R>): Effect<void>Register a schema for a node label.
decode
decode<A, I, R>(label: string, data: unknown): Effect<A, UnknownLabel | SchemaError, R>Decode raw node data using registered schema.
labels
labels: Effect<string[]>Get all registered labels.
All constructors accept { Request, Result, execute } configuration:
findAll/gqlAll: Return all matching rowsfindOne/gqlOne: Return first match or Nonesingle/gqlSingle: Return exactly one match or failvoid/gqlVoid: Execute without result
Apache-2.0
Ryan Hunter (@artimath)