@drizzle-graphql-suite/schema 0.5.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Annexare Studio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,348 @@
1
+ # @drizzle-graphql-suite/schema
2
+
3
+ Auto-generates a complete GraphQL schema with CRUD operations, relation-level filtering, and hooks from Drizzle PostgreSQL schemas.
4
+
5
+ ## Motivation
6
+
7
+ Inspired by [`drizzle-graphql`](https://github.com/drizzle-team/drizzle-graphql), this package is a purpose-built replacement focused on PostgreSQL. Key improvements:
8
+
9
+ - **Relation-level filtering** with EXISTS subqueries (`some`/`every`/`none` quantifiers)
10
+ - **Per-operation hooks system** (before/after/resolve) for auth, audit, and custom logic
11
+ - **Count queries** with full filter support
12
+ - **`buildEntities()`** for composable schema building (avoids redundant schema validation)
13
+ - **Configurable query/mutation suffixes** for naming customization
14
+ - **Per-table schema control** — exclude tables, disable queries/mutations per table (up to 90% schema size reduction)
15
+ - **Self-relation depth limiting** — separate from general depth, prevents exponential type growth
16
+ - **Relation pruning** — `false`, `'leaf'`, or `{ only: [...] }` per relation
17
+ - **`buildSchemaFromDrizzle()`** — no database connection needed (for codegen/introspection)
18
+ - **Code generation** — `generateSDL`, `generateTypes`, `generateEntityDefs`
19
+ - **Architecture** — TypeScript source, PostgreSQL-only, `SchemaBuilder` class, type caching, lazy thunks for circular relations
20
+ - **Bug fixes** — relation filter join conditions (Drizzle v0.44+), operator map replacements, `catch (e: unknown)` narrowing
21
+
22
+ ## API Reference
23
+
24
+ ### `buildSchema(db, config?)`
25
+
26
+ Builds a complete `GraphQLSchema` with all CRUD operations from a Drizzle database instance. Returns `{ schema, entities }`.
27
+
28
+ ```ts
29
+ import { buildSchema } from 'drizzle-graphql-suite/schema'
30
+ import { createYoga } from 'graphql-yoga'
31
+ import { db } from './db'
32
+
33
+ const { schema, entities } = buildSchema(db, {
34
+ limitRelationDepth: 3,
35
+ tables: { exclude: ['session'] },
36
+ })
37
+
38
+ const yoga = createYoga({ schema })
39
+ ```
40
+
41
+ ### `buildEntities(db, config?)`
42
+
43
+ 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.
44
+
45
+ ```ts
46
+ import { buildEntities } from 'drizzle-graphql-suite/schema'
47
+
48
+ const entities = buildEntities(db, { mutations: false })
49
+ // entities.queries, entities.mutations, entities.inputs, entities.types
50
+ ```
51
+
52
+ ### `buildSchemaFromDrizzle(drizzleSchema, config?)`
53
+
54
+ Builds a schema directly from Drizzle schema exports — no database connection or `.env` required. Resolvers are stubs. Intended for schema introspection and code generation.
55
+
56
+ ```ts
57
+ import { buildSchemaFromDrizzle } from 'drizzle-graphql-suite/schema'
58
+ import * as schema from './db/schema'
59
+
60
+ const { schema: graphqlSchema } = buildSchemaFromDrizzle(schema)
61
+ ```
62
+
63
+ ## Configuration
64
+
65
+ `BuildSchemaConfig` controls all aspects of schema generation:
66
+
67
+ ### `mutations`
68
+
69
+ Enable or disable mutation generation globally.
70
+
71
+ - **Type**: `boolean`
72
+ - **Default**: `true`
73
+
74
+ ### `limitRelationDepth`
75
+
76
+ Maximum depth of nested relation fields on queries. Set to `0` to omit relations, `undefined` for no limit.
77
+
78
+ - **Type**: `number | undefined`
79
+ - **Default**: `3`
80
+
81
+ ### `limitSelfRelationDepth`
82
+
83
+ 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.
84
+
85
+ - **Type**: `number`
86
+ - **Default**: `1`
87
+
88
+ ### `suffixes`
89
+
90
+ Customize query field name suffixes for list and single queries.
91
+
92
+ - **Type**: `{ list?: string; single?: string }`
93
+ - **Default**: `{ list: '', single: 'Single' }`
94
+
95
+ ### `tables`
96
+
97
+ Per-table schema control:
98
+
99
+ ```ts
100
+ {
101
+ tables: {
102
+ // Remove tables entirely (relations to them are silently skipped)
103
+ exclude: ['session', 'verification'],
104
+ // Per-table operation overrides
105
+ config: {
106
+ auditLog: { queries: true, mutations: false },
107
+ user: { mutations: false },
108
+ },
109
+ },
110
+ }
111
+ ```
112
+
113
+ - `exclude` — `string[]` — tables removed from the schema entirely
114
+ - `config` — `Record<string, TableOperations>` — per-table `queries` and `mutations` booleans
115
+
116
+ ### `pruneRelations`
117
+
118
+ Fine-grained per-relation pruning. Keys are `tableName.relationName`:
119
+
120
+ ```ts
121
+ {
122
+ pruneRelations: {
123
+ // Omit this relation field entirely
124
+ 'asset.childAssets': false,
125
+ // Expand with scalar columns only (no nested relations)
126
+ 'user.posts': 'leaf',
127
+ // Expand with only the listed child relations
128
+ 'post.comments': { only: ['author'] },
129
+ },
130
+ }
131
+ ```
132
+
133
+ - `false` — relation field omitted entirely from parent type
134
+ - `'leaf'` — relation expands with scalar columns only
135
+ - `{ only: string[] }` — relation expands with only the listed child relation fields
136
+
137
+ ### `hooks`
138
+
139
+ Per-table, per-operation hooks. See [Hooks System](#hooks-system).
140
+
141
+ ### `debug`
142
+
143
+ Enable diagnostic logging for schema size and relation tree.
144
+
145
+ - **Type**: `boolean | { schemaSize?: boolean; relationTree?: boolean }`
146
+ - **Default**: `undefined`
147
+
148
+ ## Hooks System
149
+
150
+ Hooks intercept operations for auth, validation, audit logging, or custom resolution.
151
+
152
+ ### Hook Types
153
+
154
+ Each operation supports either **before/after** hooks or a **resolve** hook (not both):
155
+
156
+ | Hook | Timing | Use Case |
157
+ |------|--------|----------|
158
+ | `before` | Before default resolver | Auth checks, argument transformation, pass data to `after` |
159
+ | `after` | After default resolver | Audit logging, result transformation |
160
+ | `resolve` | Replaces default resolver | Full control, custom data sources |
161
+
162
+ ### Operations
163
+
164
+ | Operation | Type | Description |
165
+ |-----------|------|-------------|
166
+ | `query` | Read | List query |
167
+ | `querySingle` | Read | Single record query |
168
+ | `count` | Read | Count query |
169
+ | `insert` | Write | Batch insert |
170
+ | `insertSingle` | Write | Single record insert |
171
+ | `update` | Write | Update mutation |
172
+ | `delete` | Write | Delete mutation |
173
+
174
+ ### Example
175
+
176
+ ```ts
177
+ buildSchema(db, {
178
+ hooks: {
179
+ user: {
180
+ query: {
181
+ before: async ({ args, context }) => {
182
+ if (!context.user) throw new Error('Unauthorized')
183
+ // Optionally modify args or pass data to after hook
184
+ return { args, data: { startTime: Date.now() } }
185
+ },
186
+ after: async ({ result, beforeData }) => {
187
+ console.log(`Query took ${Date.now() - beforeData.startTime}ms`)
188
+ return result
189
+ },
190
+ },
191
+ delete: {
192
+ resolve: async ({ args, context, defaultResolve }) => {
193
+ // Soft delete instead of hard delete
194
+ return defaultResolve({ ...args, set: { deletedAt: new Date() } })
195
+ },
196
+ },
197
+ },
198
+ },
199
+ })
200
+ ```
201
+
202
+ ## Relation Filtering
203
+
204
+ Filter by related rows using EXISTS subqueries with `some`, `every`, and `none` quantifiers.
205
+
206
+ ```graphql
207
+ query {
208
+ users(where: {
209
+ posts: {
210
+ some: { published: { eq: true } }
211
+ none: { flagged: { eq: true } }
212
+ }
213
+ }) {
214
+ id
215
+ name
216
+ posts {
217
+ title
218
+ }
219
+ }
220
+ }
221
+ ```
222
+
223
+ - **`some`** — at least one related row matches
224
+ - **`every`** — all related rows match
225
+ - **`none`** — no related rows match
226
+
227
+ For one-to-one relations, filters apply directly (no quantifiers needed):
228
+
229
+ ```graphql
230
+ query {
231
+ posts(where: {
232
+ author: { role: { eq: "admin" } }
233
+ }) {
234
+ title
235
+ }
236
+ }
237
+ ```
238
+
239
+ ## Generated Operations
240
+
241
+ | Pattern | Example (`user` table) | Type |
242
+ |---------|----------------------|------|
243
+ | `{table}` | `user` | Single query |
244
+ | `{table}{listSuffix}` | `users` | List query |
245
+ | `{table}Count` | `userCount` | Count query |
246
+ | `insertInto{Table}` | `insertIntoUser` | Batch insert |
247
+ | `insertInto{Table}Single` | `insertIntoUserSingle` | Single insert |
248
+ | `update{Table}` | `updateUser` | Update |
249
+ | `deleteFrom{Table}` | `deleteFromUser` | Delete |
250
+
251
+ ## Column Type Mapping
252
+
253
+ | Drizzle Type | GraphQL Type |
254
+ |-------------|--------------|
255
+ | `boolean` | `Boolean` |
256
+ | `text`, `varchar`, `char` | `String` |
257
+ | `integer`, `smallint`, `serial`, `smallserial`, `bigserial` | `Int` |
258
+ | `real`, `doublePrecision`, `numeric` | `Float` |
259
+ | `bigint` | `String` |
260
+ | `date`, `timestamp`, `time` | `String` |
261
+ | `json`, `jsonb` | `JSON` (custom scalar) |
262
+ | `bytea` | `[Int!]` |
263
+ | `vector` | `[Float!]` |
264
+ | `geometry` | `PgGeometryObject { x, y }` |
265
+ | `enum` | Generated `GraphQLEnumType` |
266
+
267
+ ## Code Generation
268
+
269
+ Three code generation functions for producing static artifacts from a GraphQL schema:
270
+
271
+ ### `generateSDL(schema)`
272
+
273
+ Generates the GraphQL Schema Definition Language string.
274
+
275
+ ```ts
276
+ import { buildSchemaFromDrizzle, generateSDL } from 'drizzle-graphql-suite/schema'
277
+ import * as drizzleSchema from './db/schema'
278
+ import { writeFileSync } from 'node:fs'
279
+
280
+ const { schema } = buildSchemaFromDrizzle(drizzleSchema)
281
+ writeFileSync('schema.graphql', generateSDL(schema))
282
+ ```
283
+
284
+ ### `generateTypes(schema, options?)`
285
+
286
+ 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.
287
+
288
+ ```ts
289
+ import { generateTypes } from 'drizzle-graphql-suite/schema'
290
+
291
+ const types = generateTypes(schema, {
292
+ drizzle: {
293
+ importPath: '@myapp/db/schema',
294
+ typeNames: { userProfile: 'UserProfile' },
295
+ },
296
+ })
297
+ writeFileSync('generated/types.ts', types)
298
+ ```
299
+
300
+ ### `generateEntityDefs(schema, options?)`
301
+
302
+ Generates a runtime schema descriptor object and `EntityDefs` type for the client package. This is an alternative to `createDrizzleClient` — useful when you want to ship pre-built schema metadata.
303
+
304
+ ```ts
305
+ import { generateEntityDefs } from 'drizzle-graphql-suite/schema'
306
+
307
+ const entityDefs = generateEntityDefs(schema, {
308
+ drizzle: { importPath: '@myapp/db/schema' },
309
+ })
310
+ writeFileSync('generated/entity-defs.ts', entityDefs)
311
+ ```
312
+
313
+ ### Full Codegen Script
314
+
315
+ ```ts
316
+ import { buildSchemaFromDrizzle, generateSDL, generateTypes, generateEntityDefs } from 'drizzle-graphql-suite/schema'
317
+ import * as drizzleSchema from './db/schema'
318
+ import { writeFileSync, mkdirSync } from 'node:fs'
319
+
320
+ const { schema } = buildSchemaFromDrizzle(drizzleSchema)
321
+
322
+ mkdirSync('generated', { recursive: true })
323
+ writeFileSync('generated/schema.graphql', generateSDL(schema))
324
+ writeFileSync('generated/types.ts', generateTypes(schema, {
325
+ drizzle: { importPath: '@myapp/db/schema' },
326
+ }))
327
+ writeFileSync('generated/entity-defs.ts', generateEntityDefs(schema, {
328
+ drizzle: { importPath: '@myapp/db/schema' },
329
+ }))
330
+ ```
331
+
332
+ ## `GeneratedEntities` Type
333
+
334
+ The return type from `buildEntities()` and `buildSchema()`:
335
+
336
+ ```ts
337
+ type GeneratedEntities = {
338
+ queries: Record<string, GraphQLFieldConfig<any, any>>
339
+ mutations: Record<string, GraphQLFieldConfig<any, any>>
340
+ inputs: Record<string, GraphQLInputObjectType>
341
+ types: Record<string, GraphQLObjectType>
342
+ }
343
+ ```
344
+
345
+ - **`queries`** — all generated query field configs, spreadable into a parent schema
346
+ - **`mutations`** — all generated mutation field configs
347
+ - **`inputs`** — filter, insert, update, and orderBy input types by name
348
+ - **`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 ADDED
@@ -0,0 +1,26 @@
1
+ import type { PgDatabase } from 'drizzle-orm/pg-core';
2
+ import type { GraphQLSchema } from 'graphql';
3
+ import type { BuildSchemaConfig, GeneratedEntities } 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
+ };
9
+ export declare const buildEntities: (db: PgDatabase<any, any, any>, config?: BuildSchemaConfig) => GeneratedEntities;
10
+ /**
11
+ * Build a GraphQL schema directly from Drizzle schema exports — no database
12
+ * connection or `.env` required. Creates a lightweight mock db instance that
13
+ * satisfies `SchemaBuilder`'s metadata needs (table definitions, relations,
14
+ * table name mapping) without an actual connection.
15
+ *
16
+ * Resolver functions on the returned entities are stubs — this is intended
17
+ * for schema introspection and code generation, not query execution.
18
+ */
19
+ export declare const buildSchemaFromDrizzle: (drizzleSchema: Record<string, unknown>, config?: BuildSchemaConfig) => {
20
+ schema: GraphQLSchema;
21
+ entities: GeneratedEntities;
22
+ };
23
+ export type { CodegenOptions } from './codegen';
24
+ export { generateEntityDefs, generateSDL, generateTypes } from './codegen';
25
+ export { SchemaBuilder } from './schema-builder';
26
+ export * from './types';