effect-gql
v0.1.0
Published
Effect-native GQL (Graph Query Language) core library
Downloads
3
Maintainers
Readme
effect-gql
Effect-native client for GQL (Graph Query Language) with type-safe query construction and schema-based node decoding.
Features
- 🎯 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
What is GQL?
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.
Installation
pnpm add effect-gql effect @effect/sql @effect/platform @effect/experimentalCore Concepts
effect-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
Usage
Type-Safe Query Construction
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
})Schema Registry for Heterogeneous Graphs
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 }
})Pre-configured Registry
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 })]
])Execution Options (Request Tags & Stats)
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 }
})
)
})Transaction Management
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" })
})
)
})Query Constructors
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>Capabilities System
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.spannerError Handling
effect-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")Implementing a Driver
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
}
)Production Drivers
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
})API Reference
GqlClient
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).
GqlRegistry
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.
GqlSchema
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
License
Apache-2.0
Author
Ryan Hunter (@artimath)
