@graphql-suite/schema 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,191 +1,495 @@
1
- [![Monthly Downloads](https://img.shields.io/npm/dm/drizzle-graphql-suite.svg)](https://www.npmjs.com/package/drizzle-graphql-suite)
2
- [![NPM](https://img.shields.io/npm/v/drizzle-graphql-suite.svg 'NPM package version')](https://www.npmjs.com/package/drizzle-graphql-suite)
3
- [![CI](https://github.com/annexare/drizzle-graphql-suite/actions/workflows/ci.yml/badge.svg)](https://github.com/annexare/drizzle-graphql-suite/actions/workflows/ci.yml)
1
+ # @graphql-suite/schema
4
2
 
5
- # drizzle-graphql-suite
3
+ > Part of [`graphql-suite`](https://github.com/annexare/graphql-suite).
4
+ > See also: [`client`](https://github.com/annexare/graphql-suite/tree/main/packages/client) | [`query`](https://github.com/annexare/graphql-suite/tree/main/packages/query)
6
5
 
7
- Auto-generated GraphQL CRUD, type-safe clients, and React Query hooks from Drizzle PostgreSQL schemas.
6
+ Auto-generates a complete GraphQL schema with CRUD operations, relation-level filtering, and hooks from Drizzle PostgreSQL schemas.
8
7
 
9
- ## Overview
10
-
11
- `drizzle-graphql-suite` is a three-layer toolkit that turns your Drizzle ORM schema into a fully working GraphQL API with end-to-end type safety:
12
-
13
- 1. **Schema builder** — generates a complete GraphQL schema with CRUD operations, relation-level filtering, per-operation hooks, and runtime permissions from Drizzle table definitions.
14
- 2. **Client** — provides a type-safe GraphQL client that infers query/mutation types directly from your Drizzle schema, with full TypeScript support for filters, relations, and results.
15
- 3. **React Query hooks** — wraps the client in TanStack React Query hooks for caching, pagination, and mutations with automatic cache invalidation.
16
-
17
- Inspired by [`drizzle-graphql`](https://github.com/drizzle-team/drizzle-graphql), rewritten with significant improvements including relation-level filtering, hooks, count queries, configurable schema generation, and code generation.
8
+ ## Installation
18
9
 
19
- ## Packages
10
+ ```bash
11
+ bun add @graphql-suite/schema
12
+ ```
20
13
 
21
- | Subpath | Package | Description |
22
- |---------|---------|-------------|
23
- | `drizzle-graphql-suite/schema` | [`@drizzle-graphql-suite/schema`](packages/schema/README.md) | GraphQL schema builder with CRUD, filtering, hooks, permissions, and codegen |
24
- | `drizzle-graphql-suite/client` | [`@drizzle-graphql-suite/client`](packages/client/README.md) | Type-safe GraphQL client with full Drizzle type inference |
25
- | `drizzle-graphql-suite/query` | [`@drizzle-graphql-suite/query`](packages/query/README.md) | TanStack React Query hooks for the client |
14
+ ```bash
15
+ npm install @graphql-suite/schema
16
+ ```
26
17
 
27
- ## Installation
18
+ Or install the full suite:
28
19
 
29
20
  ```bash
30
- bun add drizzle-graphql-suite
21
+ bun add graphql-suite
31
22
  ```
32
23
 
33
24
  ```bash
34
- npm install drizzle-graphql-suite
25
+ npm install graphql-suite
35
26
  ```
36
27
 
37
- ## Peer Dependencies
28
+ ## Motivation
38
29
 
39
- Each subpath import has its own peer dependency requirements:
30
+ Inspired by [`drizzle-graphql`](https://github.com/drizzle-team/drizzle-graphql), this package is a purpose-built replacement focused on PostgreSQL. Key improvements:
40
31
 
41
- | Subpath | Peer Dependencies |
42
- |---------|-------------------|
43
- | `./schema` | `drizzle-orm` >=0.44.0, `graphql` >=16.3.0 |
44
- | `./client` | `drizzle-orm` >=0.44.0 |
45
- | `./query` | `react` >=18.0.0, `@tanstack/react-query` >=5.0.0 |
32
+ - **Small generated schema** the generated schema stays compact even when supporting self-relations and deeply nested relations, thanks to configurable depth limiting (`limitRelationDepth`, `limitSelfRelationDepth`), per-relation pruning (`pruneRelations`), and per-table control (`tables.exclude`, per-table `queries`/`mutations` toggles) — up to 90% schema size reduction when tuned
33
+ - **Native PostgreSQL JSON/JSONB support** — `json` and `jsonb` columns map to a custom `JSON` GraphQL scalar, so structured data passes through without manual type wiring
34
+ - **Relation-level filtering** with EXISTS subqueries (`some`/`every`/`none` quantifiers)
35
+ - **Runtime permissions** `withPermissions()` builds filtered schemas per role, fully reflected in introspection
36
+ - **Row-level security helpers** — `withRowSecurity()` + `mergeHooks()` for composable auth
37
+ - **Per-operation hooks system** (before/after/resolve) for auth, audit, and custom logic
38
+ - **Count queries** with full filter support
39
+ - **`buildEntities()`** for composable schema building (avoids redundant schema validation)
40
+ - **Configurable query/mutation suffixes** for naming customization
41
+ - **Self-relation depth limiting** — separate from general depth, prevents exponential type growth
42
+ - **Relation pruning** — `false`, `'leaf'`, or `{ only: [...] }` per relation
43
+ - **`buildSchemaFromDrizzle()`** — no database connection needed (for codegen/introspection)
44
+ - **Code generation** — `generateSDL`, `generateTypes`, `generateEntityDefs`
45
+ - **Architecture** — TypeScript source, PostgreSQL-only, `SchemaBuilder` class, type caching, lazy thunks for circular relations
46
+ - **Bug fixes** — relation filter join conditions (Drizzle v0.44+), operator map replacements, `catch (e: unknown)` narrowing
46
47
 
47
- ## Quick Start
48
+ ## API Reference
48
49
 
49
- ### 1. Server — Build GraphQL Schema
50
+ ### `buildSchema(db, config?)`
51
+
52
+ Builds a complete `GraphQLSchema` with all CRUD operations from a Drizzle database instance. Returns `{ schema, entities, withPermissions }`.
50
53
 
51
54
  ```ts
52
- import { buildSchema } from 'drizzle-graphql-suite/schema'
55
+ import { buildSchema } from '@graphql-suite/schema'
53
56
  import { createYoga } from 'graphql-yoga'
54
- import { createServer } from 'node:http'
55
57
  import { db } from './db'
56
58
 
57
- const { schema, withPermissions } = buildSchema(db, {
58
- tables: { exclude: ['session', 'verification'] },
59
- hooks: {
60
- user: {
61
- query: {
62
- before: async ({ context }) => {
63
- if (!context.user) throw new Error('Unauthorized')
64
- },
65
- },
66
- },
67
- },
59
+ const { schema, entities, withPermissions } = buildSchema(db, {
60
+ limitRelationDepth: 3,
61
+ tables: { exclude: ['session'] },
68
62
  })
69
63
 
70
64
  const yoga = createYoga({ schema })
71
- const server = createServer(yoga)
72
- server.listen(4000)
73
65
  ```
74
66
 
75
- #### Per-Role Schemas (Optional)
67
+ See [Runtime Permissions](#runtime-permissions) for `withPermissions` usage.
68
+
69
+ #### Framework Integration
70
+
71
+ **Next.js App Router**
76
72
 
77
73
  ```ts
78
- import { permissive, restricted, readOnly } from 'drizzle-graphql-suite/schema'
74
+ // app/api/graphql/route.ts
75
+ import { createYoga } from 'graphql-yoga'
76
+ import { schema } from '@/lib/schema' // from buildSchema() above
79
77
 
80
- // Cached per id call withPermissions on each request
81
- const schemas = {
82
- admin: schema,
83
- editor: withPermissions(permissive('editor', { audit: false, user: readOnly() })),
84
- viewer: withPermissions(restricted('viewer', { post: { query: true } })),
85
- }
78
+ const { handleRequest } = createYoga({
79
+ schema,
80
+ graphqlEndpoint: '/api/graphql',
81
+ fetchAPI: { Response },
82
+ })
83
+
84
+ export { handleRequest as GET, handleRequest as POST }
85
+ ```
86
+
87
+ **ElysiaJS**
88
+
89
+ ```ts
90
+ // server.ts
91
+ import { Elysia } from 'elysia'
92
+ import { yoga } from '@elysiajs/graphql-yoga'
93
+ import { schema } from './schema' // from buildSchema() above
94
+
95
+ new Elysia()
96
+ .use(yoga({ schema }))
97
+ .listen(3000)
98
+ ```
99
+
100
+ ### `buildEntities(db, config?)`
101
+
102
+ Returns `GeneratedEntities` only — queries, mutations, inputs, and types — without constructing a `GraphQLSchema`. Use this when composing into a larger schema (e.g., Pothos) to avoid redundant schema validation.
103
+
104
+ ```ts
105
+ import { buildEntities } from '@graphql-suite/schema'
106
+
107
+ const entities = buildEntities(db, { mutations: false })
108
+ // entities.queries, entities.mutations, entities.inputs, entities.types
86
109
  ```
87
110
 
88
- ### 2. Client — Type-Safe Queries
111
+ ### `buildSchemaFromDrizzle(drizzleSchema, config?)`
112
+
113
+ Builds a schema directly from Drizzle schema exports — no database connection or `.env` required. Resolvers are stubs. Intended for schema introspection and code generation.
89
114
 
90
115
  ```ts
91
- import { createDrizzleClient } from 'drizzle-graphql-suite/client'
116
+ import { buildSchemaFromDrizzle } from '@graphql-suite/schema'
92
117
  import * as schema from './db/schema'
93
118
 
94
- const client = createDrizzleClient({
95
- schema,
96
- config: { suffixes: { list: 's' } },
97
- url: '/api/graphql',
98
- })
119
+ const { schema: graphqlSchema } = buildSchemaFromDrizzle(schema)
120
+ ```
121
+
122
+ ## Configuration
123
+
124
+ `BuildSchemaConfig` controls all aspects of schema generation:
125
+
126
+ ### `mutations`
127
+
128
+ Enable or disable mutation generation globally.
129
+
130
+ - **Type**: `boolean`
131
+ - **Default**: `true`
132
+
133
+ ### `limitRelationDepth`
134
+
135
+ Maximum depth of nested relation fields on queries. Set to `0` to omit relations, `undefined` for no limit.
136
+
137
+ - **Type**: `number | undefined`
138
+ - **Default**: `3`
139
+
140
+ ### `limitSelfRelationDepth`
141
+
142
+ Maximum occurrences of the same table via direct self-relations (e.g., `asset.template → asset`). At `1`, self-relation fields are omitted entirely. At `2`, one level of expansion is allowed. Cross-table paths that revisit a table reset the counter and use `limitRelationDepth` instead.
143
+
144
+ - **Type**: `number`
145
+ - **Default**: `1`
146
+
147
+ ### `suffixes`
148
+
149
+ Customize query field name suffixes for list and single queries.
150
+
151
+ - **Type**: `{ list?: string; single?: string }`
152
+ - **Default**: `{ list: '', single: 'Single' }`
99
153
 
100
- const users = await client.entity('user').query({
101
- select: {
102
- id: true,
103
- name: true,
104
- posts: { id: true, title: true },
154
+ ### `tables`
155
+
156
+ Per-table schema control:
157
+
158
+ ```ts
159
+ {
160
+ tables: {
161
+ // Remove tables entirely (relations to them are silently skipped)
162
+ exclude: ['session', 'verification'],
163
+ // Per-table operation overrides
164
+ config: {
165
+ auditLog: { queries: true, mutations: false },
166
+ user: { mutations: false },
167
+ },
105
168
  },
106
- where: { name: { ilike: '%john%' } },
107
- limit: 10,
108
- })
169
+ }
109
170
  ```
110
171
 
111
- ### 3. ReactQuery Hooks
172
+ - `exclude` `string[]` tables removed from the schema entirely
173
+ - `config` — `Record<string, TableOperations>` — per-table `queries` and `mutations` booleans
112
174
 
113
- ```tsx
114
- import { GraphQLProvider, useEntity, useEntityList } from 'drizzle-graphql-suite/query'
115
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
175
+ ### `pruneRelations`
116
176
 
117
- const queryClient = new QueryClient()
177
+ Fine-grained per-relation pruning. Keys are `tableName.relationName`:
118
178
 
119
- function App() {
120
- return (
121
- <QueryClientProvider client={queryClient}>
122
- <GraphQLProvider client={graphqlClient}>
123
- <UserList />
124
- </GraphQLProvider>
125
- </QueryClientProvider>
126
- )
179
+ ```ts
180
+ {
181
+ pruneRelations: {
182
+ // Omit this relation field entirely
183
+ 'asset.childAssets': false,
184
+ // Expand with scalar columns only (no nested relations)
185
+ 'user.posts': 'leaf',
186
+ // Expand with only the listed child relations
187
+ 'post.comments': { only: ['author'] },
188
+ },
127
189
  }
190
+ ```
191
+
192
+ - `false` — relation field omitted entirely from parent type
193
+ - `'leaf'` — relation expands with scalar columns only
194
+ - `{ only: string[] }` — relation expands with only the listed child relation fields
195
+
196
+ ### `hooks`
197
+
198
+ Per-table, per-operation hooks. See [Hooks System](#hooks-system).
199
+
200
+ ### `debug`
201
+
202
+ Enable diagnostic logging for schema size and relation tree.
203
+
204
+ - **Type**: `boolean | { schemaSize?: boolean; relationTree?: boolean }`
205
+ - **Default**: `undefined`
206
+
207
+ ## Runtime Permissions
208
+
209
+ Build filtered `GraphQLSchema` variants per role or user — introspection fully reflects what each role can see and do.
210
+
211
+ ```ts
212
+ import { buildSchema, permissive, restricted, readOnly } from '@graphql-suite/schema'
213
+
214
+ const { schema, withPermissions } = buildSchema(db)
215
+
216
+ // Full schema (admin)
217
+ const adminSchema = schema
128
218
 
129
- function UserList() {
130
- const user = useEntity('user')
131
- const { data, isLoading } = useEntityList(user, {
132
- select: { id: true, name: true, email: true },
133
- limit: 20,
134
- })
219
+ // Permissive: everything allowed except audit (excluded) and users (read-only)
220
+ const maintainerSchema = withPermissions(
221
+ permissive('maintainer', { audit: false, users: readOnly() }),
222
+ )
135
223
 
136
- if (isLoading) return <div>Loading...</div>
137
- return <ul>{data?.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
224
+ // Restricted: nothing allowed except posts and comments (queries only)
225
+ const userSchema = withPermissions(
226
+ restricted('user', { posts: { query: true }, comments: { query: true } }),
227
+ )
228
+
229
+ // Restricted with nothing granted — only Query { _empty: Boolean }
230
+ const anonSchema = withPermissions(restricted('anon'))
231
+ ```
232
+
233
+ Schemas are cached by `id` — calling `withPermissions` with the same `id` returns the same `GraphQLSchema` instance.
234
+
235
+ ### Permission Helpers
236
+
237
+ | Helper | Description |
238
+ |--------|-------------|
239
+ | `permissive(id, tables?)` | All tables allowed by default; overrides deny |
240
+ | `restricted(id, tables?)` | Nothing allowed by default; overrides grant |
241
+ | `readOnly()` | Shorthand for `{ query: true, insert: false, update: false, delete: false }` |
242
+
243
+ ### Table Access
244
+
245
+ Each table can be set to `true` (all operations), `false` (excluded entirely), or a `TableAccess` object:
246
+
247
+ ```ts
248
+ type TableAccess = {
249
+ query?: boolean // list + single + count
250
+ insert?: boolean // insert + insertSingle
251
+ update?: boolean
252
+ delete?: boolean
138
253
  }
139
254
  ```
140
255
 
141
- ## Framework Integration Examples
256
+ In **permissive** mode, omitted fields default to `true`. In **restricted** mode, omitted fields default to `false`.
257
+
258
+ ### Introspection Behavior
259
+
260
+ - `false` (excluded table) — removed from everywhere: no entry points, no relation fields on other types, no filter fields
261
+ - `readOnly()` — table types exist (accessible via relations), but only query entry points; no mutations
262
+ - Granular control — e.g. `{ query: true, insert: true, delete: false }` removes only `deleteFrom{Table}` mutation
142
263
 
143
- `buildSchema()` returns a standard `GraphQLSchema` — here's how to serve it from popular frameworks.
264
+ ## Row-Level Security
144
265
 
145
- ### Next.js App Router
266
+ Generate hooks that inject WHERE clauses for row-level filtering. Compose with other hooks using `mergeHooks`.
146
267
 
147
268
  ```ts
148
- // app/api/graphql/route.ts
149
- import { createYoga } from 'graphql-yoga'
150
- import { buildSchema } from 'drizzle-graphql-suite/schema'
151
- import { db } from '@/db'
269
+ import { buildSchema, withRowSecurity, mergeHooks } from '@graphql-suite/schema'
270
+
271
+ const { schema } = buildSchema(db, {
272
+ hooks: mergeHooks(
273
+ withRowSecurity({
274
+ posts: (context) => ({ authorId: { eq: context.user.id } }),
275
+ }),
276
+ myOtherHooks,
277
+ ),
278
+ })
279
+ ```
152
280
 
153
- const { schema } = buildSchema(db)
281
+ ### `withRowSecurity(rules)`
154
282
 
155
- const { handleRequest } = createYoga({
156
- schema,
157
- graphqlEndpoint: '/api/graphql',
158
- fetchAPI: { Response },
283
+ Generates a `HooksConfig` with `before` hooks on `query`, `querySingle`, `count`, `update`, and `delete` operations. Each rule is a function that receives the GraphQL context and returns a WHERE filter object.
284
+
285
+ ### `mergeHooks(...configs)`
286
+
287
+ Deep-merges multiple `HooksConfig` objects:
288
+
289
+ - **`before` hooks** — chained sequentially; each receives the previous hook's modified args
290
+ - **`after` hooks** — chained sequentially; each receives the previous hook's result
291
+ - **`resolve` hooks** — last one wins (cannot be composed)
292
+
293
+ ## Hooks System
294
+
295
+ Hooks intercept operations for auth, validation, audit logging, or custom resolution.
296
+
297
+ ### Hook Types
298
+
299
+ Each operation supports either **before/after** hooks or a **resolve** hook (not both):
300
+
301
+ | Hook | Timing | Use Case |
302
+ |------|--------|----------|
303
+ | `before` | Before default resolver | Auth checks, argument transformation, pass data to `after` |
304
+ | `after` | After default resolver | Audit logging, result transformation |
305
+ | `resolve` | Replaces default resolver | Full control, custom data sources |
306
+
307
+ ### Operations
308
+
309
+ | Operation | Type | Description |
310
+ |-----------|------|-------------|
311
+ | `query` | Read | List query |
312
+ | `querySingle` | Read | Single record query |
313
+ | `count` | Read | Count query |
314
+ | `insert` | Write | Batch insert |
315
+ | `insertSingle` | Write | Single record insert |
316
+ | `update` | Write | Update mutation |
317
+ | `delete` | Write | Delete mutation |
318
+
319
+ ### Example
320
+
321
+ ```ts
322
+ buildSchema(db, {
323
+ hooks: {
324
+ user: {
325
+ query: {
326
+ before: async ({ args, context }) => {
327
+ if (!context.user) throw new Error('Unauthorized')
328
+ // Optionally modify args or pass data to after hook
329
+ return { args, data: { startTime: Date.now() } }
330
+ },
331
+ after: async ({ result, beforeData }) => {
332
+ console.log(`Query took ${Date.now() - beforeData.startTime}ms`)
333
+ return result
334
+ },
335
+ },
336
+ delete: {
337
+ resolve: async ({ args, context, defaultResolve }) => {
338
+ // Soft delete instead of hard delete
339
+ return defaultResolve({ ...args, set: { deletedAt: new Date() } })
340
+ },
341
+ },
342
+ },
343
+ },
159
344
  })
345
+ ```
160
346
 
161
- export { handleRequest as GET, handleRequest as POST }
347
+ ## Relation Filtering
348
+
349
+ Filter by related rows using EXISTS subqueries with `some`, `every`, and `none` quantifiers.
350
+
351
+ ```graphql
352
+ query {
353
+ users(where: {
354
+ posts: {
355
+ some: { published: { eq: true } }
356
+ none: { flagged: { eq: true } }
357
+ }
358
+ }) {
359
+ id
360
+ name
361
+ posts {
362
+ title
363
+ }
364
+ }
365
+ }
162
366
  ```
163
367
 
164
- ### ElysiaJS
368
+ - **`some`** — at least one related row matches
369
+ - **`every`** — all related rows match
370
+ - **`none`** — no related rows match
371
+
372
+ For one-to-one relations, filters apply directly (no quantifiers needed):
373
+
374
+ ```graphql
375
+ query {
376
+ posts(where: {
377
+ author: { role: { eq: "admin" } }
378
+ }) {
379
+ title
380
+ }
381
+ }
382
+ ```
383
+
384
+ ## Generated Operations
385
+
386
+ | Pattern | Example (`user` table) | Type |
387
+ |---------|----------------------|------|
388
+ | `{table}` | `user` | Single query |
389
+ | `{table}{listSuffix}` | `users` | List query |
390
+ | `{table}Count` | `userCount` | Count query |
391
+ | `insertInto{Table}` | `insertIntoUser` | Batch insert |
392
+ | `insertInto{Table}Single` | `insertIntoUserSingle` | Single insert |
393
+ | `update{Table}` | `updateUser` | Update |
394
+ | `deleteFrom{Table}` | `deleteFromUser` | Delete |
395
+
396
+ ## Column Type Mapping
397
+
398
+ | Drizzle Type | GraphQL Type |
399
+ |-------------|--------------|
400
+ | `boolean` | `Boolean` |
401
+ | `text`, `varchar`, `char` | `String` |
402
+ | `integer`, `smallint`, `serial`, `smallserial`, `bigserial` | `Int` |
403
+ | `real`, `doublePrecision`, `numeric` | `Float` |
404
+ | `bigint` | `String` |
405
+ | `date`, `timestamp`, `time` | `String` |
406
+ | `json`, `jsonb` | `JSON` (custom scalar) |
407
+ | `bytea` | `[Int!]` |
408
+ | `vector` | `[Float!]` |
409
+ | `geometry` | `PgGeometryObject { x, y }` |
410
+ | `enum` | Generated `GraphQLEnumType` |
411
+
412
+ ## Code Generation
413
+
414
+ > **When to use:** Only when the client is in a separate repository that cannot import the Drizzle schema directly. For same-repo setups, [`createDrizzleClient`](https://github.com/annexare/graphql-suite/tree/main/packages/client) infers all types automatically — no codegen needed.
415
+
416
+ Three code generation functions for producing static artifacts from a GraphQL schema:
417
+
418
+ ### `generateSDL(schema)`
419
+
420
+ Generates the GraphQL Schema Definition Language string.
165
421
 
166
422
  ```ts
167
- // server.ts
168
- import { Elysia } from 'elysia'
169
- import { yoga } from '@elysiajs/graphql-yoga'
170
- import { buildSchema } from 'drizzle-graphql-suite/schema'
171
- import { db } from './db'
423
+ import { buildSchemaFromDrizzle, generateSDL } from '@graphql-suite/schema'
424
+ import * as drizzleSchema from './db/schema'
425
+ import { writeFileSync } from 'node:fs'
172
426
 
173
- const { schema } = buildSchema(db)
427
+ const { schema } = buildSchemaFromDrizzle(drizzleSchema)
428
+ writeFileSync('schema.graphql', generateSDL(schema))
429
+ ```
174
430
 
175
- new Elysia()
176
- .use(yoga({ schema }))
177
- .listen(3000)
431
+ ### `generateTypes(schema, options?)`
432
+
433
+ Generates TypeScript types: wire format types (Date → string), filter types, insert/update input types, and orderBy types. Optionally imports Drizzle types for precise wire format derivation.
434
+
435
+ ```ts
436
+ import { generateTypes } from '@graphql-suite/schema'
437
+
438
+ const types = generateTypes(schema, {
439
+ drizzle: {
440
+ importPath: '@myapp/db/schema',
441
+ typeNames: { userProfile: 'UserProfile' },
442
+ },
443
+ })
444
+ writeFileSync('generated/types.ts', types)
178
445
  ```
179
446
 
180
- ## AI Agent Skill
447
+ ### `generateEntityDefs(schema, options?)`
181
448
 
182
- This repo includes a [skills.sh](https://skills.sh) skill that provides AI coding agents (Claude Code, Cursor, etc.) with accurate, up-to-date guidance for all three packages.
449
+ Generates a runtime schema descriptor object and `EntityDefs` type for the client package. Use this instead of `createDrizzleClient` when the client is in a separate repo and can't import the Drizzle schema.
183
450
 
184
- ```bash
185
- bunx skills add annexare/drizzle-graphql-suite
186
- # or: npx skills add annexare/drizzle-graphql-suite
451
+ ```ts
452
+ import { generateEntityDefs } from '@graphql-suite/schema'
453
+
454
+ const entityDefs = generateEntityDefs(schema, {
455
+ drizzle: { importPath: '@myapp/db/schema' },
456
+ })
457
+ writeFileSync('generated/entity-defs.ts', entityDefs)
458
+ ```
459
+
460
+ ### Full Codegen Script
461
+
462
+ ```ts
463
+ import { buildSchemaFromDrizzle, generateSDL, generateTypes, generateEntityDefs } from '@graphql-suite/schema'
464
+ import * as drizzleSchema from './db/schema'
465
+ import { writeFileSync, mkdirSync } from 'node:fs'
466
+
467
+ const { schema } = buildSchemaFromDrizzle(drizzleSchema)
468
+
469
+ mkdirSync('generated', { recursive: true })
470
+ writeFileSync('generated/schema.graphql', generateSDL(schema))
471
+ writeFileSync('generated/types.ts', generateTypes(schema, {
472
+ drizzle: { importPath: '@myapp/db/schema' },
473
+ }))
474
+ writeFileSync('generated/entity-defs.ts', generateEntityDefs(schema, {
475
+ drizzle: { importPath: '@myapp/db/schema' },
476
+ }))
187
477
  ```
188
478
 
189
- ## License
479
+ ## `GeneratedEntities` Type
480
+
481
+ The return type from `buildEntities()` and `buildSchema()`:
482
+
483
+ ```ts
484
+ type GeneratedEntities = {
485
+ queries: Record<string, GraphQLFieldConfig<any, any>>
486
+ mutations: Record<string, GraphQLFieldConfig<any, any>>
487
+ inputs: Record<string, GraphQLInputObjectType>
488
+ types: Record<string, GraphQLObjectType>
489
+ }
490
+ ```
190
491
 
191
- MIT
492
+ - **`queries`** — all generated query field configs, spreadable into a parent schema
493
+ - **`mutations`** — all generated mutation field configs
494
+ - **`inputs`** — filter, insert, update, and orderBy input types by name
495
+ - **`types`** — output object types by table name
@@ -0,0 +1,10 @@
1
+ import { type Column, type SQL, type Table } from 'drizzle-orm';
2
+ import { type PgDatabase } from 'drizzle-orm/pg-core';
3
+ import type { DbAdapter } from './types';
4
+ export declare class PgAdapter implements DbAdapter {
5
+ readonly supportsReturning = true;
6
+ isTable(value: unknown): boolean;
7
+ executeInsert(db: PgDatabase<any, any, any>, table: Table, values: Record<string, any>[], returningColumns?: Record<string, Column>): Promise<any[]>;
8
+ executeUpdate(db: PgDatabase<any, any, any>, table: Table, set: Record<string, any>, where: SQL | undefined, returningColumns?: Record<string, Column>): Promise<any[]>;
9
+ executeDelete(db: PgDatabase<any, any, any>, table: Table, where: SQL | undefined, returningColumns?: Record<string, Column>): Promise<any[]>;
10
+ }
@@ -0,0 +1,11 @@
1
+ import type { Column, SQL, Table } from 'drizzle-orm';
2
+ import type { PgDatabase } from 'drizzle-orm/pg-core';
3
+ export interface DbAdapter {
4
+ /** Identifies tables in the schema (e.g., is(value, PgTable)) */
5
+ isTable(value: unknown): boolean;
6
+ /** Whether mutations can return data (PG: yes via RETURNING, MySQL: no) */
7
+ readonly supportsReturning: boolean;
8
+ executeInsert(db: PgDatabase<any, any, any>, table: Table, values: Record<string, any>[], returningColumns?: Record<string, Column>): Promise<any[]>;
9
+ executeUpdate(db: PgDatabase<any, any, any>, table: Table, set: Record<string, any>, where: SQL | undefined, returningColumns?: Record<string, Column>): Promise<any[]>;
10
+ executeDelete(db: PgDatabase<any, any, any>, table: Table, where: SQL | undefined, returningColumns?: Record<string, Column>): Promise<any[]>;
11
+ }
package/case-ops.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare const uncapitalize: <T extends string>(input: T) => Uncapitalize<T>;
2
+ export declare const capitalize: <T extends string>(input: T) => Capitalize<T>;
package/codegen.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { GraphQLSchema } from 'graphql';
2
+ export type CodegenOptions = {
3
+ drizzle?: {
4
+ /** Import path for Drizzle schema types (e.g. '@ir/core/db/schema') */
5
+ importPath: string;
6
+ /** Override mapping: Drizzle table name → export type name (e.g. { overrideToAsset: 'OverrideAsset' }) */
7
+ typeNames?: Record<string, string>;
8
+ };
9
+ };
10
+ export declare function generateSDL(schema: GraphQLSchema): string;
11
+ export declare function generateTypes(schema: GraphQLSchema, options?: CodegenOptions): string;
12
+ export declare function generateEntityDefs(schema: GraphQLSchema, options?: CodegenOptions): string;
@@ -0,0 +1,12 @@
1
+ import type { Relation } from 'drizzle-orm';
2
+ import { type Column, type Table } from 'drizzle-orm';
3
+ export type TableNamedRelations = {
4
+ relation: Relation;
5
+ targetTableName: string;
6
+ };
7
+ export declare const remapToGraphQLCore: (key: string, value: any, tableName: string, column: Column, relationMap?: Record<string, Record<string, TableNamedRelations>>) => any;
8
+ export declare const remapToGraphQLSingleOutput: (queryOutput: Record<string, any>, tableName: string, table: Table, relationMap?: Record<string, Record<string, TableNamedRelations>>) => Record<string, any>;
9
+ export declare const remapToGraphQLArrayOutput: (queryOutput: Record<string, any>[], tableName: string, table: Table, relationMap?: Record<string, Record<string, TableNamedRelations>>) => Record<string, any>[];
10
+ export declare const remapFromGraphQLCore: (value: any, column: Column, columnName: string) => any;
11
+ export declare const remapFromGraphQLSingleInput: (queryInput: Record<string, any>, table: Table) => Record<string, any>;
12
+ export declare const remapFromGraphQLArrayInput: (queryInput: Record<string, any>[], table: Table) => Record<string, any>[];
@@ -0,0 +1,2 @@
1
+ import { GraphQLScalarType } from 'graphql';
2
+ export declare const GraphQLJSON: GraphQLScalarType<any, unknown>;
@@ -0,0 +1,7 @@
1
+ import type { Column } from 'drizzle-orm';
2
+ import { GraphQLEnumType, GraphQLInputObjectType, GraphQLList, GraphQLNonNull, GraphQLObjectType, type GraphQLScalarType } from 'graphql';
3
+ export type ConvertedColumn<TIsInput extends boolean = false> = {
4
+ type: GraphQLScalarType | GraphQLEnumType | GraphQLNonNull<GraphQLScalarType> | GraphQLNonNull<GraphQLEnumType> | GraphQLList<GraphQLScalarType> | GraphQLList<GraphQLNonNull<GraphQLScalarType>> | GraphQLNonNull<GraphQLList<GraphQLScalarType>> | GraphQLNonNull<GraphQLList<GraphQLNonNull<GraphQLScalarType>>> | (TIsInput extends true ? GraphQLInputObjectType | GraphQLNonNull<GraphQLInputObjectType> : GraphQLObjectType | GraphQLNonNull<GraphQLObjectType>);
5
+ description?: string;
6
+ };
7
+ export declare const drizzleColumnToGraphQLType: <TColumn extends Column, TIsInput extends boolean>(column: TColumn, columnName: string, tableName: string, forceNullable?: boolean, defaultIsNullable?: boolean, isInput?: TIsInput) => ConvertedColumn<TIsInput>;
package/index.d.ts CHANGED
@@ -1 +1,32 @@
1
- export * from '@drizzle-graphql-suite/schema'
1
+ import type { PgDatabase } from 'drizzle-orm/pg-core';
2
+ import type { GraphQLSchema } from 'graphql';
3
+ import type { BuildSchemaConfig, GeneratedEntities, PermissionConfig } from './types';
4
+ export { GraphQLJSON } from './graphql/scalars';
5
+ export declare const buildSchema: (db: PgDatabase<any, any, any>, config?: BuildSchemaConfig) => {
6
+ schema: GraphQLSchema;
7
+ entities: GeneratedEntities;
8
+ withPermissions: (permissions: PermissionConfig) => GraphQLSchema;
9
+ clearPermissionCache: (id?: string) => void;
10
+ };
11
+ export declare const buildEntities: (db: PgDatabase<any, any, any>, config?: BuildSchemaConfig) => GeneratedEntities;
12
+ /**
13
+ * Build a GraphQL schema directly from Drizzle schema exports — no database
14
+ * connection or `.env` required. Creates a lightweight mock db instance that
15
+ * satisfies `SchemaBuilder`'s metadata needs (table definitions, relations,
16
+ * table name mapping) without an actual connection.
17
+ *
18
+ * Resolver functions on the returned entities are stubs — this is intended
19
+ * for schema introspection and code generation, not query execution.
20
+ */
21
+ export declare const buildSchemaFromDrizzle: (drizzleSchema: Record<string, unknown>, config?: BuildSchemaConfig) => {
22
+ schema: GraphQLSchema;
23
+ entities: GeneratedEntities;
24
+ withPermissions: (permissions: PermissionConfig) => GraphQLSchema;
25
+ clearPermissionCache: (id?: string) => void;
26
+ };
27
+ export type { CodegenOptions } from './codegen';
28
+ export { generateEntityDefs, generateSDL, generateTypes } from './codegen';
29
+ export { permissive, readOnly, restricted } from './permissions';
30
+ export { mergeHooks, withRowSecurity } from './row-security';
31
+ export { SchemaBuilder } from './schema-builder';
32
+ export * from './types';
package/index.js CHANGED
@@ -1 +1,11 @@
1
- export * from '@drizzle-graphql-suite/schema'
1
+ /** @graphql-suite/schema v0.9.0 | MIT */
2
+ import{createTableRelationsHelpers as L$,extractTablesRelationalConfig as T$}from"drizzle-orm";import{and as C,asc as a1,count as e1,createTableRelationsHelpers as $$,desc as Y$,eq as A1,exists as K1,getTableColumns as G,getTableName as P1,gt as Z$,gte as _$,ilike as U$,inArray as X$,is as r,isNotNull as J$,isNull as W$,like as K$,lt as P$,lte as H$,ne as B$,normalizeRelation as M$,not as H1,notIlike as D$,notInArray as j$,notLike as V$,One as B1,or as I1,Relations as E$,sql as Q}from"drizzle-orm";import{PgTable as R$}from"drizzle-orm/pg-core";import{GraphQLBoolean as $1,GraphQLEnumType as k$,GraphQLError as I,GraphQLInputObjectType as O,GraphQLInt as g,GraphQLList as h,GraphQLNonNull as F,GraphQLObjectType as v,GraphQLSchema as M1,GraphQLString as Y1,printSchema as w$}from"graphql";import{parseResolveInfo as p}from"graphql-parse-resolve-info";import{is as Q1}from"drizzle-orm";import{PgTable as v1}from"drizzle-orm/pg-core";class J1{supportsReturning=!0;isTable($){return Q1($,v1)}async executeInsert($,_,X,U){let Y=$.insert(_).values(X);if(U)Y=Y.returning(U);return Y=Y.onConflictDoNothing(),await Y}async executeUpdate($,_,X,U,Y){let Z=$.update(_).set(X);if(U)Z=Z.where(U);if(Y)Z=Z.returning(Y);return await Z}async executeDelete($,_,X,U){let Y=$.delete(_);if(X)Y=Y.where(X);if(U)Y=Y.returning(U);return await Y}}var y=($)=>$.length?`${$[0]?.toLocaleLowerCase()}${$.length>1?$.slice(1,$.length):""}`:$,R=($)=>$.length?`${$[0]?.toLocaleUpperCase()}${$.length>1?$.slice(1,$.length):""}`:$;import{getTableColumns as L1}from"drizzle-orm";import{GraphQLError as N}from"graphql";var R1=($,_,X,U,Y)=>{if(_ instanceof Date)return _.toISOString();if(_ instanceof Buffer)return Array.from(_);if(typeof _==="bigint")return _.toString();if(Array.isArray(_)){let Z=Y?.[X]?.[$];if(Z)return b(_,Z.targetTableName,Z.relation.referencedTable,Y);if(U.columnType==="PgGeometry"||U.columnType==="PgVector")return _;return _.map((J)=>R1($,J,X,U,Y))}if(typeof _==="object"){let Z=Y?.[X]?.[$];if(Z)return o(_,Z.targetTableName,Z.relation.referencedTable,Y);if(U.columnType==="PgGeometryObject")return _;if(U.dataType==="json")return _;return JSON.stringify(_)}return _},o=($,_,X,U)=>{for(let[Y,Z]of Object.entries($))if(Z===void 0||Z===null)delete $[Y];else $[Y]=R1(Y,Z,_,X[Y],U);return $},b=($,_,X,U)=>{for(let Y of $)o(Y,_,X,U);return $},t=($,_,X)=>{switch(_.dataType){case"date":{let U=new Date($);if(Number.isNaN(U.getTime()))throw new N(`Field '${X}' is not a valid date!`);return U}case"buffer":{if(!Array.isArray($))throw new N(`Field '${X}' is not an array!`);return Buffer.from($)}case"json":{if(_.columnType==="PgGeometryObject")return $;return $}case"array":{if(!Array.isArray($))throw new N(`Field '${X}' is not an array!`);if(_.columnType==="PgGeometry"&&$.length!==2)throw new N(`Invalid float tuple in field '${X}': expected array with length of 2, received ${$.length}`);return $}case"bigint":try{return BigInt($)}catch{throw new N(`Field '${X}' is not a BigInt!`)}default:return $}},a=($,_)=>{for(let[X,U]of Object.entries($))if(U===void 0)delete $[X];else{let Y=L1(_)[X];if(!Y)throw new N(`Unknown column: ${X}`);if(U===null&&Y.notNull){delete $[X];continue}$[X]=t(U,Y,X)}return $},k1=($,_)=>{for(let X of $)a(X,_);return $};import{is as c}from"drizzle-orm";import{PgBigInt53 as f1,PgBigSerial53 as g1,PgInteger as h1,PgSerial as d1,PgSmallInt as y1,PgSmallSerial as N1}from"drizzle-orm/pg-core";import{GraphQLBoolean as b1,GraphQLEnumType as c1,GraphQLFloat as f,GraphQLInputObjectType as p1,GraphQLInt as w1,GraphQLList as e,GraphQLNonNull as n,GraphQLObjectType as u1,GraphQLString as W1}from"graphql";import{GraphQLScalarType as T1,Kind as T}from"graphql";var s=new T1({name:"JSON",description:"The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).",serialize($){return $},parseValue($){return $},parseLiteral($){switch($.kind){case T.STRING:case T.BOOLEAN:return $.value;case T.INT:case T.FLOAT:return parseFloat($.value);case T.OBJECT:{let _=Object.create(null);return $.fields.forEach((X)=>{_[X.name.value]=s.parseLiteral(X.value)}),_}case T.LIST:return $.values.map((_)=>s.parseLiteral(_));case T.NULL:return null;default:return}}});var m1=/^[a-zA-Z0-9_]+$/,S1=new WeakMap,o1=($,_,X)=>{let U=S1.get($);if(U)return U;let Y=new c1({name:`${R(X)}${R(_)}Enum`,values:Object.fromEntries(($.enumValues??[]).map((Z,J)=>{let W=Z.replace(/-/g,"_");return[m1.test(W)?W:`Option${J}`,{value:Z,description:`Value: ${Z}`}]}))});return S1.set($,Y),Y},s1=new u1({name:"PgGeometryObject",fields:{x:{type:f},y:{type:f}}}),n1=new p1({name:"PgGeometryObjectInput",fields:{x:{type:f},y:{type:f}}}),x1=($,_,X,U)=>{switch($.dataType){case"boolean":return{type:b1,description:"Boolean"};case"json":return $.columnType==="PgGeometryObject"?{type:U?n1:s1,description:"Geometry points XY"}:{type:s,description:"JSON"};case"date":return{type:W1,description:"Date"};case"string":if($.enumValues?.length)return{type:o1($,_,X)};return{type:W1,description:"String"};case"bigint":return{type:W1,description:"BigInt"};case"number":return c($,h1)||c($,y1)||c($,f1)||c($,d1)||c($,N1)||c($,g1)?{type:w1,description:"Integer"}:{type:f,description:"Float"};case"buffer":return{type:new e(new n(w1)),description:"Buffer"};case"array":{if($.columnType==="PgVector")return{type:new e(new n(f)),description:"Array<Float>"};if($.columnType==="PgGeometry")return{type:new e(new n(f)),description:"Tuple<[Float, Float]>"};let Y=x1($.baseColumn,_,X,U);return{type:new e(new n(Y.type)),description:`Array<${Y.description}>`}}default:throw Error(`GraphQL-Suite Error: Type ${$.dataType} is not implemented!`)}},i=($,_,X,U=!1,Y=!1,Z=!1)=>{let J=x1($,_,X,Z);if(["string","boolean","number"].find((K)=>K===$.dataType))delete J.description;if(U)return J;if($.notNull&&!(Y&&($.hasDefault||$.defaultFn)))return{type:new n(J.type),description:J.description};return J};var i1=()=>({query:!0,insert:!1,update:!1,delete:!1}),r1=($,_)=>({id:$,mode:"permissive",tables:_}),l1=($,_)=>({id:$,mode:"restricted",tables:_});function F1($,_,X){let U=[...$.tables?.exclude??[]],Y={...$.tables?.config??{}},Z={};for(let W of X){if(U.includes(W))continue;let K=t1(_,W);if(K===!1){U.push(W);continue}if(K===!0)continue;let P=_.mode==="permissive",M=K.query??P,H=K.insert??P,B=K.update??P,D=K.delete??P;if(!M&&!H&&!B&&!D){U.push(W);continue}if(Y[W]={...Y[W],queries:M},!(H||B||D))Y[W]={...Y[W],mutations:!1};else if(Y[W]={...Y[W],mutations:!0},!H||!B||!D)Z[W]={insert:H,update:B,delete:D}}return{config:{...$,tables:{exclude:U.length?U:void 0,config:Object.keys(Y).length?Y:void 0}},mutationFilter:Z}}function t1($,_){let X=$.tables?.[_];if($.mode==="permissive"){if(X===void 0)return!0;return X}if(X===void 0)return!1;return X}function z1($,_){for(let[X,U]of Object.entries(_)){let Y=R(X);if(!U.insert)delete $[`insertInto${Y}`],delete $[`insertInto${Y}Single`];if(!U.update)delete $[`update${Y}`];if(!U.delete)delete $[`deleteFrom${Y}`]}}class d{db;tables;relationMap;relationalSchema;tableNamesMap;config;hooks;adapter;suffixes;limitRelationDepth;limitSelfRelationDepth;allTableNames;excludedTables;tableOperations;pruneRelations;filterTypes=new Map;orderByTypes=new Map;filterValueTypes=new Map;selectFieldTypes=new Map;innerOrder;constructor($,_){this.db=$,this.config=_??{},this.hooks=_?.hooks??{},this.adapter=new J1,this.suffixes={list:_?.suffixes?.list??"",single:_?.suffixes?.single??"Single"},this.limitRelationDepth=_?.limitRelationDepth??3,this.limitSelfRelationDepth=_?.limitSelfRelationDepth??1,this.pruneRelations=new Map(Object.entries(_?.pruneRelations??{}));let X=$._.fullSchema;if(!X)throw Error("GraphQL-Suite Error: Schema not found in drizzle instance. Make sure schema is passed to the drizzle() constructor.");let U=$._;if(this.relationalSchema=U.schema,this.tableNamesMap=U.tableNamesMap,typeof this.limitRelationDepth==="number"){if(this.limitRelationDepth<0||this.limitRelationDepth!==~~this.limitRelationDepth)throw Error("GraphQL-Suite Error: config.limitRelationDepth is supposed to be nonnegative integer or undefined!")}if(this.limitSelfRelationDepth<1||this.limitSelfRelationDepth!==~~this.limitSelfRelationDepth)throw Error("GraphQL-Suite Error: config.limitSelfRelationDepth must be a positive integer!");if(this.suffixes.list===this.suffixes.single)throw Error("GraphQL-Suite Error: List and single query suffixes cannot be the same. This would create conflicting GraphQL field names.");this.innerOrder=new O({name:"InnerOrder",fields:{direction:{type:new F(new k$({name:"OrderDirection",description:"Order by direction",values:{asc:{value:"asc",description:"Ascending order"},desc:{value:"desc",description:"Descending order"}}}))},priority:{type:new F(g),description:"Priority of current field"}}}),this.tables=this.extractTables(X),this.allTableNames=Object.keys(this.tables),this.excludedTables=new Set(_?.tables?.exclude??[]),this.tableOperations=_?.tables?.config??{};for(let Y of this.excludedTables)delete this.tables[Y];this.relationMap=this.buildRelationMap(X)}toFieldMap($,_){return{...$,..._}}buildEntities(){let $={},_={},X={},U={};for(let Y of Object.keys(this.tables)){let Z=this.tableOperations[Y],J=Z?.queries!==!1,W=Z?.mutations!==!1&&this.config.mutations!==!1;if(!J&&!W)continue;let K=this.generateTableTypes(Y),{insertInput:P,updateInput:M,tableFilters:H,tableOrder:B}=K.inputs,{selectSingleOutput:D,selectArrOutput:j,singleTableItemOutput:V,arrTableItemOutput:E}=K.outputs;if(J){let k=this.createQueryResolver(Y,B,H);$[k.name]={type:j,args:k.args,resolve:k.resolver};let w=this.createSingleQueryResolver(Y,B,H);$[w.name]={type:D,args:w.args,resolve:w.resolver};let x=this.createCountResolver(Y,H);$[x.name]={type:g,args:x.args,resolve:x.resolver},X[H.name]=H,X[B.name]=B,U[D.name]=D}if(W){let k=this.createInsertResolver(Y,P);_[k.name]={type:E,args:k.args,resolve:k.resolver};let w=this.createInsertSingleResolver(Y,P);_[w.name]={type:V,args:w.args,resolve:w.resolver};let x=this.createUpdateResolver(Y,M,H);_[x.name]={type:E,args:x.args,resolve:x.resolver};let S=this.createDeleteResolver(Y,H);_[S.name]={type:E,args:S.args,resolve:S.resolver},X[P.name]=P,X[M.name]=M,X[H.name]=H,X[B.name]=B,U[V.name]=V}}return{queries:$,mutations:_,inputs:X,types:U}}build(){let $=this.buildEntities(),{queries:_,mutations:X,inputs:U,types:Y}=$,Z={types:[...Object.values(U),...Object.values(Y)],query:new v({name:"Query",fields:_})};if(this.config.mutations!==!1)Z.mutation=new v({name:"Mutation",fields:X});let J=new M1(Z);this.logDebugInfo(J);let W=new Map,K=this.db,P=this.config,M=this.allTableNames;return{schema:J,entities:$,withPermissions:(D)=>{let j=W.get(D.id);if(j)return j;let{config:V,mutationFilter:E}=F1(P,D,M),k=new Set(V.tables?.exclude??[]);if(!M.some((A)=>!k.has(A))){let A=new M1({query:new v({name:"Query",fields:{_empty:{type:$1}}})});return W.set(D.id,A),A}let S=new d(K,V).buildEntities();if(Object.keys(E).length)z1(S.mutations,E);let z={types:[...Object.values(S.inputs),...Object.values(S.types)],query:new v({name:"Query",fields:Object.keys(S.queries).length?S.queries:{_empty:{type:$1}}})};if(V.mutations!==!1&&Object.keys(S.mutations).length)z.mutation=new v({name:"Mutation",fields:S.mutations});let L=new M1(z);return W.set(D.id,L),L},clearPermissionCache:(D)=>{if(D)W.delete(D);else W.clear()}}}logDebugInfo($){let _=this.config.debug;if(!_)return;let X=_===!0||typeof _==="object"&&_.schemaSize!==!1,U=typeof _==="object"&&_.relationTree===!0;if(X){let Y=w$($),Z=Object.keys($.getTypeMap()).filter((J)=>!J.startsWith("__")).length;console.info(`[drizzle-graphql] Schema: ${Y.length} bytes, ${Z} types`)}if(U)for(let[Y,Z]of Object.entries(this.relationMap)){let J=Object.keys(Z);if(J.length)console.info(`[drizzle-graphql] ${Y}: ${J.join(", ")}`)}}extractTables($){let _=Object.entries($).filter(([X,U])=>r(U,R$));if(!_.length)throw Error("GraphQL-Suite Error: No tables detected in Drizzle-ORM's database instance. Did you forget to pass schema to drizzle constructor?");return Object.fromEntries(_)}getTable($){let _=this.tables[$];if(!_)throw Error(`GraphQL-Suite Error: Table '${$}' not found.`);return _}buildRelationMap($){let _=Object.entries($),X=Object.entries(this.tables),U=_.filter(([Y,Z])=>r(Z,E$)).map(([Y,Z])=>{let J=X.find(([W,K])=>K===Z.table);if(!J)return null;return[J[0],Z]}).filter((Y)=>Y!==null).map(([Y,Z])=>[Y,Z.config($$(this.getTable(Y)))]);return Object.fromEntries(U.map(([Y,Z])=>{let J=Object.fromEntries(Object.entries(Z).map(([W,K])=>{let P=X.find(([M,H])=>H===K.referencedTable);if(!P)return null;return[W,{relation:K,targetTableName:P[0]}]}).filter((W)=>W!==null));return[Y,J]}))}getFilterType($){let _=this.filterTypes.get($);if(_)return _;let X=new O({name:`${R($)}Filters`,fields:()=>{let U=this.getFilterValues($),Y=this.buildRelationFilterFields($);return{...U,...Y,OR:{type:new h(new F(new O({name:`${R($)}FiltersOr`,fields:()=>({...U,...Y})})))}}}});return this.filterTypes.set($,X),X}getOrderByType($){let _=this.orderByTypes.get($);if(_)return _;let X=this.getTable($),U=G(X),Y=Object.fromEntries(Object.entries(U).map(([J])=>[J,{type:this.innerOrder}])),Z=new O({name:`${R($)}OrderBy`,fields:Y});return this.orderByTypes.set($,Z),Z}getFilterValues($){let _=this.filterValueTypes.get($);if(_)return _;let X=this.getTable($),U=G(X),Y=Object.fromEntries(Object.entries(U).map(([Z,J])=>[Z,{type:this.generateColumnFilterValues(J,$,Z)}]));return this.filterValueTypes.set($,Y),Y}getSelectFields($){let _=this.selectFieldTypes.get($);if(_)return _;let X=this.getTable($),U=G(X),Y=Object.fromEntries(Object.entries(U).map(([Z,J])=>[Z,i(J,Z,$)]));return this.selectFieldTypes.set($,Y),Y}generateColumnFilterValues($,_,X){let U=i($,X,_,!0,!1,!0),Y=new h(new F(U.type)),Z={eq:{type:U.type,description:U.description},ne:{type:U.type,description:U.description},lt:{type:U.type,description:U.description},lte:{type:U.type,description:U.description},gt:{type:U.type,description:U.description},gte:{type:U.type,description:U.description},like:{type:Y1},notLike:{type:Y1},ilike:{type:Y1},notIlike:{type:Y1},inArray:{type:Y,description:`Array<${U.description}>`},notInArray:{type:Y,description:`Array<${U.description}>`},isNull:{type:$1},isNotNull:{type:$1}};return new O({name:`${R(_)}${R(X)}Filters`,fields:{...Z,OR:{type:new h(new F(new O({name:`${R(_)}${R(X)}FiltersOr`,fields:Z})))}}})}generateSelectFields($,_,X,U=0,Y=new Set,Z=0,J,W){let K=this.relationMap[$],P=K?Object.entries(K):[],M=X?this.getOrderByType($):void 0,H=this.getFilterType($),B=this.getSelectFields($);if(J||typeof this.limitRelationDepth!=="number"&&Y.has($)||typeof this.limitRelationDepth==="number"&&U>=this.limitRelationDepth||!P.length)return{order:M,filters:H,tableFields:B,relationFields:{}};let D=[],j=new Set(Y).add($),V=U+1;for(let[k,{targetTableName:w,relation:x}]of P){if(W&&!W.includes(k))continue;let S=this.pruneRelations.get(`${$}.${k}`);if(S===!1)continue;let z=`${_}${R(k)}Relation`,L=r(x,B1),A=w===$;if(A&&Z+1>=this.limitSelfRelationDepth)continue;let U1=!A&&Y.has(w)&&typeof this.limitRelationDepth==="number"?Math.max(V,this.limitRelationDepth-1):V,m=S==="leaf",X1=S&&typeof S==="object"?S.only:void 0,q=this.generateSelectFields(w,z,!L,U1,j,A?Z+1:0,m,X1),l=new v({name:z,fields:this.toFieldMap(q.tableFields,q.relationFields)});if(L){D.push([k,{type:l,args:{where:{type:q.filters}}}]);continue}D.push([k,{type:new F(new h(new F(l))),args:{where:{type:q.filters},orderBy:{type:q.order},offset:{type:g},limit:{type:g}}}])}let E=Object.fromEntries(D);return{order:M,filters:H,tableFields:B,relationFields:E}}generateTableTypes($){let _=R($),{tableFields:X,relationFields:U,filters:Y,order:Z}=this.generateSelectFields($,_,!0),J=this.getTable($),W=G(J),K=Object.entries(W),P=Object.fromEntries(K.map(([k,w])=>[k,i(w,k,$,!1,!0,!0)])),M=Object.fromEntries(K.map(([k,w])=>[k,i(w,k,$,!0,!1,!0)])),H=new O({name:`${_}InsertInput`,fields:P}),B=new O({name:`${_}UpdateInput`,fields:M}),D=new v({name:`${_}SelectItem`,fields:this.toFieldMap(X,U)}),j=new F(new h(new F(D))),V=new v({name:`${_}Item`,fields:this.toFieldMap(X)}),E=new F(new h(new F(V)));return{inputs:{insertInput:H,updateInput:B,tableOrder:Z,tableFilters:Y},outputs:{selectSingleOutput:D,selectArrOutput:j,singleTableItemOutput:V,arrTableItemOutput:E}}}buildRelationFilterFields($){let _=this.relationMap[$];if(!_)return{};let X={};for(let[U,{targetTableName:Y,relation:Z}]of Object.entries(_))if(r(Z,B1))X[U]={type:this.getFilterType(Y)};else X[U]={type:new O({name:`${R($)}${R(U)}RelFilter`,fields:()=>({some:{type:this.getFilterType(Y)},every:{type:this.getFilterType(Y)},none:{type:this.getFilterType(Y)}})})};return X}createQueryResolver($,_,X){let U=`${y($)}${this.suffixes.list}`,Y=`${R($)}SelectItem`,Z=this.getTable($),J=this.db.query[$];if(!J)throw Error(`GraphQL-Suite Error: Table ${$} not found in drizzle instance. Did you forget to pass schema to drizzle constructor?`);return{name:U,resolver:async(P,M,H,B)=>{return this.executeWithHooks($,"query",M,H,B,async(D)=>{try{let{offset:j,limit:V,orderBy:E,where:k}=D,w=p(B,{deep:!0}),S=await J.findMany({columns:this.extractColumns(this.getFieldsByTypeName(w,Y),Z),offset:j,limit:V,orderBy:E?this.extractOrderBy(Z,E):void 0,where:k?this.extractAllFilters(Z,$,k):void 0,with:this.relationMap[$]?this.extractRelationsParams($,w,Y):void 0});return b(S,$,Z,this.relationMap)}catch(j){if(typeof j==="object"&&j!==null&&"message"in j&&typeof j.message==="string")throw new I(j.message);throw j}})},args:{offset:{type:g},limit:{type:g},orderBy:{type:_},where:{type:X}}}}createSingleQueryResolver($,_,X){let U=`${y($)}${this.suffixes.single}`,Y=`${R($)}SelectItem`,Z=this.getTable($),J=this.db.query[$];if(!J)throw Error(`GraphQL-Suite Error: Table ${$} not found in drizzle instance. Did you forget to pass schema to drizzle constructor?`);return{name:U,resolver:async(P,M,H,B)=>{return this.executeWithHooks($,"querySingle",M,H,B,async(D)=>{try{let{offset:j,orderBy:V,where:E}=D,k=p(B,{deep:!0}),x=await J.findFirst({columns:this.extractColumns(this.getFieldsByTypeName(k,Y),Z),offset:j,orderBy:V?this.extractOrderBy(Z,V):void 0,where:E?this.extractAllFilters(Z,$,E):void 0,with:this.relationMap[$]?this.extractRelationsParams($,k,Y):void 0});if(!x)return;return o(x,$,Z,this.relationMap)}catch(j){if(typeof j==="object"&&j!==null&&"message"in j&&typeof j.message==="string")throw new I(j.message);throw j}})},args:{offset:{type:g},orderBy:{type:_},where:{type:X}}}}createCountResolver($,_){let X=`${y($)}Count`,U=this.getTable($);return{name:X,resolver:async(J,W,K,P)=>{return this.executeWithHooks($,"count",W,K,P,async(M)=>{try{let{where:H}=M,B=H?this.extractAllFilters(U,$,H):void 0;return await this.executeCountQuery(U,B)}catch(H){if(typeof H==="object"&&H!==null&&"message"in H&&typeof H.message==="string")throw new I(H.message);throw H}})},args:{where:{type:_}}}}createInsertResolver($,_){let X=`insertInto${R($)}`,U=`${R($)}Item`,Y=this.getTable($),Z={values:{type:new F(new h(new F(_)))}};return{name:X,resolver:async(W,K,P,M)=>{return this.executeWithHooks($,"insert",K,P,M,async(H)=>{try{let B=k1(H.values,Y);if(!B.length)throw new I("No values were provided!");let D=p(M,{deep:!0}),j=this.extractColumnsSQLFormat(this.getFieldsByTypeName(D,U),Y),V=await this.adapter.executeInsert(this.db,Y,B,j);return b(V,$,Y)}catch(B){if(typeof B==="object"&&B!==null&&"message"in B&&typeof B.message==="string")throw new I(B.message);throw B}})},args:Z}}createInsertSingleResolver($,_){let X=`insertInto${R($)}Single`,U=`${R($)}Item`,Y=this.getTable($),Z={values:{type:new F(_)}};return{name:X,resolver:async(W,K,P,M)=>{return this.executeWithHooks($,"insertSingle",K,P,M,async(H)=>{try{let B=a(H.values,Y),D=p(M,{deep:!0}),j=this.extractColumnsSQLFormat(this.getFieldsByTypeName(D,U),Y),V=await this.adapter.executeInsert(this.db,Y,[B],j);if(!V[0])return;return o(V[0],$,Y)}catch(B){if(typeof B==="object"&&B!==null&&"message"in B&&typeof B.message==="string")throw new I(B.message);throw B}})},args:Z}}createUpdateResolver($,_,X){let U=`update${R($)}`,Y=`${R($)}Item`,Z=this.getTable($),J={set:{type:new F(_)},where:{type:X}};return{name:U,resolver:async(K,P,M,H)=>{return this.executeWithHooks($,"update",P,M,H,async(B)=>{try{let{where:D,set:j}=B,V=p(H,{deep:!0}),E=this.extractColumnsSQLFormat(this.getFieldsByTypeName(V,Y),Z),k=a(j,Z);if(!Object.keys(k).length)throw new I("Unable to update with no values specified!");let w=D?this.extractAllFilters(Z,$,D):void 0,x=await this.adapter.executeUpdate(this.db,Z,k,w,E);return b(x,$,Z)}catch(D){if(typeof D==="object"&&D!==null&&"message"in D&&typeof D.message==="string")throw new I(D.message);throw D}})},args:J}}createDeleteResolver($,_){let X=`deleteFrom${R($)}`,U=`${R($)}Item`,Y=this.getTable($);return{name:X,resolver:async(W,K,P,M)=>{return this.executeWithHooks($,"delete",K,P,M,async(H)=>{try{let{where:B}=H,D=p(M,{deep:!0}),j=this.extractColumnsSQLFormat(this.getFieldsByTypeName(D,U),Y),V=B?this.extractAllFilters(Y,$,B):void 0,E=await this.adapter.executeDelete(this.db,Y,V,j);return b(E,$,Y)}catch(B){if(typeof B==="object"&&B!==null&&"message"in B&&typeof B.message==="string")throw new I(B.message);throw B}})},args:{where:{type:_}}}}extractColumnFilters($,_,X){if(!X.OR?.length)delete X.OR;let U=Object.entries(X);if(X.OR){let P=[];for(let j of X.OR){let V=this.extractColumnFilters($,_,j);if(V)P.push(V)}let M=P.length>1?I1(...P):P.length===1?P[0]:void 0;if(U.length<=1)return M;let{OR:H,...B}=X,D=this.extractColumnFilters($,_,B);if(!D)return M;if(!M)return D;return C(D,M)}let Y={eq:A1,ne:B$,gt:Z$,gte:_$,lt:P$,lte:H$},Z={like:K$,notLike:V$,ilike:U$,notIlike:D$},J={inArray:X$,notInArray:j$},W={isNull:W$,isNotNull:J$},K=[];for(let[P,M]of U){if(M===null||M===!1)continue;if(P in Y){let H=Y[P];if(H){let B=t(M,$,_);K.push(H($,B))}}else if(P in Z){let H=Z[P];if(H)K.push(H($,M))}else if(P in J){let H=J[P];if(H){if(!M.length)throw new I(`WHERE ${_}: Unable to use operator ${P} with an empty array!`);let B=M.map((D)=>t(D,$,_));K.push(H($,B))}}else if(P in W){let H=W[P];if(H)K.push(H($))}}return K.length?K.length>1?C(...K):K[0]:void 0}extractTableColumnFilters($,_,X){if(!X.OR?.length)delete X.OR;let U=G($),Y=[],Z=[];for(let[W,K]of Object.entries(X)){if(W==="OR")continue;if(K===null)continue;if(U[W])Y.push([W,K]);else Z.push([W,K])}if(X.OR){let W=[];for(let B of X.OR){let D=this.extractAllFilters($,_,B);if(D)W.push(D)}let K=W.length>1?I1(...W):W.length===1?W[0]:void 0;if(Y.length===0&&Z.length===0)return K;let{OR:P,...M}=X,H=this.extractAllFilters($,_,M);if(!H)return K;if(!K)return H;return C(H,K)}let J=[];for(let[W,K]of Y){let P=U[W];if(!P)continue;let M=this.extractColumnFilters(P,W,K);if(M)J.push(M)}for(let[W,K]of Z){let P=this.extractRelationFilters($,_,W,K);if(P)J.push(P)}return J.length?J.length>1?C(...J):J[0]:void 0}extractAllFilters($,_,X){return this.extractTableColumnFilters($,_,X)}extractRelationFilters($,_,X,U){let Y=this.relationMap[_]?.[X];if(!Y)return;let{targetTableName:Z,relation:J}=Y,W=this.getTable(Z);if(r(J,B1))return this.buildExistsSubquery($,W,J,Z,U,"some");else{let P=[];if(U.some){let M=this.buildExistsSubquery($,W,J,Z,U.some,"some");if(M)P.push(M)}if(U.every){let M=this.buildExistsSubquery($,W,J,Z,U.every,"every");if(M)P.push(M)}if(U.none){let M=this.buildExistsSubquery($,W,J,Z,U.none,"none");if(M)P.push(M)}return P.length?P.length>1?C(...P):P[0]:void 0}}buildExistsSubquery($,_,X,U,Y,Z){let J=this.buildJoinCondition($,_,X);if(!J)return;let W=this.extractAllFilters(_,U,Y),K=W?C(J,W):J;if(!K)return;let P=this.db.select({_:Q`1`}).from(_).where(K);switch(Z){case"some":return K1(P);case"none":return H1(K1(P));case"every":{if(!W)return;let M=H1(W),H=C(J,M);if(!H)return;let B=this.db.select({_:Q`1`}).from(_).where(H);return H1(K1(B))}}}buildJoinCondition($,_,X){try{let{fields:U,references:Y}=M$(this.relationalSchema,this.tableNamesMap,X);if(!U?.length||!Y?.length)return;let Z=P1($),J=[];for(let W=0;W<U.length;W++){let K=U[W],P=Y[W];if(!K||!P)continue;if(P1(K.table)===Z)J.push(Q`${Q.identifier(Z)}.${Q.identifier(K.name)} = ${P}`);else if(P1(P.table)===Z)J.push(Q`${K} = ${Q.identifier(Z)}.${Q.identifier(P.name)}`);else J.push(A1(K,P))}return J.length>1?C(...J):J[0]}catch{return}}extractOrderBy($,_){let X=[];for(let[U,Y]of Object.entries(_).sort((Z,J)=>{let W=Z[1]?.priority??0;return(J[1]?.priority??0)-W})){if(!Y)continue;let{direction:Z}=Y,J=G($)[U];if(!J)continue;X.push(Z==="asc"?a1(J):Y$(J))}return X}extractColumns($,_){let X=G(_),U=[];for(let[Y,Z]of Object.entries($)){if(!X[Z.name])continue;U.push([Z.name,!0])}if(!U.length){let Y=Object.entries(X),Z=Y.find((J)=>!q1.includes(J[1].columnType))?.[0]??Y[0]?.[0]??"";U.push([Z,!0])}return Object.fromEntries(U)}extractColumnsSQLFormat($,_){let X=G(_),U=[];for(let[Y,Z]of Object.entries($)){let J=X[Z.name];if(!J)continue;U.push([Z.name,J])}if(!U.length){let Y=Object.entries(X),Z=Y.find((W)=>!q1.includes(W[1].columnType))?.[0]??Y[0]?.[0]??"",J=X[Z];if(J)U.push([Z,J])}return Object.fromEntries(U)}getFieldsByTypeName($,_){return $.fieldsByTypeName[_]??{}}extractRelationsParams($,_,X){return this.extractRelationsParamsInner($,X,_,!0)}extractRelationsParamsInner($,_,X,U=!1){let Y=this.relationMap[$];if(!Y)return;let Z=Object.entries(X.fieldsByTypeName).find(([W])=>W===_)?.[1];if(!Z)return;let J={};for(let[W,{targetTableName:K}]of Object.entries(Y)){let P=`${U?R($):_}${R(W)}Relation`,M=Object.values(Z).find((E)=>E.name===W)?.fieldsByTypeName[P];if(!M)continue;let H=this.getTable(K),D={columns:this.extractColumns(M,H)},j=Object.values(Z).find((E)=>E.name===W),V=j?.args;D.orderBy=V?.orderBy?this.extractOrderBy(H,V.orderBy):void 0,D.where=V?.where?this.extractAllFilters(H,W,V.where):void 0,D.offset=V?.offset??void 0,D.limit=V?.limit??void 0,D.with=j?this.extractRelationsParamsInner(K,P,j):void 0,J[W]=D}return J}async executeWithHooks($,_,X,U,Y,Z){let J=this.hooks[$];if(!J)return Z(X);let W=J[_];if(!W)return Z(X);if("resolve"in W&&W.resolve){let H=W.resolve;return H({args:X,context:U,info:Y,defaultResolve:(D)=>Z(D??X)})}let K=X,P;if("before"in W&&W.before){let H=await W.before({args:X,context:U,info:Y});if(H){if(H.args)K=H.args;if(H.data)P=H.data}}let M=await Z(K);if("after"in W&&W.after)return W.after({result:M,beforeData:P,context:U,info:Y});return M}async executeCountQuery($,_){let X=this.db;if(X.$count&&typeof X.$count==="function")try{let J=_?await X.$count($,_):await X.$count($);return Number(J)||0}catch(J){}let U=this.db.select({count:e1()}).from($),Z=(await(_?U.where(_):U))[0]?.count||0;return Number(Z)}}var q1=["SQLiteBigInt","SQLiteBlobJson","SQLiteBlobBuffer"];import{isEnumType as S$,isInputObjectType as Z1,isListType as V1,isNonNullType as u,isObjectType as D1,isScalarType as x$,printSchema as F$}from"graphql";function G1($){let _=$.getQueryType(),X=$.getMutationType(),U=new Map;if(!_)return U;let Y=_.getFields(),Z=X?.getFields()??{};for(let[W]of Object.entries(Y)){if(!W.endsWith("Count"))continue;let K=W.slice(0,-5),P=Y[`${K}s`]?`${K}s`:Y[K]?K:void 0,M=Y[K]?K:void 0;if(!P)continue;let H=Y[P];if(!H)continue;let B=j1(H.type);if(!B)continue;let D=B.getFields(),j=[],V={};for(let[m,X1]of Object.entries(D)){let q=O1(X1.type);if(D1(q.type)){let l=q.isList;V[m]={entity:m,type:l?"many":"one",_graphqlType:q.type}}else j.push(m)}let E=R(K),k=Z[`insertInto${E}`]?`insertInto${E}`:void 0,w=Z[`insertInto${E}Single`]?`insertInto${E}Single`:void 0,x=Z[`update${E}`]?`update${E}`:void 0,S=Z[`deleteFrom${E}`]?`deleteFrom${E}`:void 0,z=$.getTypeMap(),L=`${E}Filters`in z,A=`${E}InsertInput`in z,E1=`${E}UpdateInput`in z,U1=`${E}OrderBy`in z;U.set(K,{tableName:K,typeName:E,fields:j,relations:V,queryName:M,queryListName:P,countName:W,insertName:k,insertSingleName:w,updateName:x,deleteName:S,hasFilters:L,hasInsertInput:A,hasUpdateInput:E1,hasOrderBy:U1})}let J=new Map;for(let[W,K]of U)J.set(W,new Set(K.fields));for(let W of U.values())for(let[,K]of Object.entries(W.relations)){let P=K._graphqlType;if(delete K._graphqlType,!P)continue;let M=P.getFields(),H=new Set;for(let[j,V]of Object.entries(M)){let E=O1(V.type);if(!D1(E.type))H.add(j)}let B,D=0;for(let[j,V]of J){let E=0;for(let k of H)if(V.has(k))E++;if(E===H.size&&E>D)D=E,B=j}if(B)K.entity=B}return U}function j1($){if(u($))return j1($.ofType);if(V1($))return j1($.ofType);if(D1($))return $;return null}function O1($){let _=!1,X=!1,U=$;if(u(U))X=!0,U=U.ofType;if(V1(U)){if(_=!0,U=U.ofType,u(U))U=U.ofType}return{type:U,isList:_,isNonNull:X}}function z$($,_,X){let Y=$.getTypeMap()[`${_}Filters`];if(!Y||!Z1(Y))return"";let Z=[],J=Y.getFields();Z.push(`export type ${_}Filters = {`);for(let[W,K]of Object.entries(J)){if(W==="OR"){Z.push(` OR?: ${_}Filters[]`);continue}let M=X.get(y(_))?.relations[W];if(M){let D=R(M.entity);if(M.type==="one")Z.push(` ${W}?: ${D}Filters`);else Z.push(` ${W}?: { some?: ${D}Filters; every?: ${D}Filters; none?: ${D}Filters }`);continue}let H=K.type,B=u(H)?H.ofType:H;if(Z1(B)){let D=B.getFields(),j=A$(D);Z.push(` ${W}?: {`),Z.push(` eq?: ${j} | null`),Z.push(` ne?: ${j} | null`),Z.push(` lt?: ${j} | null`),Z.push(` lte?: ${j} | null`),Z.push(` gt?: ${j} | null`),Z.push(` gte?: ${j} | null`),Z.push(" like?: string | null"),Z.push(" notLike?: string | null"),Z.push(" ilike?: string | null"),Z.push(" notIlike?: string | null"),Z.push(` inArray?: ${j}[] | null`),Z.push(` notInArray?: ${j}[] | null`),Z.push(" isNull?: boolean | null"),Z.push(" isNotNull?: boolean | null"),Z.push(` OR?: Array<Omit<${_}Filters['${W}'], 'OR'>> | null`),Z.push(" }")}}return Z.push("}"),Z.join(`
3
+ `)}function A$($){let _=$.eq;if(!_)return"unknown";return _1(_.type)}function _1($){if(u($))return _1($.ofType);if(V1($))return`${_1($.ofType)}[]`;if(x$($))switch($.name){case"String":return"string";case"Int":case"Float":return"number";case"Boolean":return"boolean";case"JSON":return"unknown";default:return"unknown"}if(S$($))return $.getValues().map((_)=>`'${_.value}'`).join(" | ");return"unknown"}function C1($,_,X){let Y=$.getTypeMap()[_];if(!Y||!Z1(Y))return"";let Z=[],J=Y.getFields();Z.push(`export type ${X} = {`);for(let[W,K]of Object.entries(J)){let P=u(K.type),M=_1(K.type),H=P?"":"?",B=P?"":" | null";Z.push(` ${W}${H}: ${M}${B}`)}return Z.push("}"),Z.join(`
4
+ `)}function I$($,_){let U=$.getTypeMap()[`${_}OrderBy`];if(!U||!Z1(U))return"";let Y=U.getFields(),Z=Object.keys(Y),J=[];J.push(`export type ${_}OrderBy = {`);for(let W of Z)J.push(` ${W}?: { direction: 'asc' | 'desc'; priority: number }`);return J.push("}"),J.join(`
5
+ `)}function q$($,_,X){let U=X[$.tableName]??R($.tableName);if(_)return`export type ${$.typeName}Wire = Omit<Drizzle${U}, DateKeys<Drizzle${U}>>
6
+ & { [K in DateKeys<Drizzle${U}>]: string }`;return`// Wire format for ${$.tableName} (no Drizzle import configured)
7
+ export type ${$.typeName}Wire = Record<string, unknown>`}function O$($){return F$($)}function C$($,_){let X=G1($),U=_?.drizzle?.importPath,Y=_?.drizzle?.typeNames??{},Z=[];if(Z.push("// ─── Auto-generated by drizzle-graphql-pg codegen ────────"),Z.push("// Do not edit manually. Re-run the codegen script to update."),Z.push(""),Z.push("// biome-ignore lint/suspicious/noExplicitAny: utility type for date key extraction"),Z.push("type DateKeys<T> = { [K in keyof T]: T[K] extends Date | null ? K : T[K] extends Date ? K : never }[keyof T]"),Z.push(""),U){let J=[];for(let W of X.values()){let K=Y[W.tableName]??R(W.tableName);J.push(` type ${K} as Drizzle${K}`)}Z.push("import {"),Z.push(J.join(`,
8
+ `)),Z.push(`} from '${U}'`),Z.push("")}Z.push("// ─── Wire Format Types ──────────────────────────────────"),Z.push("// Drizzle types with Date fields converted to string (GraphQL serialization)"),Z.push("");for(let J of X.values())Z.push(q$(J,U,Y)),Z.push("");Z.push("// ─── Filter Types ──────────────────────────────────────"),Z.push("");for(let J of X.values()){if(!J.hasFilters)continue;let W=z$($,J.typeName,X);if(W)Z.push(W),Z.push("")}Z.push("// ─── Input Types ──────────────────────────────────────"),Z.push("");for(let J of X.values()){if(J.hasInsertInput){let W=C1($,`${J.typeName}InsertInput`,`${J.typeName}InsertInput`);if(W)Z.push(W),Z.push("")}if(J.hasUpdateInput){let W=C1($,`${J.typeName}UpdateInput`,`${J.typeName}UpdateInput`);if(W)Z.push(W),Z.push("")}}Z.push("// ─── OrderBy Types ──────────────────────────────────────"),Z.push("");for(let J of X.values()){if(!J.hasOrderBy)continue;let W=I$($,J.typeName);if(W)Z.push(W),Z.push("")}return Z.join(`
9
+ `)}function G$($,_){let X=G1($),U=_?.drizzle?.typeNames??{},Y=[];Y.push("// ─── Auto-generated by drizzle-graphql-pg codegen ────────"),Y.push("// Do not edit manually. Re-run the codegen script to update."),Y.push(""),Y.push("import type {");let Z=[];for(let J of X.values()){if(Z.push(` ${J.typeName}Wire`),J.hasFilters)Z.push(` ${J.typeName}Filters`);if(J.hasInsertInput)Z.push(` ${J.typeName}InsertInput`);if(J.hasUpdateInput)Z.push(` ${J.typeName}UpdateInput`);if(J.hasOrderBy)Z.push(` ${J.typeName}OrderBy`)}Y.push(Z.join(`,
10
+ `)),Y.push("} from './types'"),Y.push(""),Y.push("export const schema = {");for(let J of X.values()){if(Y.push(` ${J.tableName}: {`),J.queryName)Y.push(` queryName: '${J.queryName}',`);if(J.queryListName)Y.push(` queryListName: '${J.queryListName}',`);if(J.countName)Y.push(` countName: '${J.countName}',`);if(J.insertName)Y.push(` insertName: '${J.insertName}',`);if(J.insertSingleName)Y.push(` insertSingleName: '${J.insertSingleName}',`);if(J.updateName)Y.push(` updateName: '${J.updateName}',`);if(J.deleteName)Y.push(` deleteName: '${J.deleteName}',`);Y.push(` fields: [${J.fields.map((W)=>`'${W}'`).join(", ")}],`),Y.push(" relations: {");for(let[W,K]of Object.entries(J.relations))Y.push(` ${W}: { entity: '${K.entity}', type: '${K.type}' },`);Y.push(" },"),Y.push(" },")}Y.push("} as const"),Y.push(""),Y.push("export type EntityDefs = {");for(let J of X.values()){Y.push(` ${J.tableName}: {`),Y.push(` fields: ${J.typeName}Wire`),Y.push(" relations: {");for(let[W,K]of Object.entries(J.relations))Y.push(` ${W}: { entity: '${K.entity}'; type: '${K.type}' }`);if(Y.push(" }"),J.hasFilters)Y.push(` filters: ${J.typeName}Filters`);if(J.hasInsertInput)Y.push(` insertInput: ${J.typeName}InsertInput`);if(J.hasUpdateInput)Y.push(` updateInput: ${J.typeName}UpdateInput`);if(J.hasOrderBy)Y.push(` orderBy: ${J.typeName}OrderBy`);Y.push(" }")}Y.push("}"),Y.push(""),Y.push("export type TableNameMap = {");for(let J of X.values()){let W=U[J.tableName]??R(J.tableName);Y.push(` ${W}: '${J.tableName}'`)}return Y.push("}"),Y.push(""),Y.join(`
11
+ `)}function Q$($){let _={};for(let[X,U]of Object.entries($)){let Y=(W)=>{let K=U(W.context),P=W.args?.where,M=P?{...P,...K}:K;return{args:{...W.args,where:M}}},Z=["query","querySingle","count","update","delete"],J={};for(let W of Z)J[W]={before:Y};_[X]=J}return _}function v$(...$){let _={};for(let X of $){if(!X)continue;for(let[U,Y]of Object.entries(X)){if(!_[U])_[U]={};let Z=_[U];for(let[J,W]of Object.entries(Y)){if(!Z[J]){Z[J]=W;continue}let K=Z[J];if("resolve"in W){Z[J]=W;continue}if("resolve"in K){Z[J]=W;continue}let P={};if(W.before&&K.before){let M=K.before,H=W.before;P.before=async(B)=>{let D=await M(B),j=D?.args?{...B,args:D.args}:B,V=await H(j);return{args:V?.args??D?.args??void 0,data:V?.data??D?.data??void 0}}}else P.before=W.before??K.before;if(W.after&&K.after){let M=K.after,H=W.after;P.after=async(B)=>{let D=await M(B);return H({...B,result:D})}}else P.after=W.after??K.after;Z[J]=P}}}return _}var D0=($,_)=>{return new d($,_).build()},j0=($,_)=>{return new d($,_).buildEntities()},V0=($,_)=>{let{tables:X,tableNamesMap:U}=T$($,L$),Y=Object.keys(X),Z={findMany:()=>Promise.resolve([]),findFirst:()=>Promise.resolve(null)},J=Object.fromEntries(Y.map((P)=>[P,Z]));return new d({_:{fullSchema:$,schema:X,tableNamesMap:U},query:J,select:()=>({from:()=>({where:()=>({})})})},_).build()};export{Q$ as withRowSecurity,l1 as restricted,i1 as readOnly,r1 as permissive,v$ as mergeHooks,C$ as generateTypes,O$ as generateSDL,G$ as generateEntityDefs,V0 as buildSchemaFromDrizzle,D0 as buildSchema,j0 as buildEntities,d as SchemaBuilder,s as GraphQLJSON};
package/package.json CHANGED
@@ -1,14 +1,24 @@
1
1
  {
2
2
  "name": "@graphql-suite/schema",
3
- "version": "0.8.3",
3
+ "version": "0.9.0",
4
4
  "description": "GraphQL schema builder with CRUD operations, relation filtering, and hooks from Drizzle ORM",
5
5
  "license": "MIT",
6
6
  "author": "https://github.com/dmythro",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/annexare/drizzle-graphql-suite.git",
9
+ "url": "git+https://github.com/annexare/graphql-suite.git",
10
10
  "directory": "packages/schema"
11
11
  },
12
+ "homepage": "https://graphql-suite.annexare.com/schema/overview/",
13
+ "keywords": [
14
+ "drizzle",
15
+ "graphql",
16
+ "schema",
17
+ "orm",
18
+ "postgresql",
19
+ "crud",
20
+ "typescript"
21
+ ],
12
22
  "type": "module",
13
23
  "publishConfig": {
14
24
  "access": "public"
@@ -22,7 +32,7 @@
22
32
  }
23
33
  },
24
34
  "dependencies": {
25
- "@drizzle-graphql-suite/schema": "0.8.3"
35
+ "graphql-parse-resolve-info": "4.14.1"
26
36
  },
27
37
  "peerDependencies": {
28
38
  "drizzle-orm": ">=0.44.0",
@@ -0,0 +1,25 @@
1
+ import type { GraphQLFieldConfig } from 'graphql';
2
+ import type { BuildSchemaConfig, PermissionConfig, TableAccess } from './types';
3
+ export declare const readOnly: () => TableAccess;
4
+ export declare const permissive: (id: string, tables?: Record<string, boolean | TableAccess>) => PermissionConfig;
5
+ export declare const restricted: (id: string, tables?: Record<string, boolean | TableAccess>) => PermissionConfig;
6
+ type MergeResult = {
7
+ config: BuildSchemaConfig;
8
+ /** Tables that need individual mutation entry points filtered after build */
9
+ mutationFilter: Record<string, {
10
+ insert: boolean;
11
+ update: boolean;
12
+ delete: boolean;
13
+ }>;
14
+ };
15
+ /**
16
+ * Converts a PermissionConfig into BuildSchemaConfig overrides.
17
+ * Returns the merged config + a map of tables needing post-build mutation filtering.
18
+ */
19
+ export declare function mergePermissionsIntoConfig(baseConfig: BuildSchemaConfig, permissions: PermissionConfig, allTableNames: string[]): MergeResult;
20
+ /**
21
+ * Removes disallowed mutation entry points from the mutations record.
22
+ * Mutates and returns the same record.
23
+ */
24
+ export declare function postFilterMutations(mutations: Record<string, GraphQLFieldConfig<any, any>>, mutationFilter: MergeResult['mutationFilter']): void;
25
+ export {};
@@ -0,0 +1,21 @@
1
+ import type { HooksConfig } from './types';
2
+ type RowSecurityRule = (context: any) => Record<string, unknown>;
3
+ /**
4
+ * Generates a HooksConfig that injects WHERE clauses from row-level security rules.
5
+ * Rules are applied as `before` hooks on query, querySingle, count, update, and delete operations.
6
+ *
7
+ * ```ts
8
+ * const hooks = withRowSecurity({
9
+ * posts: (context) => ({ authorId: { eq: context.user.id } }),
10
+ * })
11
+ * ```
12
+ */
13
+ export declare function withRowSecurity(rules: Record<string, RowSecurityRule>): HooksConfig;
14
+ /**
15
+ * Deep-merges multiple HooksConfig objects.
16
+ * - `before` hooks are chained: each runs sequentially, passing the modified args forward.
17
+ * - `after` hooks are chained: each runs sequentially, passing the modified result forward.
18
+ * - `resolve` hooks: last one wins (cannot be composed).
19
+ */
20
+ export declare function mergeHooks(...configs: (HooksConfig | undefined)[]): HooksConfig;
21
+ export {};
@@ -0,0 +1,72 @@
1
+ import type { Column, Relation, Table, TablesRelationalConfig } from 'drizzle-orm';
2
+ import { type SQL } from 'drizzle-orm';
3
+ import { type PgDatabase, PgTable } from 'drizzle-orm/pg-core';
4
+ import { GraphQLSchema } from 'graphql';
5
+ import type { ResolveTree } from 'graphql-parse-resolve-info';
6
+ import { type TableNamedRelations } from './data-mappers';
7
+ import type { BuildSchemaConfig, GeneratedEntities, PermissionConfig } from './types';
8
+ export declare class SchemaBuilder {
9
+ protected db: PgDatabase<any, any, any>;
10
+ protected tables: Record<string, PgTable>;
11
+ protected relationMap: Record<string, Record<string, TableNamedRelations>>;
12
+ protected relationalSchema: TablesRelationalConfig;
13
+ protected tableNamesMap: Record<string, string>;
14
+ private config;
15
+ private hooks;
16
+ private adapter;
17
+ private suffixes;
18
+ private limitRelationDepth;
19
+ private limitSelfRelationDepth;
20
+ private allTableNames;
21
+ private excludedTables;
22
+ private tableOperations;
23
+ private pruneRelations;
24
+ private filterTypes;
25
+ private orderByTypes;
26
+ private filterValueTypes;
27
+ private selectFieldTypes;
28
+ private innerOrder;
29
+ constructor(db: PgDatabase<any, any, any>, config?: BuildSchemaConfig);
30
+ private toFieldMap;
31
+ buildEntities(): GeneratedEntities;
32
+ build(): {
33
+ schema: GraphQLSchema;
34
+ entities: GeneratedEntities;
35
+ withPermissions: (permissions: PermissionConfig) => GraphQLSchema;
36
+ clearPermissionCache: (id?: string) => void;
37
+ };
38
+ private logDebugInfo;
39
+ private extractTables;
40
+ private getTable;
41
+ private buildRelationMap;
42
+ private getFilterType;
43
+ private getOrderByType;
44
+ private getFilterValues;
45
+ private getSelectFields;
46
+ private generateColumnFilterValues;
47
+ private generateSelectFields;
48
+ private generateTableTypes;
49
+ private buildRelationFilterFields;
50
+ private createQueryResolver;
51
+ private createSingleQueryResolver;
52
+ private createCountResolver;
53
+ private createInsertResolver;
54
+ private createInsertSingleResolver;
55
+ private createUpdateResolver;
56
+ private createDeleteResolver;
57
+ protected extractColumnFilters(column: Column, columnName: string, operators: any): SQL | undefined;
58
+ protected extractTableColumnFilters(table: Table, tableName: string, filters: any): SQL | undefined;
59
+ /** Combined filter extraction: column filters + relation filters */
60
+ private extractAllFilters;
61
+ protected extractRelationFilters(table: Table, tableName: string, relationName: string, filterValue: any): SQL | undefined;
62
+ protected buildExistsSubquery(parentTable: Table, targetTable: Table, relation: Relation, targetTableName: string, filterValue: any, quantifier: 'some' | 'every' | 'none'): SQL | undefined;
63
+ protected buildJoinCondition(parentTable: Table, _targetTable: Table, relation: Relation): SQL | undefined;
64
+ protected extractOrderBy(table: Table, orderArgs: any): SQL[];
65
+ protected extractColumns(tree: Record<string, ResolveTree>, table: Table): Record<string, true>;
66
+ private extractColumnsSQLFormat;
67
+ private getFieldsByTypeName;
68
+ private extractRelationsParams;
69
+ private extractRelationsParamsInner;
70
+ private executeWithHooks;
71
+ private executeCountQuery;
72
+ }
package/types.d.ts ADDED
@@ -0,0 +1,256 @@
1
+ import type { GraphQLFieldConfig, GraphQLInputObjectType, GraphQLObjectType, GraphQLResolveInfo } from 'graphql';
2
+ export type GeneratedEntities = {
3
+ queries: Record<string, GraphQLFieldConfig<any, any>>;
4
+ mutations: Record<string, GraphQLFieldConfig<any, any>>;
5
+ inputs: Record<string, GraphQLInputObjectType>;
6
+ types: Record<string, GraphQLObjectType>;
7
+ };
8
+ export type OperationType = 'query' | 'querySingle' | 'count' | 'insert' | 'insertSingle' | 'update' | 'delete';
9
+ export type HookContext = {
10
+ args: any;
11
+ context: any;
12
+ info: GraphQLResolveInfo;
13
+ };
14
+ export type BeforeHookResult = {
15
+ args?: any;
16
+ data?: any;
17
+ };
18
+ export type BeforeHookFn = (ctx: HookContext) => Promise<BeforeHookResult | undefined> | BeforeHookResult | undefined;
19
+ export type AfterHookContext = {
20
+ result: any;
21
+ beforeData: any;
22
+ context: any;
23
+ info: GraphQLResolveInfo;
24
+ };
25
+ export type AfterHookFn = (ctx: AfterHookContext) => Promise<any> | any;
26
+ export type ResolveHookContext = HookContext & {
27
+ defaultResolve: (overrideArgs?: any) => Promise<any>;
28
+ };
29
+ export type ResolveHookFn = (ctx: ResolveHookContext) => Promise<any> | any;
30
+ export type OperationHooks = {
31
+ before?: BeforeHookFn;
32
+ after?: AfterHookFn;
33
+ } | {
34
+ resolve: ResolveHookFn;
35
+ };
36
+ export type TableHookConfig = {
37
+ [K in OperationType]?: OperationHooks;
38
+ };
39
+ export type HooksConfig = {
40
+ [tableName: string]: TableHookConfig;
41
+ };
42
+ export type TableAccess = {
43
+ query?: boolean;
44
+ insert?: boolean;
45
+ update?: boolean;
46
+ delete?: boolean;
47
+ };
48
+ export type PermissionConfig = {
49
+ id: string;
50
+ mode: 'permissive' | 'restricted';
51
+ tables?: Record<string, boolean | TableAccess>;
52
+ };
53
+ export type TableOperations = {
54
+ /** Generate query operations (list, single, count). @default true */
55
+ queries?: boolean;
56
+ /** Generate mutation operations (insert, insertSingle, update, delete). @default follows global `mutations` */
57
+ mutations?: boolean;
58
+ };
59
+ /**
60
+ * Controls how a specific relation expands in the schema.
61
+ * - `false`: relation field omitted entirely from parent type
62
+ * - `'leaf'`: relation expands with scalar columns only (no child relations)
63
+ * - `{ only: string[] }`: relation expands with only the listed child relation fields
64
+ */
65
+ export type RelationPruneRule = false | 'leaf' | {
66
+ only: string[];
67
+ };
68
+ /**
69
+ * Configuration for both the GraphQL schema builder (server) and the
70
+ * type-safe client. Shared across `graphql-suite/schema` and
71
+ * `graphql-suite/client` so a single config object drives both
72
+ * the runtime schema and the inferred TypeScript types.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * const config = {
77
+ * mutations: true,
78
+ * limitRelationDepth: 5,
79
+ * limitSelfRelationDepth: 2,
80
+ * suffixes: { list: 's', single: '' },
81
+ * tables: { exclude: ['session', 'account'] },
82
+ * pruneRelations: {
83
+ * 'asset.childAssets': false,
84
+ * 'override.asset': 'leaf',
85
+ * 'attribute.asset': { only: ['selectedVariant'] },
86
+ * },
87
+ * } as const satisfies BuildSchemaConfig
88
+ * ```
89
+ */
90
+ export type BuildSchemaConfig = {
91
+ /**
92
+ * Whether to generate GraphQL mutation operations (insert, update, delete).
93
+ * Set to `false` for a read-only schema.
94
+ * On the client side, controls whether mutation helpers are generated.
95
+ * @default true
96
+ */
97
+ mutations?: boolean;
98
+ /**
99
+ * Maximum depth for expanding relation fields in the generated schema
100
+ * and in client-side filter types (`InferEntityFilters`).
101
+ *
102
+ * - **Server**: limits how many levels of nested relations appear in
103
+ * GraphQL object types. `undefined` means no limit.
104
+ * - **Client**: limits recursive relation filter type expansion to
105
+ * prevent TS7056 on circular schemas. Capped at 5.
106
+ *
107
+ * Set to `0` to omit relations altogether.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * // Allow up to 5 levels of nesting
112
+ * { limitRelationDepth: 5 }
113
+ * ```
114
+ * @default 3 (server) / 1 (client filter types)
115
+ */
116
+ limitRelationDepth?: number;
117
+ /**
118
+ * Max occurrences of the same table via direct self-relations in a
119
+ * single type path. Only applies to relations where source and target
120
+ * table are identical (e.g., `asset.parent → asset`).
121
+ *
122
+ * - `1` = self-relation fields are omitted entirely
123
+ * - `2` = one level of self-relation expansion (the nested type has
124
+ * no further self-relation fields)
125
+ *
126
+ * Cross-table cycles that revisit a table are governed by
127
+ * `limitRelationDepth` instead.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * // category.parent → category (expanded), but nested category
132
+ * // won't have parent/children fields
133
+ * { limitSelfRelationDepth: 2 }
134
+ * ```
135
+ * @default 1
136
+ */
137
+ limitSelfRelationDepth?: number;
138
+ /**
139
+ * Customizes the suffixes appended to auto-generated query names.
140
+ *
141
+ * Given a table named `asset`:
142
+ * - List query: `asset` + `list` suffix → e.g. `"assets"` or `"assetList"`
143
+ * - Single query: `asset` + `single` suffix → e.g. `"asset"` or `"assetSingle"`
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * // "assets" / "asset" (pluralize list, no suffix for single)
148
+ * { suffixes: { list: 's', single: '' } }
149
+ *
150
+ * // "assetList" / "assetSingle" (explicit suffixes)
151
+ * { suffixes: { list: 'List', single: 'Single' } }
152
+ * ```
153
+ * @default { list: '', single: 'Single' }
154
+ *
155
+ * TODO: Consider adding Intl.PluralRules-based pluralization as an alternative
156
+ * to simple suffix appending (e.g., `category` → `categories` instead of `categorys`).
157
+ */
158
+ suffixes?: {
159
+ list?: string;
160
+ single?: string;
161
+ };
162
+ /**
163
+ * Per-table lifecycle hooks for queries and mutations.
164
+ * Keys are table names as they appear in the Drizzle schema object.
165
+ *
166
+ * **Server-only** — hooks are executed during GraphQL resolution and have
167
+ * no effect on the client package. The client imports `BuildSchemaConfig`
168
+ * for type-level inference only (`limitRelationDepth`, `tables`, etc.)
169
+ * and ignores `hooks` entirely.
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * {
174
+ * hooks: {
175
+ * asset: {
176
+ * query: { before: (ctx) => { ... } },
177
+ * insert: { after: (ctx, result) => { ... } },
178
+ * },
179
+ * },
180
+ * }
181
+ * ```
182
+ */
183
+ hooks?: HooksConfig;
184
+ /**
185
+ * Per-table configuration: exclude tables entirely or limit which
186
+ * operations are generated per table.
187
+ *
188
+ * Table names must match the keys in the Drizzle schema object.
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * {
193
+ * tables: {
194
+ * // Remove auth tables from the GraphQL schema
195
+ * exclude: ['session', 'account', 'verification'],
196
+ * // Make 'auditLog' read-only
197
+ * config: { auditLog: { queries: true, mutations: false } },
198
+ * },
199
+ * }
200
+ * ```
201
+ */
202
+ tables?: {
203
+ /** Tables to completely exclude (no types, no operations, relations to them skipped). */
204
+ exclude?: readonly string[];
205
+ /** Per-table operation overrides. Tables not listed get default behavior. */
206
+ config?: Record<string, TableOperations>;
207
+ };
208
+ /**
209
+ * Fine-grained per-relation pruning rules that control how each
210
+ * relation expands in the generated schema.
211
+ *
212
+ * **Server-only** — pruning shapes the generated GraphQL schema.
213
+ * The client builds queries from the schema descriptor, which already
214
+ * reflects pruning (pruned relations are absent), so the client
215
+ * cannot generate queries for pruned fields.
216
+ *
217
+ * Keys use `tableName.relationName` format. Values:
218
+ * - `false` — relation field is omitted entirely from the parent type
219
+ * - `'leaf'` — relation expands with scalar columns only (no nested relations)
220
+ * - `{ only: string[] }` — relation expands with only the listed child relations
221
+ *
222
+ * @example
223
+ * ```ts
224
+ * {
225
+ * pruneRelations: {
226
+ * // Remove back-reference completely
227
+ * 'assetType.assets': false,
228
+ * // Show override.asset but don't expand its relations
229
+ * 'override.asset': 'leaf',
230
+ * // Only keep selectedVariant on attribute.asset
231
+ * 'attribute.asset': { only: ['selectedVariant'] },
232
+ * },
233
+ * }
234
+ * ```
235
+ */
236
+ pruneRelations?: Record<string, RelationPruneRule>;
237
+ /**
238
+ * Enable debug logging for schema diagnostics (server-side only).
239
+ *
240
+ * - `true` — logs SDL byte size and type count
241
+ * - `{ schemaSize?: boolean; relationTree?: boolean }` — selective logging
242
+ *
243
+ * @example
244
+ * ```ts
245
+ * // Log everything
246
+ * { debug: true }
247
+ *
248
+ * // Only log the relation expansion tree
249
+ * { debug: { relationTree: true } }
250
+ * ```
251
+ */
252
+ debug?: boolean | {
253
+ schemaSize?: boolean;
254
+ relationTree?: boolean;
255
+ };
256
+ };