@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 +425 -121
- package/adapters/pg.d.ts +10 -0
- package/adapters/types.d.ts +11 -0
- package/case-ops.d.ts +2 -0
- package/codegen.d.ts +12 -0
- package/data-mappers.d.ts +12 -0
- package/graphql/scalars.d.ts +2 -0
- package/graphql/type-builder.d.ts +7 -0
- package/index.d.ts +32 -1
- package/index.js +11 -1
- package/package.json +13 -3
- package/permissions.d.ts +25 -0
- package/row-security.d.ts +21 -0
- package/schema-builder.d.ts +72 -0
- package/types.d.ts +256 -0
package/README.md
CHANGED
|
@@ -1,191 +1,495 @@
|
|
|
1
|
-
|
|
2
|
-
[](https://www.npmjs.com/package/drizzle-graphql-suite)
|
|
3
|
-
[](https://github.com/annexare/drizzle-graphql-suite/actions/workflows/ci.yml)
|
|
1
|
+
# @graphql-suite/schema
|
|
4
2
|
|
|
5
|
-
|
|
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-
|
|
6
|
+
Auto-generates a complete GraphQL schema with CRUD operations, relation-level filtering, and hooks from Drizzle PostgreSQL schemas.
|
|
8
7
|
|
|
9
|
-
##
|
|
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
|
-
|
|
10
|
+
```bash
|
|
11
|
+
bun add @graphql-suite/schema
|
|
12
|
+
```
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
18
|
+
Or install the full suite:
|
|
28
19
|
|
|
29
20
|
```bash
|
|
30
|
-
bun add
|
|
21
|
+
bun add graphql-suite
|
|
31
22
|
```
|
|
32
23
|
|
|
33
24
|
```bash
|
|
34
|
-
npm install
|
|
25
|
+
npm install graphql-suite
|
|
35
26
|
```
|
|
36
27
|
|
|
37
|
-
##
|
|
28
|
+
## Motivation
|
|
38
29
|
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
##
|
|
48
|
+
## API Reference
|
|
48
49
|
|
|
49
|
-
###
|
|
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 '
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
+
// app/api/graphql/route.ts
|
|
75
|
+
import { createYoga } from 'graphql-yoga'
|
|
76
|
+
import { schema } from '@/lib/schema' // from buildSchema() above
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
###
|
|
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 {
|
|
116
|
+
import { buildSchemaFromDrizzle } from '@graphql-suite/schema'
|
|
92
117
|
import * as schema from './db/schema'
|
|
93
118
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
limit: 10,
|
|
108
|
-
})
|
|
169
|
+
}
|
|
109
170
|
```
|
|
110
171
|
|
|
111
|
-
|
|
172
|
+
- `exclude` — `string[]` — tables removed from the schema entirely
|
|
173
|
+
- `config` — `Record<string, TableOperations>` — per-table `queries` and `mutations` booleans
|
|
112
174
|
|
|
113
|
-
|
|
114
|
-
import { GraphQLProvider, useEntity, useEntityList } from 'drizzle-graphql-suite/query'
|
|
115
|
-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
175
|
+
### `pruneRelations`
|
|
116
176
|
|
|
117
|
-
|
|
177
|
+
Fine-grained per-relation pruning. Keys are `tableName.relationName`:
|
|
118
178
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
264
|
+
## Row-Level Security
|
|
144
265
|
|
|
145
|
-
|
|
266
|
+
Generate hooks that inject WHERE clauses for row-level filtering. Compose with other hooks using `mergeHooks`.
|
|
146
267
|
|
|
147
268
|
```ts
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
281
|
+
### `withRowSecurity(rules)`
|
|
154
282
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
import
|
|
169
|
-
import {
|
|
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 } =
|
|
427
|
+
const { schema } = buildSchemaFromDrizzle(drizzleSchema)
|
|
428
|
+
writeFileSync('schema.graphql', generateSDL(schema))
|
|
429
|
+
```
|
|
174
430
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
447
|
+
### `generateEntityDefs(schema, options?)`
|
|
181
448
|
|
|
182
|
-
|
|
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
|
-
```
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
package/adapters/pg.d.ts
ADDED
|
@@ -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
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,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
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
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
|
-
"
|
|
35
|
+
"graphql-parse-resolve-info": "4.14.1"
|
|
26
36
|
},
|
|
27
37
|
"peerDependencies": {
|
|
28
38
|
"drizzle-orm": ">=0.44.0",
|
package/permissions.d.ts
ADDED
|
@@ -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
|
+
};
|