@drizzle-graphql-suite/schema 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,17 +1,43 @@
1
1
  # @drizzle-graphql-suite/schema
2
2
 
3
+ > Part of [`drizzle-graphql-suite`](https://github.com/annexare/drizzle-graphql-suite).
4
+ > See also: [`client`](https://github.com/annexare/drizzle-graphql-suite/tree/main/packages/client) | [`query`](https://github.com/annexare/drizzle-graphql-suite/tree/main/packages/query)
5
+
3
6
  Auto-generates a complete GraphQL schema with CRUD operations, relation-level filtering, and hooks from Drizzle PostgreSQL schemas.
4
7
 
8
+ ## Installation
9
+
10
+ ```bash
11
+ bun add @drizzle-graphql-suite/schema
12
+ ```
13
+
14
+ ```bash
15
+ npm install @drizzle-graphql-suite/schema
16
+ ```
17
+
18
+ Or install the full suite:
19
+
20
+ ```bash
21
+ bun add drizzle-graphql-suite
22
+ ```
23
+
24
+ ```bash
25
+ npm install drizzle-graphql-suite
26
+ ```
27
+
5
28
  ## Motivation
6
29
 
7
30
  Inspired by [`drizzle-graphql`](https://github.com/drizzle-team/drizzle-graphql), this package is a purpose-built replacement focused on PostgreSQL. Key improvements:
8
31
 
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
9
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
10
37
  - **Per-operation hooks system** (before/after/resolve) for auth, audit, and custom logic
11
38
  - **Count queries** with full filter support
12
39
  - **`buildEntities()`** for composable schema building (avoids redundant schema validation)
13
40
  - **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
41
  - **Self-relation depth limiting** — separate from general depth, prevents exponential type growth
16
42
  - **Relation pruning** — `false`, `'leaf'`, or `{ only: [...] }` per relation
17
43
  - **`buildSchemaFromDrizzle()`** — no database connection needed (for codegen/introspection)
@@ -23,14 +49,14 @@ Inspired by [`drizzle-graphql`](https://github.com/drizzle-team/drizzle-graphql)
23
49
 
24
50
  ### `buildSchema(db, config?)`
25
51
 
26
- Builds a complete `GraphQLSchema` with all CRUD operations from a Drizzle database instance. Returns `{ schema, entities }`.
52
+ Builds a complete `GraphQLSchema` with all CRUD operations from a Drizzle database instance. Returns `{ schema, entities, withPermissions }`.
27
53
 
28
54
  ```ts
29
55
  import { buildSchema } from 'drizzle-graphql-suite/schema'
30
56
  import { createYoga } from 'graphql-yoga'
31
57
  import { db } from './db'
32
58
 
33
- const { schema, entities } = buildSchema(db, {
59
+ const { schema, entities, withPermissions } = buildSchema(db, {
34
60
  limitRelationDepth: 3,
35
61
  tables: { exclude: ['session'] },
36
62
  })
@@ -38,6 +64,39 @@ const { schema, entities } = buildSchema(db, {
38
64
  const yoga = createYoga({ schema })
39
65
  ```
40
66
 
67
+ See [Runtime Permissions](#runtime-permissions) for `withPermissions` usage.
68
+
69
+ #### Framework Integration
70
+
71
+ **Next.js App Router**
72
+
73
+ ```ts
74
+ // app/api/graphql/route.ts
75
+ import { createYoga } from 'graphql-yoga'
76
+ import { schema } from '@/lib/schema' // from buildSchema() above
77
+
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
+
41
100
  ### `buildEntities(db, config?)`
42
101
 
43
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.
@@ -145,6 +204,92 @@ Enable diagnostic logging for schema size and relation tree.
145
204
  - **Type**: `boolean | { schemaSize?: boolean; relationTree?: boolean }`
146
205
  - **Default**: `undefined`
147
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 'drizzle-graphql-suite/schema'
213
+
214
+ const { schema, withPermissions } = buildSchema(db)
215
+
216
+ // Full schema (admin)
217
+ const adminSchema = schema
218
+
219
+ // Permissive: everything allowed except audit (excluded) and users (read-only)
220
+ const maintainerSchema = withPermissions(
221
+ permissive('maintainer', { audit: false, users: readOnly() }),
222
+ )
223
+
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
253
+ }
254
+ ```
255
+
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
263
+
264
+ ## Row-Level Security
265
+
266
+ Generate hooks that inject WHERE clauses for row-level filtering. Compose with other hooks using `mergeHooks`.
267
+
268
+ ```ts
269
+ import { buildSchema, withRowSecurity, mergeHooks } from 'drizzle-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
+ ```
280
+
281
+ ### `withRowSecurity(rules)`
282
+
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
+
148
293
  ## Hooks System
149
294
 
150
295
  Hooks intercept operations for auth, validation, audit logging, or custom resolution.
@@ -266,6 +411,8 @@ query {
266
411
 
267
412
  ## Code Generation
268
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/drizzle-graphql-suite/tree/main/packages/client) infers all types automatically — no codegen needed.
415
+
269
416
  Three code generation functions for producing static artifacts from a GraphQL schema:
270
417
 
271
418
  ### `generateSDL(schema)`
@@ -299,7 +446,7 @@ writeFileSync('generated/types.ts', types)
299
446
 
300
447
  ### `generateEntityDefs(schema, options?)`
301
448
 
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.
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.
303
450
 
304
451
  ```ts
305
452
  import { generateEntityDefs } from 'drizzle-graphql-suite/schema'
package/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import type { PgDatabase } from 'drizzle-orm/pg-core';
2
2
  import type { GraphQLSchema } from 'graphql';
3
- import type { BuildSchemaConfig, GeneratedEntities } from './types';
3
+ import type { BuildSchemaConfig, GeneratedEntities, PermissionConfig } from './types';
4
4
  export { GraphQLJSON } from './graphql/scalars';
5
5
  export declare const buildSchema: (db: PgDatabase<any, any, any>, config?: BuildSchemaConfig) => {
6
6
  schema: GraphQLSchema;
7
7
  entities: GeneratedEntities;
8
+ withPermissions: (permissions: PermissionConfig) => GraphQLSchema;
9
+ clearPermissionCache: (id?: string) => void;
8
10
  };
9
11
  export declare const buildEntities: (db: PgDatabase<any, any, any>, config?: BuildSchemaConfig) => GeneratedEntities;
10
12
  /**
@@ -19,8 +21,12 @@ export declare const buildEntities: (db: PgDatabase<any, any, any>, config?: Bui
19
21
  export declare const buildSchemaFromDrizzle: (drizzleSchema: Record<string, unknown>, config?: BuildSchemaConfig) => {
20
22
  schema: GraphQLSchema;
21
23
  entities: GeneratedEntities;
24
+ withPermissions: (permissions: PermissionConfig) => GraphQLSchema;
25
+ clearPermissionCache: (id?: string) => void;
22
26
  };
23
27
  export type { CodegenOptions } from './codegen';
24
28
  export { generateEntityDefs, generateSDL, generateTypes } from './codegen';
29
+ export { permissive, readOnly, restricted } from './permissions';
30
+ export { mergeHooks, withRowSecurity } from './row-security';
25
31
  export { SchemaBuilder } from './schema-builder';
26
32
  export * from './types';
package/index.js CHANGED
@@ -363,6 +363,103 @@ var drizzleColumnToGraphQLType = (column, columnName, tableName, forceNullable =
363
363
  return typeDesc;
364
364
  };
365
365
 
366
+ // src/permissions.ts
367
+ var readOnly = () => ({
368
+ query: true,
369
+ insert: false,
370
+ update: false,
371
+ delete: false
372
+ });
373
+ var permissive = (id, tables) => ({
374
+ id,
375
+ mode: "permissive",
376
+ tables
377
+ });
378
+ var restricted = (id, tables) => ({
379
+ id,
380
+ mode: "restricted",
381
+ tables
382
+ });
383
+ function mergePermissionsIntoConfig(baseConfig, permissions, allTableNames) {
384
+ const excluded = [...baseConfig.tables?.exclude ?? []];
385
+ const tableConfig = {
386
+ ...baseConfig.tables?.config ?? {}
387
+ };
388
+ const mutationFilter = {};
389
+ for (const tableName of allTableNames) {
390
+ if (excluded.includes(tableName))
391
+ continue;
392
+ const access = resolveTableAccess(permissions, tableName);
393
+ if (access === false) {
394
+ excluded.push(tableName);
395
+ continue;
396
+ }
397
+ if (access === true) {
398
+ continue;
399
+ }
400
+ const defaultAllow = permissions.mode === "permissive";
401
+ const queryAllowed = access.query ?? defaultAllow;
402
+ const insertAllowed = access.insert ?? defaultAllow;
403
+ const updateAllowed = access.update ?? defaultAllow;
404
+ const deleteAllowed = access.delete ?? defaultAllow;
405
+ if (!queryAllowed && !insertAllowed && !updateAllowed && !deleteAllowed) {
406
+ excluded.push(tableName);
407
+ continue;
408
+ }
409
+ tableConfig[tableName] = {
410
+ ...tableConfig[tableName],
411
+ queries: queryAllowed
412
+ };
413
+ const anyMutation = insertAllowed || updateAllowed || deleteAllowed;
414
+ if (!anyMutation) {
415
+ tableConfig[tableName] = { ...tableConfig[tableName], mutations: false };
416
+ } else {
417
+ tableConfig[tableName] = { ...tableConfig[tableName], mutations: true };
418
+ if (!insertAllowed || !updateAllowed || !deleteAllowed) {
419
+ mutationFilter[tableName] = {
420
+ insert: insertAllowed,
421
+ update: updateAllowed,
422
+ delete: deleteAllowed
423
+ };
424
+ }
425
+ }
426
+ }
427
+ const config = {
428
+ ...baseConfig,
429
+ tables: {
430
+ exclude: excluded.length ? excluded : undefined,
431
+ config: Object.keys(tableConfig).length ? tableConfig : undefined
432
+ }
433
+ };
434
+ return { config, mutationFilter };
435
+ }
436
+ function resolveTableAccess(permissions, tableName) {
437
+ const override = permissions.tables?.[tableName];
438
+ if (permissions.mode === "permissive") {
439
+ if (override === undefined)
440
+ return true;
441
+ return override;
442
+ }
443
+ if (override === undefined)
444
+ return false;
445
+ return override;
446
+ }
447
+ function postFilterMutations(mutations, mutationFilter) {
448
+ for (const [tableName, flags] of Object.entries(mutationFilter)) {
449
+ const cap = capitalize(tableName);
450
+ if (!flags.insert) {
451
+ delete mutations[`insertInto${cap}`];
452
+ delete mutations[`insertInto${cap}Single`];
453
+ }
454
+ if (!flags.update) {
455
+ delete mutations[`update${cap}`];
456
+ }
457
+ if (!flags.delete) {
458
+ delete mutations[`deleteFrom${cap}`];
459
+ }
460
+ }
461
+ }
462
+
366
463
  // src/schema-builder.ts
367
464
  class SchemaBuilder {
368
465
  db;
@@ -376,6 +473,7 @@ class SchemaBuilder {
376
473
  suffixes;
377
474
  limitRelationDepth;
378
475
  limitSelfRelationDepth;
476
+ allTableNames;
379
477
  excludedTables;
380
478
  tableOperations;
381
479
  pruneRelations;
@@ -434,6 +532,7 @@ class SchemaBuilder {
434
532
  }
435
533
  });
436
534
  this.tables = this.extractTables(schema);
535
+ this.allTableNames = Object.keys(this.tables);
437
536
  this.excludedTables = new Set(config?.tables?.exclude ?? []);
438
537
  this.tableOperations = config?.tables?.config ?? {};
439
538
  for (const tableName of this.excludedTables) {
@@ -533,7 +632,58 @@ class SchemaBuilder {
533
632
  }
534
633
  const schema = new GraphQLSchema(graphQLSchemaConfig);
535
634
  this.logDebugInfo(schema);
536
- return { schema, entities };
635
+ const cache = new Map;
636
+ const db = this.db;
637
+ const baseConfig = this.config;
638
+ const allTableNames = this.allTableNames;
639
+ const withPermissions = (permissions) => {
640
+ const cached = cache.get(permissions.id);
641
+ if (cached)
642
+ return cached;
643
+ const { config: mergedConfig, mutationFilter } = mergePermissionsIntoConfig(baseConfig, permissions, allTableNames);
644
+ const excludedSet = new Set(mergedConfig.tables?.exclude ?? []);
645
+ const hasAnyTable = allTableNames.some((t) => !excludedSet.has(t));
646
+ if (!hasAnyTable) {
647
+ const emptySchema = new GraphQLSchema({
648
+ query: new GraphQLObjectType2({
649
+ name: "Query",
650
+ fields: {
651
+ _empty: { type: GraphQLBoolean2 }
652
+ }
653
+ })
654
+ });
655
+ cache.set(permissions.id, emptySchema);
656
+ return emptySchema;
657
+ }
658
+ const permBuilder = new SchemaBuilder(db, mergedConfig);
659
+ const permEntities = permBuilder.buildEntities();
660
+ if (Object.keys(mutationFilter).length) {
661
+ postFilterMutations(permEntities.mutations, mutationFilter);
662
+ }
663
+ const permSchemaConfig = {
664
+ types: [...Object.values(permEntities.inputs), ...Object.values(permEntities.types)],
665
+ query: new GraphQLObjectType2({
666
+ name: "Query",
667
+ fields: Object.keys(permEntities.queries).length ? permEntities.queries : { _empty: { type: GraphQLBoolean2 } }
668
+ })
669
+ };
670
+ if (mergedConfig.mutations !== false && Object.keys(permEntities.mutations).length) {
671
+ permSchemaConfig.mutation = new GraphQLObjectType2({
672
+ name: "Mutation",
673
+ fields: permEntities.mutations
674
+ });
675
+ }
676
+ const permSchema = new GraphQLSchema(permSchemaConfig);
677
+ cache.set(permissions.id, permSchema);
678
+ return permSchema;
679
+ };
680
+ const clearPermissionCache = (id) => {
681
+ if (id)
682
+ cache.delete(id);
683
+ else
684
+ cache.clear();
685
+ };
686
+ return { schema, entities, withPermissions, clearPermissionCache };
537
687
  }
538
688
  logDebugInfo(schema) {
539
689
  const debug = this.config.debug;
@@ -1774,6 +1924,81 @@ function generateEntityDefs(schema, options) {
1774
1924
  return lines.join(`
1775
1925
  `);
1776
1926
  }
1927
+ // src/row-security.ts
1928
+ function withRowSecurity(rules) {
1929
+ const hooks = {};
1930
+ for (const [tableName, rule] of Object.entries(rules)) {
1931
+ const before = (ctx) => {
1932
+ const whereClause = rule(ctx.context);
1933
+ const existingWhere = ctx.args?.where;
1934
+ const mergedWhere = existingWhere ? { ...existingWhere, ...whereClause } : whereClause;
1935
+ return { args: { ...ctx.args, where: mergedWhere } };
1936
+ };
1937
+ const ops = ["query", "querySingle", "count", "update", "delete"];
1938
+ const tableHooks = {};
1939
+ for (const op of ops) {
1940
+ tableHooks[op] = { before };
1941
+ }
1942
+ hooks[tableName] = tableHooks;
1943
+ }
1944
+ return hooks;
1945
+ }
1946
+ function mergeHooks(...configs) {
1947
+ const result = {};
1948
+ for (const config of configs) {
1949
+ if (!config)
1950
+ continue;
1951
+ for (const [tableName, tableHooks] of Object.entries(config)) {
1952
+ if (!result[tableName]) {
1953
+ result[tableName] = {};
1954
+ }
1955
+ const existing = result[tableName];
1956
+ for (const [op, hooks] of Object.entries(tableHooks)) {
1957
+ if (!existing[op]) {
1958
+ existing[op] = hooks;
1959
+ continue;
1960
+ }
1961
+ const existingOp = existing[op];
1962
+ if ("resolve" in hooks) {
1963
+ existing[op] = hooks;
1964
+ continue;
1965
+ }
1966
+ if ("resolve" in existingOp) {
1967
+ existing[op] = hooks;
1968
+ continue;
1969
+ }
1970
+ const merged = {};
1971
+ if (hooks.before && existingOp.before) {
1972
+ const first = existingOp.before;
1973
+ const second = hooks.before;
1974
+ merged.before = async (ctx) => {
1975
+ const firstResult = await first(ctx);
1976
+ const nextCtx = firstResult?.args ? { ...ctx, args: firstResult.args } : ctx;
1977
+ const secondResult = await second(nextCtx);
1978
+ return {
1979
+ args: secondResult?.args ?? firstResult?.args ?? undefined,
1980
+ data: secondResult?.data ?? firstResult?.data ?? undefined
1981
+ };
1982
+ };
1983
+ } else {
1984
+ merged.before = hooks.before ?? existingOp.before;
1985
+ }
1986
+ if (hooks.after && existingOp.after) {
1987
+ const first = existingOp.after;
1988
+ const second = hooks.after;
1989
+ merged.after = async (ctx) => {
1990
+ const firstResult = await first(ctx);
1991
+ return second({ ...ctx, result: firstResult });
1992
+ };
1993
+ } else {
1994
+ merged.after = hooks.after ?? existingOp.after;
1995
+ }
1996
+ existing[op] = merged;
1997
+ }
1998
+ }
1999
+ }
2000
+ return result;
2001
+ }
1777
2002
 
1778
2003
  // src/index.ts
1779
2004
  var buildSchema = (db, config) => {
@@ -1801,6 +2026,11 @@ var buildSchemaFromDrizzle = (drizzleSchema, config) => {
1801
2026
  return builder.build();
1802
2027
  };
1803
2028
  export {
2029
+ withRowSecurity,
2030
+ restricted,
2031
+ readOnly,
2032
+ permissive,
2033
+ mergeHooks,
1804
2034
  generateTypes,
1805
2035
  generateSDL,
1806
2036
  generateEntityDefs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drizzle-graphql-suite/schema",
3
- "version": "0.5.0",
3
+ "version": "0.7.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",
@@ -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 {};
@@ -1,6 +1,6 @@
1
1
  import { type PgDatabase } from 'drizzle-orm/pg-core';
2
2
  import { GraphQLSchema } from 'graphql';
3
- import type { BuildSchemaConfig, GeneratedEntities } from './types';
3
+ import type { BuildSchemaConfig, GeneratedEntities, PermissionConfig } from './types';
4
4
  export declare class SchemaBuilder {
5
5
  private db;
6
6
  private tables;
@@ -13,6 +13,7 @@ export declare class SchemaBuilder {
13
13
  private suffixes;
14
14
  private limitRelationDepth;
15
15
  private limitSelfRelationDepth;
16
+ private allTableNames;
16
17
  private excludedTables;
17
18
  private tableOperations;
18
19
  private pruneRelations;
@@ -27,6 +28,8 @@ export declare class SchemaBuilder {
27
28
  build(): {
28
29
  schema: GraphQLSchema;
29
30
  entities: GeneratedEntities;
31
+ withPermissions: (permissions: PermissionConfig) => GraphQLSchema;
32
+ clearPermissionCache: (id?: string) => void;
30
33
  };
31
34
  private logDebugInfo;
32
35
  private extractTables;
package/types.d.ts CHANGED
@@ -39,6 +39,17 @@ export type TableHookConfig = {
39
39
  export type HooksConfig = {
40
40
  [tableName: string]: TableHookConfig;
41
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
+ };
42
53
  export type TableOperations = {
43
54
  /** Generate query operations (list, single, count). @default true */
44
55
  queries?: boolean;