@sequenceholdings/orm 0.1.3 → 2.1.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.
Files changed (59) hide show
  1. package/README.md +108 -38
  2. package/dist/authoring.cjs +128 -16
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +21 -4
  5. package/dist/authoring.d.ts +21 -4
  6. package/dist/authoring.js +128 -16
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +83 -9
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +47 -1
  11. package/dist/canonical.d.ts +47 -1
  12. package/dist/canonical.js +83 -10
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +816 -134
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +818 -136
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs.map +1 -1
  19. package/dist/client.js.map +1 -1
  20. package/dist/compiler.cjs +488 -96
  21. package/dist/compiler.cjs.map +1 -1
  22. package/dist/compiler.d.cts +34 -2
  23. package/dist/compiler.d.ts +34 -2
  24. package/dist/compiler.js +489 -97
  25. package/dist/compiler.js.map +1 -1
  26. package/dist/index.cjs +420 -71
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +266 -9
  29. package/dist/index.d.ts +266 -9
  30. package/dist/index.js +410 -73
  31. package/dist/index.js.map +1 -1
  32. package/dist/ir.cjs +146 -12
  33. package/dist/ir.cjs.map +1 -1
  34. package/dist/ir.d.cts +161 -5
  35. package/dist/ir.d.ts +161 -5
  36. package/dist/ir.js +144 -13
  37. package/dist/ir.js.map +1 -1
  38. package/dist/{naming-CTvWSRTf.d.ts → naming-DCn94LJ4.d.ts} +15 -1
  39. package/dist/{naming-DRyinU4u.d.cts → naming-DPglwzkB.d.cts} +15 -1
  40. package/dist/predicate-v2.cjs +136 -11
  41. package/dist/predicate-v2.cjs.map +1 -1
  42. package/dist/predicate-v2.d.cts +3 -0
  43. package/dist/predicate-v2.d.ts +3 -0
  44. package/dist/predicate-v2.js +136 -11
  45. package/dist/predicate-v2.js.map +1 -1
  46. package/dist/sdl.cjs +460 -13
  47. package/dist/sdl.cjs.map +1 -1
  48. package/dist/sdl.d.cts +1 -1
  49. package/dist/sdl.d.ts +1 -1
  50. package/dist/sdl.js +459 -14
  51. package/dist/sdl.js.map +1 -1
  52. package/package.json +6 -4
  53. package/src/codegen-gql/preset.cjs +64 -0
  54. package/src/codegen-gql/server-fns.cjs +11 -6
  55. package/templates/orm-namespace/codegen.preset.cjs +17 -0
  56. package/templates/orm-namespace/codegen.ts +1 -1
  57. package/templates/orm-namespace/package.json +3 -0
  58. package/templates/orm-namespace/schema/tables.ts +1 -1
  59. package/templates/orm-namespace/sequence.config.ts +1 -1
package/dist/sdl.cjs CHANGED
@@ -3,10 +3,16 @@
3
3
  var zod = require('zod');
4
4
  var crypto = require('crypto');
5
5
 
6
+ // src/canonical/v2.ts
7
+
8
+ // src/constants.ts
9
+ var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
10
+
6
11
  // src/canonical/v2.ts
7
12
  var CANONICAL_FORMAT = 2;
8
13
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
9
14
  var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
15
+ var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
10
16
  var COLUMN_TYPES = [
11
17
  "uuid",
12
18
  "text",
@@ -27,18 +33,33 @@ var canonicalEnumSchema = zod.z.object({
27
33
  var canonicalColumnSchema = zod.z.object({
28
34
  /** Property name in TS (camelCase); columnName is the Postgres name. */
29
35
  name: zod.z.string().min(1).max(63),
30
- columnName: identifier,
36
+ columnName: columnIdentifier,
31
37
  type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
32
38
  nullable: zod.z.boolean().default(true),
33
39
  unique: zod.z.boolean().default(false),
34
40
  /** Literal default, or one of the allowlisted SQL functions. */
35
41
  default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
36
42
  defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
43
+ /**
44
+ * Stamp this column from the database clock on UPDATE (`now()`). The
45
+ * column stays insertable (defaults fill gaps) but is omitted from
46
+ * SetInput / settable so callers cannot backdate it.
47
+ */
48
+ onUpdateFn: zod.z.enum(["now()"]).optional(),
37
49
  description: zod.z.string().max(500).optional(),
38
50
  /** Migration hints — same semantics as v1. */
39
- renamedFrom: identifier.optional(),
51
+ renamedFrom: columnIdentifier.optional(),
40
52
  cast: zod.z.string().max(200).optional(),
41
53
  deprecated: zod.z.boolean().default(false)
54
+ }).superRefine((column, ctx) => {
55
+ if (column.onUpdateFn === void 0) return;
56
+ if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
57
+ ctx.addIssue({
58
+ code: "custom",
59
+ path: ["onUpdateFn"],
60
+ message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
61
+ });
62
+ }
42
63
  });
43
64
  var canonicalLinkSchema = zod.z.object({
44
65
  name: zod.z.string().min(1).max(63),
@@ -46,15 +67,15 @@ var canonicalLinkSchema = zod.z.object({
46
67
  /** Target table name (namespace-local). Absent for toPlatformUser. */
47
68
  target: identifier.optional(),
48
69
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
49
- on: identifier,
70
+ on: columnIdentifier,
50
71
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
51
- references: identifier.optional(),
72
+ references: columnIdentifier.optional(),
52
73
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
53
74
  enforce: zod.z.boolean().default(false),
54
75
  description: zod.z.string().max(500).optional()
55
76
  });
56
77
  var canonicalIndexSchema = zod.z.object({
57
- columns: zod.z.array(identifier).min(1),
78
+ columns: zod.z.array(columnIdentifier).min(1),
58
79
  unique: zod.z.boolean().default(false),
59
80
  name: identifier.optional()
60
81
  });
@@ -145,6 +166,13 @@ var canonicalCowMutationSchema = zod.z.object({
145
166
  }
146
167
  }
147
168
  });
169
+ var canonicalForeignKeySchema = zod.z.object({
170
+ name: identifier,
171
+ columns: zod.z.array(columnIdentifier).min(1).max(8),
172
+ target: identifier,
173
+ references: zod.z.array(columnIdentifier).min(1).max(8),
174
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
175
+ }).strict();
148
176
  var canonicalTableSchema = zod.z.object({
149
177
  table: identifier,
150
178
  description: zod.z.string().max(2e3).optional(),
@@ -153,7 +181,7 @@ var canonicalTableSchema = zod.z.object({
153
181
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
154
182
  source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
155
183
  columns: zod.z.array(canonicalColumnSchema).default([]),
156
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
184
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
157
185
  links: zod.z.array(canonicalLinkSchema).default([]),
158
186
  indexes: zod.z.array(canonicalIndexSchema).default([]),
159
187
  policies: canonicalPoliciesSchema.default({}),
@@ -164,14 +192,16 @@ var canonicalTableSchema = zod.z.object({
164
192
  */
165
193
  actions: zod.z.array(canonicalActionSchema).default([]),
166
194
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
167
- cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
195
+ cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
196
+ /** Omitted by old definitions, preserving their existing content hashes. */
197
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
168
198
  });
169
199
  var canonicalViewSchema = zod.z.object({
170
200
  view: identifier,
171
201
  description: zod.z.string().max(2e3).optional(),
172
202
  columns: zod.z.array(canonicalColumnSchema).min(1),
173
203
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
174
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
204
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
175
205
  links: zod.z.array(canonicalLinkSchema).default([]),
176
206
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
177
207
  query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
@@ -184,6 +214,9 @@ var canonicalViewSchema = zod.z.object({
184
214
  if (column.default !== void 0 || column.defaultFn !== void 0) {
185
215
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
186
216
  }
217
+ if (column.onUpdateFn !== void 0) {
218
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare onUpdateFn` });
219
+ }
187
220
  if (column.unique) {
188
221
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
189
222
  }
@@ -217,6 +250,45 @@ var canonicalDefinitionSchema = zod.z.object({
217
250
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
218
251
  }
219
252
  seen.add(table.table);
253
+ if (table.source !== void 0) {
254
+ for (const [columnIndex, column] of table.columns.entries()) {
255
+ if (column.onUpdateFn !== void 0) {
256
+ ctx.addIssue({
257
+ code: "custom",
258
+ path: ["tables", index, "columns", columnIndex],
259
+ message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
260
+ });
261
+ }
262
+ }
263
+ }
264
+ }
265
+ const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
266
+ for (const [tableIndex, table] of definition.tables.entries()) {
267
+ const names = /* @__PURE__ */ new Set([
268
+ ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
269
+ ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
270
+ ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
271
+ ]);
272
+ for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
273
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
274
+ if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
275
+ names.add(fk.name);
276
+ const target = fkTables.get(fk.target);
277
+ if (table.source || !target || target.source) {
278
+ fail("foreign keys must connect owned tables in the same namespace");
279
+ continue;
280
+ }
281
+ if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
282
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
283
+ for (const [position, name] of fk.columns.entries()) {
284
+ const sourceColumn = table.columns.find((column) => column.columnName === name);
285
+ const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
286
+ if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
287
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
288
+ }
289
+ const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
290
+ if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail("foreign key target columns must form a unique key");
291
+ }
220
292
  }
221
293
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
222
294
  for (const [tableIndex, table] of definition.tables.entries()) {
@@ -352,12 +424,13 @@ function normalizeCanonical(definition) {
352
424
  ...definition,
353
425
  enums: sortByName(definition.enums),
354
426
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
355
- const { cowMutations, ...rest } = t;
427
+ const { cowMutations, foreignKeys, ...rest } = t;
356
428
  return {
357
429
  ...rest,
358
430
  links: sortByName(t.links),
359
431
  indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
360
432
  actions: sortByName(t.actions),
433
+ ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
361
434
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
362
435
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
363
436
  };
@@ -446,6 +519,8 @@ var ORDER_BY_ENUM_VALUES = [
446
519
  "DESC_NULLS_LAST"
447
520
  ];
448
521
  var PLATFORM_USER_TYPENAME = "PlatformUser";
522
+ var DATE_BUCKET_ENUM = "DateBucket";
523
+ var BUCKETABLE_COLUMN_TYPES = ["timestamptz", "date"];
449
524
  function tableNames(table) {
450
525
  const root = camelCase(table);
451
526
  const rootPascal = pascalCase(table);
@@ -478,9 +553,327 @@ function tableNames(table) {
478
553
  sumFields: `${type}SumFields`,
479
554
  avgFields: `${type}AvgFields`,
480
555
  aggregateBoolExp: `${type}AggregateBoolExp`,
481
- aggregateBoolExpCount: `${type}AggregateBoolExpCount`
556
+ aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
557
+ aggregateGroupsRoot: `${root}AggregateGroups`,
558
+ groupByColumnEnum: `${type}GroupByColumn`,
559
+ groupByInput: `${type}GroupBy`,
560
+ groupKey: `${type}GroupKey`,
561
+ aggregateGroup: `${type}AggregateGroup`
482
562
  };
483
563
  }
564
+ var IR_VERSION = 2;
565
+ var LEGACY_IR_VERSION = 1;
566
+ var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
567
+ zod.z.object({ irVersion: irVersionSchema });
568
+ var scalarLiteral = zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]);
569
+ var valueIrSchema = zod.z.discriminatedUnion("kind", [
570
+ zod.z.object({ kind: zod.z.literal("literal"), value: zod.z.union([scalarLiteral, zod.z.array(scalarLiteral)]) }),
571
+ zod.z.object({ kind: zod.z.literal("variable"), name: zod.z.string() }),
572
+ zod.z.object({ kind: zod.z.literal("claim"), key: zod.z.enum(["sub", "org"]) })
573
+ ]);
574
+ var CMP_OPS = [
575
+ "eq",
576
+ "neq",
577
+ "gt",
578
+ "gte",
579
+ "lt",
580
+ "lte",
581
+ "in",
582
+ "nin",
583
+ "like",
584
+ "ilike",
585
+ "isNull",
586
+ "contains",
587
+ "hasKey"
588
+ ];
589
+ var predIrSchema = zod.z.lazy(
590
+ () => zod.z.discriminatedUnion("kind", [
591
+ zod.z.object({ kind: zod.z.literal("and"), operands: zod.z.array(predIrSchema).min(1) }),
592
+ zod.z.object({ kind: zod.z.literal("or"), operands: zod.z.array(predIrSchema).min(1) }),
593
+ zod.z.object({ kind: zod.z.literal("not"), operand: predIrSchema }),
594
+ zod.z.object({ kind: zod.z.literal("cmp"), op: zod.z.enum(CMP_OPS), column: zod.z.string(), value: valueIrSchema }),
595
+ zod.z.object({ kind: zod.z.literal("exists"), link: zod.z.string(), where: predIrSchema }),
596
+ zod.z.object({
597
+ kind: zod.z.literal("aggPredicate"),
598
+ link: zod.z.string(),
599
+ fn: zod.z.literal("count"),
600
+ op: zod.z.enum(["eq", "neq", "gt", "gte", "lt", "lte"]),
601
+ value: zod.z.number(),
602
+ where: predIrSchema.optional()
603
+ }),
604
+ zod.z.object({ kind: zod.z.literal("capability"), key: zod.z.string() }),
605
+ zod.z.object({ kind: zod.z.literal("role"), key: zod.z.string() }),
606
+ zod.z.object({ kind: zod.z.literal("dynamicFilter"), slot: zod.z.string() }),
607
+ zod.z.object({ kind: zod.z.literal("const"), value: zod.z.boolean() })
608
+ ])
609
+ );
610
+ var orderIrSchema = zod.z.discriminatedUnion("kind", [
611
+ zod.z.object({
612
+ kind: zod.z.literal("column"),
613
+ column: zod.z.string(),
614
+ /** Order through a to-one link: `link` is set and `column` is on the target. */
615
+ link: zod.z.string().optional(),
616
+ direction: zod.z.enum(["asc", "desc"]),
617
+ nulls: zod.z.enum(["first", "last"]).optional()
618
+ }),
619
+ /** §4f: `$orderBy: [FooOrderBy!]` variable — validated + lowered at runtime. */
620
+ zod.z.object({ kind: zod.z.literal("dynamic"), slot: zod.z.string() })
621
+ ]);
622
+ var limitIrSchema = zod.z.object({
623
+ /** Fixed value or a variable, clamped to [1, max]; absent → default. */
624
+ variable: zod.z.string().optional(),
625
+ value: zod.z.number().int().positive().optional(),
626
+ default: zod.z.number().int().positive(),
627
+ max: zod.z.number().int().positive()
628
+ });
629
+ var selectionIrSchema = zod.z.lazy(
630
+ () => zod.z.object({
631
+ typename: zod.z.string(),
632
+ columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
633
+ relations: zod.z.array(zod.z.object({
634
+ alias: zod.z.string(),
635
+ link: zod.z.string(),
636
+ selection: selectionIrSchema,
637
+ where: predIrSchema.optional(),
638
+ orderBy: zod.z.array(orderIrSchema).optional(),
639
+ limit: limitIrSchema.optional(),
640
+ offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional()
641
+ })),
642
+ aggregates: zod.z.array(zod.z.object({
643
+ alias: zod.z.string(),
644
+ link: zod.z.string().optional(),
645
+ where: predIrSchema.optional(),
646
+ count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
647
+ sum: zod.z.array(zod.z.string()).optional(),
648
+ avg: zod.z.array(zod.z.string()).optional(),
649
+ nodes: selectionIrSchema.optional()
650
+ }))
651
+ })
652
+ );
653
+ var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
654
+ var selectRootSchema = zod.z.object({
655
+ kind: zod.z.literal("select"),
656
+ alias: zod.z.string(),
657
+ table: zod.z.string(),
658
+ selection: selectionIrSchema,
659
+ /** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
660
+ where: predIrSchema,
661
+ orderBy: zod.z.array(orderIrSchema).default([]),
662
+ distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
663
+ limit: limitIrSchema,
664
+ offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
665
+ includeIf: includeIfSchema.optional()
666
+ });
667
+ var byPkRootSchema = zod.z.object({
668
+ kind: zod.z.literal("byPk"),
669
+ alias: zod.z.string(),
670
+ table: zod.z.string(),
671
+ selection: selectionIrSchema,
672
+ pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
673
+ /** Policy predicate — ANDed with the PK match. */
674
+ where: predIrSchema,
675
+ includeIf: includeIfSchema.optional()
676
+ });
677
+ var aggregateRootSchema = zod.z.object({
678
+ kind: zod.z.literal("aggregate"),
679
+ alias: zod.z.string(),
680
+ table: zod.z.string(),
681
+ where: predIrSchema,
682
+ count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
683
+ sum: zod.z.array(zod.z.string()).optional(),
684
+ avg: zod.z.array(zod.z.string()).optional(),
685
+ nodes: selectionIrSchema.optional(),
686
+ includeIf: includeIfSchema.optional()
687
+ });
688
+ var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
689
+ var groupByKeyIrSchema = zod.z.object({
690
+ column: zod.z.string(),
691
+ bucket: zod.z.enum(DATE_BUCKETS).optional()
692
+ });
693
+ var aggregateGroupsRootSchema = zod.z.object({
694
+ kind: zod.z.literal("aggregateGroups"),
695
+ alias: zod.z.string(),
696
+ table: zod.z.string(),
697
+ /** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
698
+ where: predIrSchema,
699
+ groupBy: zod.z.array(groupByKeyIrSchema).min(1),
700
+ /**
701
+ * The `key { … }` projection: which group columns appear in each entry's key
702
+ * object, in selection order. A subset of `groupBy` (the compiler rejects a
703
+ * key field that is not a group key — it would be indeterminate within the
704
+ * group). Empty when the document selects only `aggregate`.
705
+ */
706
+ key: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })).default([]),
707
+ count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
708
+ sum: zod.z.array(zod.z.string()).optional(),
709
+ avg: zod.z.array(zod.z.string()).optional(),
710
+ /** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
711
+ limit: limitIrSchema,
712
+ offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
713
+ includeIf: includeIfSchema.optional()
714
+ });
715
+ var writeValuesSchema = zod.z.union([
716
+ zod.z.object({ slot: zod.z.string() }),
717
+ zod.z.object({ entries: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })) })
718
+ ]);
719
+ var onConflictIrSchema = zod.z.union([
720
+ zod.z.object({ slot: zod.z.string() }),
721
+ zod.z.object({ constraint: zod.z.string(), updateColumns: zod.z.array(zod.z.string()) })
722
+ ]);
723
+ var mutationRootSchema = zod.z.object({
724
+ kind: zod.z.literal("mutation"),
725
+ /**
726
+ * CRUD verbs plus the structured copy-on-write ops. `cowOverride` copies the
727
+ * synced row into the overlay (INSERT…SELECT, ON CONFLICT DO NOTHING) then
728
+ * applies `set` — both statements generated by the executor from this
729
+ * metadata, never authored SQL. `cowRevert` deletes the override so the
730
+ * coalesce view falls back to synced. Both are gated on a matching synced
731
+ * row and carry the write policy in `where` like update/delete.
732
+ */
733
+ op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
734
+ alias: zod.z.string(),
735
+ table: zod.z.string(),
736
+ /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
737
+ cowSynced: zod.z.string().optional(),
738
+ /** cowOverride: logical properties copied INSERT…SELECT from synced (pk included). */
739
+ cowCopyColumns: zod.z.array(zod.z.string()).optional(),
740
+ /** cowOverride: insert-policy WITH CHECK evaluated over the copied rows. */
741
+ cowCopyCheck: predIrSchema.optional(),
742
+ /** insert/insertOne: rows come from a slot ($object/$objects) or literals. */
743
+ objects: zod.z.union([
744
+ zod.z.object({ slot: zod.z.string() }),
745
+ zod.z.object({ rows: zod.z.array(zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema }))) })
746
+ ]).optional(),
747
+ /** Server-pinned columns (e.g. author_sub ← claim('sub')) merged into every inserted row. */
748
+ pinnedColumns: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).default([]),
749
+ onConflict: onConflictIrSchema.optional(),
750
+ set: writeValuesSchema.optional(),
751
+ append: writeValuesSchema.optional(),
752
+ inc: writeValuesSchema.optional(),
753
+ /** update/delete row filter: AND(writePolicy, documentWhere/pk, dynamicFilter?). */
754
+ where: predIrSchema.optional(),
755
+ pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).optional(),
756
+ /** WITH CHECK analogue evaluated against NEW row values (insert/update). */
757
+ insertCheck: predIrSchema.optional(),
758
+ returning: selectionIrSchema.nullable(),
759
+ affectedRows: zod.z.boolean().default(false),
760
+ /** v2: abort the transaction when the actual write count differs, regardless of selection. */
761
+ assertAffectedRows: zod.z.number().int().min(0).max(2147483647).optional(),
762
+ includeIf: includeIfSchema.optional()
763
+ });
764
+ var rootIrSchema = zod.z.discriminatedUnion("kind", [
765
+ selectRootSchema,
766
+ byPkRootSchema,
767
+ aggregateRootSchema,
768
+ aggregateGroupsRootSchema,
769
+ mutationRootSchema
770
+ ]);
771
+ var INPUT_REF_KINDS = [
772
+ "boolExp",
773
+ "orderBy",
774
+ "selectColumn",
775
+ "insertInput",
776
+ "setInput",
777
+ "appendInput",
778
+ "onConflict"
779
+ ];
780
+ var variableDefaultIrSchema = zod.z.object({
781
+ value: zod.z.union([scalarLiteral, zod.z.array(scalarLiteral)])
782
+ });
783
+ var variableIrSchema = zod.z.object({
784
+ name: zod.z.string(),
785
+ type: zod.z.union([
786
+ zod.z.object({ kind: zod.z.literal("scalar"), scalar: zod.z.enum(COLUMN_TYPES) }),
787
+ /** `values` is the declared membership whitelist — the executor validates against it. */
788
+ zod.z.object({ kind: zod.z.literal("enum"), name: zod.z.string(), values: zod.z.array(zod.z.string()).min(1) }),
789
+ /** §4f dynamic slot: validated against `tables[table]` whitelists at runtime. */
790
+ zod.z.object({ kind: zod.z.literal("inputRef"), input: zod.z.enum(INPUT_REF_KINDS), table: zod.z.string() })
791
+ ]),
792
+ nullable: zod.z.boolean(),
793
+ list: zod.z.boolean(),
794
+ /** Absent = no default declared; see {@link variableDefaultIrSchema}. */
795
+ defaultValue: variableDefaultIrSchema.optional()
796
+ });
797
+ var compiledOperationSchema = zod.z.object({
798
+ irVersion: irVersionSchema,
799
+ name: zod.z.string(),
800
+ opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
801
+ kind: zod.z.enum(["query", "mutation"]),
802
+ requires: zod.z.object({
803
+ capability: zod.z.string().optional(),
804
+ role: zod.z.string().optional()
805
+ }).nullable(),
806
+ variables: zod.z.array(variableIrSchema),
807
+ /** Every table the op touches — data-token scoping + audit. */
808
+ tableSet: zod.z.array(zod.z.string()).min(1),
809
+ roots: zod.z.array(rootIrSchema).min(1)
810
+ }).superRefine((operation, ctx) => {
811
+ for (const [index, root] of operation.roots.entries()) {
812
+ if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
813
+ if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
814
+ ctx.addIssue({ code: "custom", path: ["roots", index], message: "affected-row assertions require unconditional mutation roots and IR v2" });
815
+ }
816
+ }
817
+ });
818
+ var tableMetaIrSchema = zod.z.object({
819
+ table: zod.z.string(),
820
+ schema: zod.z.string(),
821
+ typename: zod.z.string(),
822
+ /**
823
+ * True when statements over this relation evaluate a claims-gated synced
824
+ * view: synced twins (`source: gold.*`) materialize as views whose baked
825
+ * policy reads `current_setting('request.jwt.claims')`, and namespace views
826
+ * inherit the gate transitively through their declared dependencies. The
827
+ * executor must stamp that GUC (transaction-scoped) before any statement
828
+ * touching such a relation, or the synced arm yields zero rows regardless
829
+ * of the compiled predicates.
830
+ */
831
+ claimsGated: zod.z.boolean().default(false),
832
+ columns: zod.z.array(zod.z.object({
833
+ name: zod.z.string(),
834
+ columnName: zod.z.string(),
835
+ type: zod.z.union([zod.z.enum(COLUMN_TYPES), zod.z.object({ enum: zod.z.string(), values: zod.z.array(zod.z.string()) })]),
836
+ nullable: zod.z.boolean()
837
+ })),
838
+ /** Empty for keyless read-only views; keyed tables and views list logical columns. */
839
+ primaryKey: zod.z.array(zod.z.string()),
840
+ links: zod.z.array(zod.z.object({
841
+ name: zod.z.string(),
842
+ kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
843
+ target: zod.z.string(),
844
+ on: zod.z.string(),
845
+ references: zod.z.string()
846
+ })),
847
+ /** §4f whitelists: what dynamic inputs may reference. */
848
+ filterable: zod.z.array(zod.z.string()),
849
+ orderable: zod.z.array(zod.z.string()),
850
+ insertable: zod.z.array(zod.z.string()),
851
+ settable: zod.z.array(zod.z.string()),
852
+ appendable: zod.z.array(zod.z.string()),
853
+ /**
854
+ * Logical columns stamped with Postgres `now()` on UPDATE (canonical
855
+ * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
856
+ */
857
+ touchOnUpdate: zod.z.array(zod.z.string()).optional(),
858
+ constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
859
+ /** Depth/node caps for dynamic filter trees on this table. */
860
+ dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
861
+ dynamicFilterMaxNodes: zod.z.number().int().positive().default(64)
862
+ });
863
+ zod.z.object({
864
+ irVersion: irVersionSchema,
865
+ namespace: zod.z.string(),
866
+ schemaHash: zod.z.string(),
867
+ opsHash: zod.z.string(),
868
+ tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
869
+ operations: zod.z.record(zod.z.string(), compiledOperationSchema)
870
+ }).superRefine((artifact, ctx) => {
871
+ for (const [name, operation] of Object.entries(artifact.operations)) {
872
+ if (operation.irVersion > artifact.irVersion) {
873
+ ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
874
+ }
875
+ }
876
+ });
484
877
 
485
878
  // src/sdl/generate.ts
486
879
  var SdlGenerationError = class extends Error {
@@ -559,8 +952,12 @@ function generateSdl(canonical, options = {}) {
559
952
  '"""Gates an operation behind a platform capability or custom role \u2014 checked by the data plane before execution."""\ndirective @requires(capability: String, role: String) on QUERY | MUTATION'
560
953
  );
561
954
  blocks.push("directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT");
955
+ blocks.push(
956
+ '"""Abort the entire mutation unless this root affects exactly the document-authored number of rows. Mutation roots only; cannot be conditional."""\ndirective @assertAffectedRows(exactly: Int!) on FIELD'
957
+ );
562
958
  for (const scalar of CUSTOM_SCALARS) blocks.push(`scalar ${scalar}`);
563
959
  blocks.push(renderEnum(ORDER_BY_ENUM, [...ORDER_BY_ENUM_VALUES]));
960
+ blocks.push(renderEnum(DATE_BUCKET_ENUM, DATE_BUCKETS.map((bucket) => bucket.toUpperCase())));
564
961
  for (const enumName of [...usedEnums].sort()) {
565
962
  const entry = enumByName.get(enumName);
566
963
  if (!entry) continue;
@@ -590,9 +987,24 @@ function generateSdl(canonical, options = {}) {
590
987
  blocks.push(renderQueryRoot(tables, namesByTable));
591
988
  const mutationRoot = renderMutationRoot(tables, namesByTable);
592
989
  if (mutationRoot !== null) blocks.push(mutationRoot);
990
+ assertNoDuplicateTypeNames(blocks);
593
991
  return `${blocks.join("\n\n")}
594
992
  `;
595
993
  }
994
+ var TYPE_DECLARATION_RE = /^(?:type|input|enum|scalar|interface|union)\s+([_A-Za-z][_0-9A-Za-z]*)/;
995
+ function assertNoDuplicateTypeNames(blocks) {
996
+ const seen = /* @__PURE__ */ new Set();
997
+ for (const block of blocks) {
998
+ const declared = TYPE_DECLARATION_RE.exec(block)?.[1];
999
+ if (declared === void 0) continue;
1000
+ if (seen.has(declared)) {
1001
+ throw new SdlGenerationError(
1002
+ `Generated schema declares "${declared}" twice. A canonical enum or table name is PascalCased into this GraphQL type name and collides with another declaration (for example an enum named "date_bucket" renders as "DateBucket", the shared bucket enum). Rename the offending enum or table.`
1003
+ );
1004
+ }
1005
+ seen.add(declared);
1006
+ }
1007
+ }
596
1008
  function assertEnumValue(value, context) {
597
1009
  if (!GRAPHQL_NAME_RE.test(value) || RESERVED_ENUM_VALUES.has(value)) {
598
1010
  throw new SdlGenerationError(
@@ -681,7 +1093,8 @@ function renderTableBlocks(ctx) {
681
1093
  renderSelectColumnEnum(ctx),
682
1094
  renderOrderByInput(ctx),
683
1095
  renderBoolExp(ctx),
684
- ...renderAggregateTypes(ctx)
1096
+ ...renderAggregateTypes(ctx),
1097
+ ...renderGroupByTypes(ctx)
685
1098
  ];
686
1099
  if (ctx.emitAggregateBoolExp) blocks.push(...renderAggregateBoolExp(ctx));
687
1100
  const pk = primaryKeyColumns(table);
@@ -818,6 +1231,32 @@ ${sumLines.join("\n")}
818
1231
  }
819
1232
  return blocks;
820
1233
  }
1234
+ function groupableColumns(table) {
1235
+ return table.columns.filter((column) => column.type !== "jsonb");
1236
+ }
1237
+ function renderGroupByTypes(ctx) {
1238
+ const { table, names } = ctx;
1239
+ const groupable = groupableColumns(table);
1240
+ if (groupable.length === 0) return [];
1241
+ const keyLines = groupable.map((column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`);
1242
+ return [
1243
+ renderEnum(names.groupByColumnEnum, groupable.map((column) => fieldName(column.name))),
1244
+ `input ${names.groupByInput} {
1245
+ column: ${names.groupByColumnEnum}!
1246
+ bucket: ${DATE_BUCKET_ENUM}
1247
+ }`,
1248
+ `type ${names.groupKey} {
1249
+ ${keyLines.join("\n")}
1250
+ }`,
1251
+ `type ${names.aggregateGroup} {
1252
+ key: ${names.groupKey}!
1253
+ aggregate: ${names.aggregateFields}
1254
+ }`
1255
+ ];
1256
+ }
1257
+ function groupRootArgs(names) {
1258
+ return `where: ${names.boolExp}, groupBy: [${names.groupByInput}!]!, limit: Int, offset: Int`;
1259
+ }
821
1260
  function renderAggregateBoolExp(ctx) {
822
1261
  const { names } = ctx;
823
1262
  return [
@@ -843,6 +1282,9 @@ function constraintValues(table) {
843
1282
  }
844
1283
  return [...values].sort();
845
1284
  }
1285
+ function clientSettableColumns(table) {
1286
+ return table.columns.filter((column) => column.onUpdateFn === void 0);
1287
+ }
846
1288
  function renderWriteBlocks(ctx, access) {
847
1289
  const { table, names, pinned } = ctx;
848
1290
  const blocks = [];
@@ -864,7 +1306,7 @@ ${insertLines.join("\n")}
864
1306
  }`);
865
1307
  }
866
1308
  if (access.update) {
867
- const setLines = table.columns.map(
1309
+ const setLines = clientSettableColumns(table).map(
868
1310
  (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
869
1311
  );
870
1312
  blocks.push(`input ${names.setInput} {
@@ -891,7 +1333,7 @@ ${incLines.join("\n")}
891
1333
  if (access.insert && allowsOnConflict(table) && constraints.length > 0) {
892
1334
  for (const value of constraints) assertEnumValue(value, `constraint enum for table "${table.table}"`);
893
1335
  blocks.push(renderEnum(names.constraintEnum, constraints));
894
- const updateColumns = table.columns.map((column) => fieldName(column.name));
1336
+ const updateColumns = clientSettableColumns(table).map((column) => fieldName(column.name));
895
1337
  blocks.push(renderEnum(names.updateColumnEnum, updateColumns));
896
1338
  blocks.push(
897
1339
  `input ${names.onConflict} {
@@ -923,6 +1365,9 @@ function renderQueryRoot(tables, namesByTable) {
923
1365
  lines.push(` ${names.byPkRoot}(${args}): ${names.typename}`);
924
1366
  }
925
1367
  lines.push(` ${names.aggregateRoot}(${rootArgs(names)}): ${names.aggregate}!`);
1368
+ if (groupableColumns(table).length > 0) {
1369
+ lines.push(` ${names.aggregateGroupsRoot}(${groupRootArgs(names)}): [${names.aggregateGroup}!]!`);
1370
+ }
926
1371
  }
927
1372
  lines.push("}");
928
1373
  return lines.join("\n");
@@ -1095,7 +1540,9 @@ ${body}
1095
1540
  `;
1096
1541
  }
1097
1542
 
1543
+ exports.BUCKETABLE_COLUMN_TYPES = BUCKETABLE_COLUMN_TYPES;
1098
1544
  exports.CUSTOM_SCALARS = CUSTOM_SCALARS;
1545
+ exports.DATE_BUCKET_ENUM = DATE_BUCKET_ENUM;
1099
1546
  exports.MANIFEST_VERSION = MANIFEST_VERSION;
1100
1547
  exports.NUMERICISH_COLUMN_TYPES = NUMERICISH_COLUMN_TYPES;
1101
1548
  exports.ORDER_BY_ENUM = ORDER_BY_ENUM;