Skip to content

artimath/effect-gql

Repository files navigation

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/experimental

Core 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]-> patterns
  • streaming: Stream large result sets
  • mutations: Support INSERT/UPDATE/DELETE
  • transactions: ACID transaction support
  • parameters: Parameterized query support
  • maxTraversalDepth: Maximum graph traversal depth

Predefined Profiles:

import * as GqlCapabilities from "effect-gql/GqlCapabilities"

// Conservative defaults
GqlCapabilities.defaults

// Cloud Spanner Graph GA capabilities
GqlCapabilities.spanner

Error 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:

  1. 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()
)
  1. 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(): GqlRegistryShape

Create an empty registry.

makeWithSchemas

makeWithSchemas(entries: [label: string, schema: Schema][]): GqlRegistryShape

Create 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 rows
  • findOne / gqlOne: Return first match or None
  • single / gqlSingle: Return exactly one match or fail
  • void / gqlVoid: Execute without result

License

Apache-2.0

Author

Ryan Hunter (@artimath)

Links

About

Effect-native GQL (Graph Query Language) core library

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published