@drizzle-graphql-suite/schema 0.6.0 → 0.8.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
@@ -32,6 +32,8 @@ Inspired by [`drizzle-graphql`](https://github.com/drizzle-team/drizzle-graphql)
32
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
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
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
35
37
  - **Per-operation hooks system** (before/after/resolve) for auth, audit, and custom logic
36
38
  - **Count queries** with full filter support
37
39
  - **`buildEntities()`** for composable schema building (avoids redundant schema validation)
@@ -47,14 +49,14 @@ Inspired by [`drizzle-graphql`](https://github.com/drizzle-team/drizzle-graphql)
47
49
 
48
50
  ### `buildSchema(db, config?)`
49
51
 
50
- 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 }`.
51
53
 
52
54
  ```ts
53
55
  import { buildSchema } from 'drizzle-graphql-suite/schema'
54
56
  import { createYoga } from 'graphql-yoga'
55
57
  import { db } from './db'
56
58
 
57
- const { schema, entities } = buildSchema(db, {
59
+ const { schema, entities, withPermissions } = buildSchema(db, {
58
60
  limitRelationDepth: 3,
59
61
  tables: { exclude: ['session'] },
60
62
  })
@@ -62,6 +64,8 @@ const { schema, entities } = buildSchema(db, {
62
64
  const yoga = createYoga({ schema })
63
65
  ```
64
66
 
67
+ See [Runtime Permissions](#runtime-permissions) for `withPermissions` usage.
68
+
65
69
  #### Framework Integration
66
70
 
67
71
  **Next.js App Router**
@@ -200,6 +204,92 @@ Enable diagnostic logging for schema size and relation tree.
200
204
  - **Type**: `boolean | { schemaSize?: boolean; relationTree?: boolean }`
201
205
  - **Default**: `undefined`
202
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
+
203
293
  ## Hooks System
204
294
 
205
295
  Hooks intercept operations for auth, validation, audit logging, or custom resolution.
@@ -321,6 +411,8 @@ query {
321
411
 
322
412
  ## Code Generation
323
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
+
324
416
  Three code generation functions for producing static artifacts from a GraphQL schema:
325
417
 
326
418
  ### `generateSDL(schema)`
@@ -354,7 +446,7 @@ writeFileSync('generated/types.ts', types)
354
446
 
355
447
  ### `generateEntityDefs(schema, options?)`
356
448
 
357
- 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.
358
450
 
359
451
  ```ts
360
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;
@@ -1041,16 +1191,22 @@ class SchemaBuilder {
1041
1191
  delete operators.OR;
1042
1192
  const entries = Object.entries(operators);
1043
1193
  if (operators.OR) {
1044
- if (entries.length > 1) {
1045
- throw new GraphQLError2(`WHERE ${columnName}: Cannot specify both fields and 'OR' in column operators!`);
1046
- }
1047
- const variants2 = [];
1194
+ const orVariants = [];
1048
1195
  for (const variant of operators.OR) {
1049
1196
  const extracted = this.extractColumnFilters(column, columnName, variant);
1050
1197
  if (extracted)
1051
- variants2.push(extracted);
1198
+ orVariants.push(extracted);
1052
1199
  }
1053
- return variants2.length ? variants2.length > 1 ? or(...variants2) : variants2[0] : undefined;
1200
+ const orClause = orVariants.length > 1 ? or(...orVariants) : orVariants.length === 1 ? orVariants[0] : undefined;
1201
+ if (entries.length <= 1)
1202
+ return orClause;
1203
+ const { OR: _, ...rest } = operators;
1204
+ const fieldClause = this.extractColumnFilters(column, columnName, rest);
1205
+ if (!fieldClause)
1206
+ return orClause;
1207
+ if (!orClause)
1208
+ return fieldClause;
1209
+ return and(fieldClause, orClause);
1054
1210
  }
1055
1211
  const comparisonOps = { eq, ne, gt, gte, lt, lte };
1056
1212
  const stringOps = { like, notLike, ilike, notIlike };
@@ -1108,16 +1264,22 @@ class SchemaBuilder {
1108
1264
  }
1109
1265
  }
1110
1266
  if (filters.OR) {
1111
- if (columnEntries.length > 0 || relationEntries.length > 0) {
1112
- throw new GraphQLError2(`WHERE ${tableName}: Cannot specify both fields and 'OR' in table filters!`);
1113
- }
1114
- const variants2 = [];
1267
+ const orVariants = [];
1115
1268
  for (const variant of filters.OR) {
1116
1269
  const extracted = this.extractAllFilters(table, tableName, variant);
1117
1270
  if (extracted)
1118
- variants2.push(extracted);
1271
+ orVariants.push(extracted);
1119
1272
  }
1120
- return variants2.length ? variants2.length > 1 ? or(...variants2) : variants2[0] : undefined;
1273
+ const orClause = orVariants.length > 1 ? or(...orVariants) : orVariants.length === 1 ? orVariants[0] : undefined;
1274
+ if (columnEntries.length === 0 && relationEntries.length === 0)
1275
+ return orClause;
1276
+ const { OR: _, ...rest } = filters;
1277
+ const fieldClause = this.extractAllFilters(table, tableName, rest);
1278
+ if (!fieldClause)
1279
+ return orClause;
1280
+ if (!orClause)
1281
+ return fieldClause;
1282
+ return and(fieldClause, orClause);
1121
1283
  }
1122
1284
  const variants = [];
1123
1285
  for (const [columnName, operators] of columnEntries) {
@@ -1774,6 +1936,81 @@ function generateEntityDefs(schema, options) {
1774
1936
  return lines.join(`
1775
1937
  `);
1776
1938
  }
1939
+ // src/row-security.ts
1940
+ function withRowSecurity(rules) {
1941
+ const hooks = {};
1942
+ for (const [tableName, rule] of Object.entries(rules)) {
1943
+ const before = (ctx) => {
1944
+ const whereClause = rule(ctx.context);
1945
+ const existingWhere = ctx.args?.where;
1946
+ const mergedWhere = existingWhere ? { ...existingWhere, ...whereClause } : whereClause;
1947
+ return { args: { ...ctx.args, where: mergedWhere } };
1948
+ };
1949
+ const ops = ["query", "querySingle", "count", "update", "delete"];
1950
+ const tableHooks = {};
1951
+ for (const op of ops) {
1952
+ tableHooks[op] = { before };
1953
+ }
1954
+ hooks[tableName] = tableHooks;
1955
+ }
1956
+ return hooks;
1957
+ }
1958
+ function mergeHooks(...configs) {
1959
+ const result = {};
1960
+ for (const config of configs) {
1961
+ if (!config)
1962
+ continue;
1963
+ for (const [tableName, tableHooks] of Object.entries(config)) {
1964
+ if (!result[tableName]) {
1965
+ result[tableName] = {};
1966
+ }
1967
+ const existing = result[tableName];
1968
+ for (const [op, hooks] of Object.entries(tableHooks)) {
1969
+ if (!existing[op]) {
1970
+ existing[op] = hooks;
1971
+ continue;
1972
+ }
1973
+ const existingOp = existing[op];
1974
+ if ("resolve" in hooks) {
1975
+ existing[op] = hooks;
1976
+ continue;
1977
+ }
1978
+ if ("resolve" in existingOp) {
1979
+ existing[op] = hooks;
1980
+ continue;
1981
+ }
1982
+ const merged = {};
1983
+ if (hooks.before && existingOp.before) {
1984
+ const first = existingOp.before;
1985
+ const second = hooks.before;
1986
+ merged.before = async (ctx) => {
1987
+ const firstResult = await first(ctx);
1988
+ const nextCtx = firstResult?.args ? { ...ctx, args: firstResult.args } : ctx;
1989
+ const secondResult = await second(nextCtx);
1990
+ return {
1991
+ args: secondResult?.args ?? firstResult?.args ?? undefined,
1992
+ data: secondResult?.data ?? firstResult?.data ?? undefined
1993
+ };
1994
+ };
1995
+ } else {
1996
+ merged.before = hooks.before ?? existingOp.before;
1997
+ }
1998
+ if (hooks.after && existingOp.after) {
1999
+ const first = existingOp.after;
2000
+ const second = hooks.after;
2001
+ merged.after = async (ctx) => {
2002
+ const firstResult = await first(ctx);
2003
+ return second({ ...ctx, result: firstResult });
2004
+ };
2005
+ } else {
2006
+ merged.after = hooks.after ?? existingOp.after;
2007
+ }
2008
+ existing[op] = merged;
2009
+ }
2010
+ }
2011
+ }
2012
+ return result;
2013
+ }
1777
2014
 
1778
2015
  // src/index.ts
1779
2016
  var buildSchema = (db, config) => {
@@ -1801,6 +2038,11 @@ var buildSchemaFromDrizzle = (drizzleSchema, config) => {
1801
2038
  return builder.build();
1802
2039
  };
1803
2040
  export {
2041
+ withRowSecurity,
2042
+ restricted,
2043
+ readOnly,
2044
+ permissive,
2045
+ mergeHooks,
1804
2046
  generateTypes,
1805
2047
  generateSDL,
1806
2048
  generateEntityDefs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drizzle-graphql-suite/schema",
3
- "version": "0.6.0",
3
+ "version": "0.8.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,18 +1,23 @@
1
- import { type PgDatabase } from 'drizzle-orm/pg-core';
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';
2
4
  import { GraphQLSchema } from 'graphql';
3
- import type { BuildSchemaConfig, GeneratedEntities } from './types';
5
+ import type { ResolveTree } from 'graphql-parse-resolve-info';
6
+ import { type TableNamedRelations } from './data-mappers';
7
+ import type { BuildSchemaConfig, GeneratedEntities, PermissionConfig } from './types';
4
8
  export declare class SchemaBuilder {
5
- private db;
6
- private tables;
7
- private relationMap;
8
- private relationalSchema;
9
- private tableNamesMap;
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>;
10
14
  private config;
11
15
  private hooks;
12
16
  private adapter;
13
17
  private suffixes;
14
18
  private limitRelationDepth;
15
19
  private limitSelfRelationDepth;
20
+ private allTableNames;
16
21
  private excludedTables;
17
22
  private tableOperations;
18
23
  private pruneRelations;
@@ -27,6 +32,8 @@ export declare class SchemaBuilder {
27
32
  build(): {
28
33
  schema: GraphQLSchema;
29
34
  entities: GeneratedEntities;
35
+ withPermissions: (permissions: PermissionConfig) => GraphQLSchema;
36
+ clearPermissionCache: (id?: string) => void;
30
37
  };
31
38
  private logDebugInfo;
32
39
  private extractTables;
@@ -47,15 +54,15 @@ export declare class SchemaBuilder {
47
54
  private createInsertSingleResolver;
48
55
  private createUpdateResolver;
49
56
  private createDeleteResolver;
50
- private extractColumnFilters;
51
- private extractTableColumnFilters;
57
+ protected extractColumnFilters(column: Column, columnName: string, operators: any): SQL | undefined;
58
+ protected extractTableColumnFilters(table: Table, tableName: string, filters: any): SQL | undefined;
52
59
  /** Combined filter extraction: column filters + relation filters */
53
60
  private extractAllFilters;
54
- private extractRelationFilters;
55
- private buildExistsSubquery;
56
- private buildJoinCondition;
57
- private extractOrderBy;
58
- private extractColumns;
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>;
59
66
  private extractColumnsSQLFormat;
60
67
  private getFieldsByTypeName;
61
68
  private extractRelationsParams;
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;