@doync/drizzle
v0.4.0
Published
doync Drizzle flavor: createDoyncDrizzle one-call entry for schema + queries + mutations
Downloads
1,120
Maintainers
Readme
@doync/drizzle
The Drizzle flavor of doync's DX surface: one call, createDoyncDrizzle, takes your Drizzle schema and drizzle-kit migrations and returns everything a Drizzle consumer needs — a typed db, the derived doync schema, a bound Drizzle-flavored defineQuery / defineMutation, and the plain-core registry helpers.
Under the hood the flavor still produces the same DoyncSchema, QueryDefinition, and MutationDefinition values plain @doync/core defines. The engine never sees Drizzle — only compiled SQL for query identity and Membership, plus an opaque row decoder so useQuery yields the same nested shape Drizzle would. You can mix Drizzle-authored definitions with plain-core ones in the same tree.
Install
pnpm add @doync/drizzle @doync/core drizzle-orm| Package | Role |
| --- | --- |
| @doync/drizzle | This flavor |
| @doync/core | Schema / query / mutation definition API the flavor builds on |
| drizzle-orm | Peer — >=0.30.0 (developed and tested against ^0.45) |
drizzle-orm is this package's peer on purpose: @doync/core carries none. You already need it to author tables; pin a version you trust and let the corpus canaries break loudly on a bad upgrade rather than silently mis-detect. Pair with drizzle-kit in your app (or workspace) to generate the migrations journal createDoyncDrizzle consumes — drizzle-kit is not a runtime dependency of this package.
For a browser or mobile client you will also want @doync/web / @doync/mobile and @doync/react. For Cloudflare Durable Objects, add @doync/server.
createDoyncDrizzle
import { createDoyncDrizzle } from '@doync/drizzle'
import * as drizzleSchema from './db/drizzle-schema'
import migrations from './db/migrations/migrations.js'
export const ctxValidationSchema = z.object({ userId: z.string() })
export const {
schema, // DoyncSchema (tables + migration track)
defineQuery, // Drizzle-flavored, bound to `db`
defineQueries, // plain @doync/core tree helper
defineMutation, // Drizzle-flavored ({ args, ctx, db }) bodies
defineMutations, // plain @doync/core tree helper
// optional `db` — SqliteRemoteDatabase over your schema for local reads
} = createDoyncDrizzle({
schema: drizzleSchema,
migrations,
ctxValidationSchema,
// optional `name` — Database name (default 'doync') — must match the client
})Options
| Field | Required | Notes |
| --- | --- | --- |
| schema | yes | Consumer Drizzle tables + relations() exports. A namespace import (import * as schema from './…') works; inference keeps db.query.* populated. |
| migrations | yes | drizzle-kit's migrations.js default export: { journal: { entries: [{ idx, tag }] }, migrations: { m0000: sql, … } }. Extra journal fields are ignored. Statement-breakpoint markers are stripped before the track reaches the engine. Backfill SQL the kit cannot diff belongs as a Kit custom migration in this same journal (drizzle-kit generate --custom --name=…). |
| ctxValidationSchema | yes | Standard Schema whose output is your auth-context shape: what authenticated ctx looks like in query resolvers and mutation bodies (null when anonymous). |
| name | no | Database name this db resolves imperative local reads against. Defaults to 'doync'. Must match the name the topology client was constructed with; two databases in one app get two createDoyncDrizzle calls with distinct names. |
Result (CreateDoyncDrizzleResult)
| Field | Type / role |
| --- | --- |
| schema | Derived DoyncSchema: every SQLiteTable (name + columns) plus the ordered migration track. Hand to Origin / Mirror / client constructors. |
| defineQueries / defineMutations | They register the defined queries and mutations by name. |
| defineQuery | Bound Drizzle-flavored query definer (see below). |
| defineMutation | Bound Drizzle-flavored mutation definer (see below). |
| db | SqliteRemoteDatabase for the consumer schema. await db.query… is a Local read — typed rows as fresh as the last re-dump, not a live Subscription. Writes on this db always refuse (LOCAL_READ_WRITE_MESSAGE). |
Defining a Drizzle schema
Author tables the usual Drizzle way. Every synced table needs a primary key (inline or composite); relations() exports are used for RQB nesting and filtered out of the doync table list.
// db/drizzle-schema.ts
import { relations } from 'drizzle-orm'
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
export const task = sqliteTable('task', {
id: text('id').primaryKey(),
title: text('title').notNull(),
done: integer('done').notNull().default(0),
ownerId: text('owner_id'),
})
export const taskRelations = relations(task, () => ({}))Generate migrations with drizzle-kit and import the journal the kit emits. For seed/backfill SQL the generated diff cannot express, add a custom migration so it sits in the journal at the right order:
drizzle-kit generate --custom --name=default_project
# edit the empty .sql the kit wrote, then wire it into migrations.jsQueries
defineQuery(argsValidationSchema?, ({ args, ctx, db }) => builder) — the resolver returns a real Drizzle RQB builder. Do not annotate the return type; row shape and one-ness are inferred from the builder (findMany → multi row; findFirst → one-query; mixing the two across return paths is a compile error — one-ness is static per definition). Nested with and columns projections flow through to useQuery.
import { eq } from 'drizzle-orm'
import * as z from 'zod/mini'
import { task } from './db/drizzle-schema'
import { db, defineQuery, defineQueries } from './schema'
export const queries = defineQueries({
// No args — findMany ⇒ multi-row Subscription
allTasks: defineQuery(({ db }) =>
db.query.task.findMany({ orderBy: (t, { asc }) => [asc(t.id)] }),
),
// Args + standard-schema validator; findFirst ⇒ one-query
taskById: defineQuery(z.string(), ({ args: id, db }) =>
db.query.task.findFirst({ where: eq(task.id, id) }),
),
// ctx-filtered: anonymous / user resolve to DIFFERENT statement identities
myOpen: defineQuery(({ ctx, db }) => {
if (ctx === null) {
return db.query.task.findMany({ where: (t, { eq }) => eq(t.id, '') })
}
return db.query.task.findMany({
where: (t, { and, eq }) => and(eq(t.done, 0), eq(t.ownerId, ctx.userId)),
})
}),
})Identity and Membership come only from the builder's compiled SQL (.toSQL() → { sql, params }). The engine parses that statement the same way it would a hand-written one. At runtime the flavor attaches a row decoder, so the nested shape useQuery returns matches Drizzle's own sqlite-proxy result.
Mutations
defineMutation(argsValidationSchema?, ({ args, ctx, db }) => …) — same single-object input shape as queries. The bound db is a per-invocation sqlite-proxy over the engine's MutationExec: writes, .returning(), core selects, and RQB reads all run through it. Bodies must be deterministic and DB-only (no ID minting, no external I/O) — external work belongs in server overrides via defineMutations(shared, serverOverrides).
import { eq } from 'drizzle-orm'
import * as z from 'zod/mini'
import { task } from './db/drizzle-schema'
import { defineMutation, defineMutations } from './schema'
const createArgs = z.object({
id: z.string(),
title: z.string(),
ownerId: z.string(),
})
export const mutations = defineMutations({
task: {
create: defineMutation(createArgs, async ({ args, db }) => {
// ids arrive in args — client-generated, never minted here
await db.insert(task).values({
id: args.id,
title: args.title,
ownerId: args.ownerId,
done: 0,
})
}),
rename: defineMutation(
z.object({ id: z.string(), title: z.string() }),
async ({ args, db }) => {
await db
.update(task)
.set({ title: args.title })
.where(eq(task.id, args.id))
},
),
},
})Fail-closed inside a body: db.transaction, db.batch, and any BEGIN / COMMIT / ROLLBACK / SAVEPOINT / RELEASE arriving as raw SQL throw — a mutation already runs inside one transaction.
Relationship to plain @doync/core
| Concern | Plain @doync/core | @doync/drizzle |
| --- | --- | --- |
| Schema | Plain DoyncSchema into createDoync | Derived by createDoyncDrizzle from Drizzle tables + drizzle-kit journal |
| Query body | ({ args, ctx, sql }) => sql`…` / sql.one`…` | ({ args, ctx, db }) => db.query….findMany/findFirst(…) |
| Mutation body | ({ args, ctx, sql }) => sql.exec(…) | ({ args, ctx, db }) => db.insert/update/… |
| Registry trees | defineQueries / defineMutations | Same helpers, re-exported on the result |
| Produced types | DoyncSchema, QueryDefinition, MutationDefinition | Identical — engines and hooks are flavor-blind |
Use this package when your app already models tables in Drizzle. Stay on plain core when you want hand-written SQL and no drizzle-orm peer. Both flavors hand the same shape to @doync/server, @doync/web, @doync/mobile, and @doync/react.
Local reads
After a client registers a local-read source under the same Database name, the shared db answers imperative Local reads:
// Web tab after createWebClient({ name: 'doync', … }) —
// as fresh as the last re-dump, not a live Subscription.
const open = await db.query.task.findMany({
where: (t, { eq }) => eq(t.done, 0),
})Writes on this db refuse with LOCAL_READ_WRITE_MESSAGE ('doync: this db is read-only — writes go through mutations'). Mutation bodies receive a different, write-capable db and are unaffected.
Public surface
The main entry (.) is governed by semver. Public symbols:
- Runtime:
createDoyncDrizzle - Types:
CreateDoyncDrizzleOptions, CreateDoyncDrizzleResult, DrizzleCompiled DrizzleDefineMutation, DrizzleDefineQuery, DrizzleMigrations, DrizzleMutationBody, DrizzleMutationInput, DrizzleOne, DrizzleQueryInput, DrizzleResolver, DrizzleRow
Internal (@doync/drizzle/internal)
Anything imported from @doync/drizzle/internal is not part of the semver surface. It may change or disappear in any release, including patches, without notice. Sibling @doync/* packages reach internals through that specifier when they must; application code should not. Import the Drizzle wrappers from @doync/drizzle.
