@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.
- package/README.md +108 -38
- package/dist/authoring.cjs +128 -16
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +21 -4
- package/dist/authoring.d.ts +21 -4
- package/dist/authoring.js +128 -16
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +83 -9
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +47 -1
- package/dist/canonical.d.ts +47 -1
- package/dist/canonical.js +83 -10
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +816 -134
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +818 -136
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +488 -96
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +34 -2
- package/dist/compiler.d.ts +34 -2
- package/dist/compiler.js +489 -97
- package/dist/compiler.js.map +1 -1
- package/dist/index.cjs +420 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +266 -9
- package/dist/index.d.ts +266 -9
- package/dist/index.js +410 -73
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +146 -12
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +161 -5
- package/dist/ir.d.ts +161 -5
- package/dist/ir.js +144 -13
- package/dist/ir.js.map +1 -1
- package/dist/{naming-CTvWSRTf.d.ts → naming-DCn94LJ4.d.ts} +15 -1
- package/dist/{naming-DRyinU4u.d.cts → naming-DPglwzkB.d.cts} +15 -1
- package/dist/predicate-v2.cjs +136 -11
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +3 -0
- package/dist/predicate-v2.d.ts +3 -0
- package/dist/predicate-v2.js +136 -11
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +460 -13
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +1 -1
- package/dist/sdl.d.ts +1 -1
- package/dist/sdl.js +459 -14
- package/dist/sdl.js.map +1 -1
- package/package.json +6 -4
- package/src/codegen-gql/preset.cjs +64 -0
- package/src/codegen-gql/server-fns.cjs +11 -6
- package/templates/orm-namespace/codegen.preset.cjs +17 -0
- package/templates/orm-namespace/codegen.ts +1 -1
- package/templates/orm-namespace/package.json +3 -0
- package/templates/orm-namespace/schema/tables.ts +1 -1
- package/templates/orm-namespace/sequence.config.ts +1 -1
package/dist/compiler.cjs
CHANGED
|
@@ -7,6 +7,7 @@ var graphql = require('graphql');
|
|
|
7
7
|
// src/compiler-gql/artifact.ts
|
|
8
8
|
|
|
9
9
|
// src/constants.ts
|
|
10
|
+
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
10
11
|
var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
|
|
11
12
|
var ORM_SCHEMA_PREFIX = "orm_";
|
|
12
13
|
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
@@ -21,6 +22,7 @@ function stableJson(value) {
|
|
|
21
22
|
var CANONICAL_FORMAT = 2;
|
|
22
23
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
23
24
|
var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
25
|
+
var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
24
26
|
var COLUMN_TYPES = [
|
|
25
27
|
"uuid",
|
|
26
28
|
"text",
|
|
@@ -41,18 +43,33 @@ var canonicalEnumSchema = zod.z.object({
|
|
|
41
43
|
var canonicalColumnSchema = zod.z.object({
|
|
42
44
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
43
45
|
name: zod.z.string().min(1).max(63),
|
|
44
|
-
columnName:
|
|
46
|
+
columnName: columnIdentifier,
|
|
45
47
|
type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
|
|
46
48
|
nullable: zod.z.boolean().default(true),
|
|
47
49
|
unique: zod.z.boolean().default(false),
|
|
48
50
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
49
51
|
default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
|
|
50
52
|
defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
|
|
53
|
+
/**
|
|
54
|
+
* Stamp this column from the database clock on UPDATE (`now()`). The
|
|
55
|
+
* column stays insertable (defaults fill gaps) but is omitted from
|
|
56
|
+
* SetInput / settable so callers cannot backdate it.
|
|
57
|
+
*/
|
|
58
|
+
onUpdateFn: zod.z.enum(["now()"]).optional(),
|
|
51
59
|
description: zod.z.string().max(500).optional(),
|
|
52
60
|
/** Migration hints — same semantics as v1. */
|
|
53
|
-
renamedFrom:
|
|
61
|
+
renamedFrom: columnIdentifier.optional(),
|
|
54
62
|
cast: zod.z.string().max(200).optional(),
|
|
55
63
|
deprecated: zod.z.boolean().default(false)
|
|
64
|
+
}).superRefine((column, ctx) => {
|
|
65
|
+
if (column.onUpdateFn === void 0) return;
|
|
66
|
+
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
67
|
+
ctx.addIssue({
|
|
68
|
+
code: "custom",
|
|
69
|
+
path: ["onUpdateFn"],
|
|
70
|
+
message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
|
|
71
|
+
});
|
|
72
|
+
}
|
|
56
73
|
});
|
|
57
74
|
var canonicalLinkSchema = zod.z.object({
|
|
58
75
|
name: zod.z.string().min(1).max(63),
|
|
@@ -60,15 +77,15 @@ var canonicalLinkSchema = zod.z.object({
|
|
|
60
77
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
61
78
|
target: identifier.optional(),
|
|
62
79
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
63
|
-
on:
|
|
80
|
+
on: columnIdentifier,
|
|
64
81
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
65
|
-
references:
|
|
82
|
+
references: columnIdentifier.optional(),
|
|
66
83
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
67
84
|
enforce: zod.z.boolean().default(false),
|
|
68
85
|
description: zod.z.string().max(500).optional()
|
|
69
86
|
});
|
|
70
87
|
var canonicalIndexSchema = zod.z.object({
|
|
71
|
-
columns: zod.z.array(
|
|
88
|
+
columns: zod.z.array(columnIdentifier).min(1),
|
|
72
89
|
unique: zod.z.boolean().default(false),
|
|
73
90
|
name: identifier.optional()
|
|
74
91
|
});
|
|
@@ -159,6 +176,13 @@ var canonicalCowMutationSchema = zod.z.object({
|
|
|
159
176
|
}
|
|
160
177
|
}
|
|
161
178
|
});
|
|
179
|
+
var canonicalForeignKeySchema = zod.z.object({
|
|
180
|
+
name: identifier,
|
|
181
|
+
columns: zod.z.array(columnIdentifier).min(1).max(8),
|
|
182
|
+
target: identifier,
|
|
183
|
+
references: zod.z.array(columnIdentifier).min(1).max(8),
|
|
184
|
+
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
|
|
185
|
+
}).strict();
|
|
162
186
|
var canonicalTableSchema = zod.z.object({
|
|
163
187
|
table: identifier,
|
|
164
188
|
description: zod.z.string().max(2e3).optional(),
|
|
@@ -167,7 +191,7 @@ var canonicalTableSchema = zod.z.object({
|
|
|
167
191
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
168
192
|
source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
169
193
|
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
170
|
-
primaryKey: zod.z.array(
|
|
194
|
+
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
171
195
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
172
196
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
173
197
|
policies: canonicalPoliciesSchema.default({}),
|
|
@@ -178,14 +202,16 @@ var canonicalTableSchema = zod.z.object({
|
|
|
178
202
|
*/
|
|
179
203
|
actions: zod.z.array(canonicalActionSchema).default([]),
|
|
180
204
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
181
|
-
cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
|
|
205
|
+
cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
|
|
206
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
207
|
+
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
|
|
182
208
|
});
|
|
183
209
|
var canonicalViewSchema = zod.z.object({
|
|
184
210
|
view: identifier,
|
|
185
211
|
description: zod.z.string().max(2e3).optional(),
|
|
186
212
|
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
187
213
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
188
|
-
primaryKey: zod.z.array(
|
|
214
|
+
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
189
215
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
190
216
|
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
191
217
|
query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
|
|
@@ -198,6 +224,9 @@ var canonicalViewSchema = zod.z.object({
|
|
|
198
224
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
199
225
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
|
|
200
226
|
}
|
|
227
|
+
if (column.onUpdateFn !== void 0) {
|
|
228
|
+
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare onUpdateFn` });
|
|
229
|
+
}
|
|
201
230
|
if (column.unique) {
|
|
202
231
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
|
|
203
232
|
}
|
|
@@ -231,6 +260,45 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
231
260
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
232
261
|
}
|
|
233
262
|
seen.add(table.table);
|
|
263
|
+
if (table.source !== void 0) {
|
|
264
|
+
for (const [columnIndex, column] of table.columns.entries()) {
|
|
265
|
+
if (column.onUpdateFn !== void 0) {
|
|
266
|
+
ctx.addIssue({
|
|
267
|
+
code: "custom",
|
|
268
|
+
path: ["tables", index, "columns", columnIndex],
|
|
269
|
+
message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
276
|
+
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
277
|
+
const names = /* @__PURE__ */ new Set([
|
|
278
|
+
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
279
|
+
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
280
|
+
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
281
|
+
]);
|
|
282
|
+
for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
|
|
283
|
+
const fail2 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
|
|
284
|
+
if (names.has(fk.name)) fail2(`duplicate foreign key '${fk.name}'`);
|
|
285
|
+
names.add(fk.name);
|
|
286
|
+
const target = fkTables.get(fk.target);
|
|
287
|
+
if (table.source || !target || target.source) {
|
|
288
|
+
fail2("foreign keys must connect owned tables in the same namespace");
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (fk.columns.length !== fk.references.length) fail2("foreign key columns and references must have the same number of columns");
|
|
292
|
+
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail2("foreign key columns must not repeat");
|
|
293
|
+
for (const [position, name] of fk.columns.entries()) {
|
|
294
|
+
const sourceColumn = table.columns.find((column) => column.columnName === name);
|
|
295
|
+
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
296
|
+
if (!sourceColumn || !targetColumn) fail2("foreign key references an unknown column");
|
|
297
|
+
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail2("foreign key column types must match");
|
|
298
|
+
}
|
|
299
|
+
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
300
|
+
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail2("foreign key target columns must form a unique key");
|
|
301
|
+
}
|
|
234
302
|
}
|
|
235
303
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
236
304
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
@@ -326,7 +394,7 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
326
394
|
const visiting = /* @__PURE__ */ new Set();
|
|
327
395
|
const visited = /* @__PURE__ */ new Set();
|
|
328
396
|
let cycleReported = false;
|
|
329
|
-
const
|
|
397
|
+
const visit2 = (name, path) => {
|
|
330
398
|
if (visited.has(name) || cycleReported) return;
|
|
331
399
|
if (visiting.has(name)) {
|
|
332
400
|
cycleReported = true;
|
|
@@ -340,12 +408,12 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
340
408
|
visiting.add(name);
|
|
341
409
|
const view = viewByName.get(name);
|
|
342
410
|
for (const dependency of view?.dependencies ?? []) {
|
|
343
|
-
if (viewByName.has(dependency))
|
|
411
|
+
if (viewByName.has(dependency)) visit2(dependency, [...path, name]);
|
|
344
412
|
}
|
|
345
413
|
visiting.delete(name);
|
|
346
414
|
visited.add(name);
|
|
347
415
|
};
|
|
348
|
-
for (const view of views)
|
|
416
|
+
for (const view of views) visit2(view.view, []);
|
|
349
417
|
});
|
|
350
418
|
function canonicalRelations(definition) {
|
|
351
419
|
const views = (definition.views ?? []).map((view) => ({
|
|
@@ -366,12 +434,13 @@ function normalizeCanonical(definition) {
|
|
|
366
434
|
...definition,
|
|
367
435
|
enums: sortByName(definition.enums),
|
|
368
436
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
|
|
369
|
-
const { cowMutations, ...rest } = t;
|
|
437
|
+
const { cowMutations, foreignKeys, ...rest } = t;
|
|
370
438
|
return {
|
|
371
439
|
...rest,
|
|
372
440
|
links: sortByName(t.links),
|
|
373
441
|
indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
374
442
|
actions: sortByName(t.actions),
|
|
443
|
+
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
375
444
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
376
445
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
377
446
|
};
|
|
@@ -388,7 +457,18 @@ function normalizeCanonical(definition) {
|
|
|
388
457
|
}
|
|
389
458
|
|
|
390
459
|
// src/ir/index.ts
|
|
391
|
-
var IR_VERSION =
|
|
460
|
+
var IR_VERSION = 2;
|
|
461
|
+
var LEGACY_IR_VERSION = 1;
|
|
462
|
+
var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
|
|
463
|
+
var operationVersionSchema = zod.z.object({ irVersion: irVersionSchema });
|
|
464
|
+
function requiredArtifactIrVersion(operations) {
|
|
465
|
+
let required = LEGACY_IR_VERSION;
|
|
466
|
+
for (const operation of operations) {
|
|
467
|
+
const { irVersion } = operationVersionSchema.parse(operation);
|
|
468
|
+
if (irVersion > required) required = irVersion;
|
|
469
|
+
}
|
|
470
|
+
return required;
|
|
471
|
+
}
|
|
392
472
|
var CLAIMS_CONTRACT = {
|
|
393
473
|
sub: "string",
|
|
394
474
|
org: "string?",
|
|
@@ -515,6 +595,33 @@ var aggregateRootSchema = zod.z.object({
|
|
|
515
595
|
nodes: selectionIrSchema.optional(),
|
|
516
596
|
includeIf: includeIfSchema.optional()
|
|
517
597
|
});
|
|
598
|
+
var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
|
|
599
|
+
var groupByKeyIrSchema = zod.z.object({
|
|
600
|
+
column: zod.z.string(),
|
|
601
|
+
bucket: zod.z.enum(DATE_BUCKETS).optional()
|
|
602
|
+
});
|
|
603
|
+
var aggregateGroupsRootSchema = zod.z.object({
|
|
604
|
+
kind: zod.z.literal("aggregateGroups"),
|
|
605
|
+
alias: zod.z.string(),
|
|
606
|
+
table: zod.z.string(),
|
|
607
|
+
/** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
|
|
608
|
+
where: predIrSchema,
|
|
609
|
+
groupBy: zod.z.array(groupByKeyIrSchema).min(1),
|
|
610
|
+
/**
|
|
611
|
+
* The `key { … }` projection: which group columns appear in each entry's key
|
|
612
|
+
* object, in selection order. A subset of `groupBy` (the compiler rejects a
|
|
613
|
+
* key field that is not a group key — it would be indeterminate within the
|
|
614
|
+
* group). Empty when the document selects only `aggregate`.
|
|
615
|
+
*/
|
|
616
|
+
key: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })).default([]),
|
|
617
|
+
count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
|
|
618
|
+
sum: zod.z.array(zod.z.string()).optional(),
|
|
619
|
+
avg: zod.z.array(zod.z.string()).optional(),
|
|
620
|
+
/** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
|
|
621
|
+
limit: limitIrSchema,
|
|
622
|
+
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
|
|
623
|
+
includeIf: includeIfSchema.optional()
|
|
624
|
+
});
|
|
518
625
|
var writeValuesSchema = zod.z.union([
|
|
519
626
|
zod.z.object({ slot: zod.z.string() }),
|
|
520
627
|
zod.z.object({ entries: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })) })
|
|
@@ -560,12 +667,15 @@ var mutationRootSchema = zod.z.object({
|
|
|
560
667
|
insertCheck: predIrSchema.optional(),
|
|
561
668
|
returning: selectionIrSchema.nullable(),
|
|
562
669
|
affectedRows: zod.z.boolean().default(false),
|
|
670
|
+
/** v2: abort the transaction when the actual write count differs, regardless of selection. */
|
|
671
|
+
assertAffectedRows: zod.z.number().int().min(0).max(2147483647).optional(),
|
|
563
672
|
includeIf: includeIfSchema.optional()
|
|
564
673
|
});
|
|
565
674
|
var rootIrSchema = zod.z.discriminatedUnion("kind", [
|
|
566
675
|
selectRootSchema,
|
|
567
676
|
byPkRootSchema,
|
|
568
677
|
aggregateRootSchema,
|
|
678
|
+
aggregateGroupsRootSchema,
|
|
569
679
|
mutationRootSchema
|
|
570
680
|
]);
|
|
571
681
|
var INPUT_REF_KINDS = [
|
|
@@ -595,7 +705,7 @@ var variableIrSchema = zod.z.object({
|
|
|
595
705
|
defaultValue: variableDefaultIrSchema.optional()
|
|
596
706
|
});
|
|
597
707
|
var compiledOperationSchema = zod.z.object({
|
|
598
|
-
irVersion:
|
|
708
|
+
irVersion: irVersionSchema,
|
|
599
709
|
name: zod.z.string(),
|
|
600
710
|
opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
601
711
|
kind: zod.z.enum(["query", "mutation"]),
|
|
@@ -607,6 +717,13 @@ var compiledOperationSchema = zod.z.object({
|
|
|
607
717
|
/** Every table the op touches — data-token scoping + audit. */
|
|
608
718
|
tableSet: zod.z.array(zod.z.string()).min(1),
|
|
609
719
|
roots: zod.z.array(rootIrSchema).min(1)
|
|
720
|
+
}).superRefine((operation, ctx) => {
|
|
721
|
+
for (const [index, root] of operation.roots.entries()) {
|
|
722
|
+
if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
|
|
723
|
+
if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
|
|
724
|
+
ctx.addIssue({ code: "custom", path: ["roots", index], message: "affected-row assertions require unconditional mutation roots and IR v2" });
|
|
725
|
+
}
|
|
726
|
+
}
|
|
610
727
|
});
|
|
611
728
|
var tableMetaIrSchema = zod.z.object({
|
|
612
729
|
table: zod.z.string(),
|
|
@@ -643,18 +760,29 @@ var tableMetaIrSchema = zod.z.object({
|
|
|
643
760
|
insertable: zod.z.array(zod.z.string()),
|
|
644
761
|
settable: zod.z.array(zod.z.string()),
|
|
645
762
|
appendable: zod.z.array(zod.z.string()),
|
|
763
|
+
/**
|
|
764
|
+
* Logical columns stamped with Postgres `now()` on UPDATE (canonical
|
|
765
|
+
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
766
|
+
*/
|
|
767
|
+
touchOnUpdate: zod.z.array(zod.z.string()).optional(),
|
|
646
768
|
constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
|
|
647
769
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
648
770
|
dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
|
|
649
771
|
dynamicFilterMaxNodes: zod.z.number().int().positive().default(64)
|
|
650
772
|
});
|
|
651
773
|
var compiledArtifactSchema = zod.z.object({
|
|
652
|
-
irVersion:
|
|
774
|
+
irVersion: irVersionSchema,
|
|
653
775
|
namespace: zod.z.string(),
|
|
654
776
|
schemaHash: zod.z.string(),
|
|
655
777
|
opsHash: zod.z.string(),
|
|
656
778
|
tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
|
|
657
779
|
operations: zod.z.record(zod.z.string(), compiledOperationSchema)
|
|
780
|
+
}).superRefine((artifact, ctx) => {
|
|
781
|
+
for (const [name, operation] of Object.entries(artifact.operations)) {
|
|
782
|
+
if (operation.irVersion > artifact.irVersion) {
|
|
783
|
+
ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
658
786
|
});
|
|
659
787
|
var IrVersionError = class extends Error {
|
|
660
788
|
constructor(found) {
|
|
@@ -665,19 +793,19 @@ var IrVersionError = class extends Error {
|
|
|
665
793
|
function normalizeIr(raw) {
|
|
666
794
|
if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
|
|
667
795
|
const version = raw.irVersion;
|
|
668
|
-
if (version !== IR_VERSION) throw new IrVersionError(version);
|
|
796
|
+
if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
|
|
669
797
|
return compiledArtifactSchema.parse(raw);
|
|
670
798
|
}
|
|
671
799
|
|
|
672
800
|
// src/predicate-v2/ast.ts
|
|
673
801
|
var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
674
|
-
function walkPolicy(node,
|
|
675
|
-
|
|
802
|
+
function walkPolicy(node, visit2) {
|
|
803
|
+
visit2(node);
|
|
676
804
|
if (node.kind === "and" || node.kind === "or") {
|
|
677
|
-
for (const operand of node.operands) walkPolicy(operand,
|
|
805
|
+
for (const operand of node.operands) walkPolicy(operand, visit2);
|
|
678
806
|
}
|
|
679
|
-
if (node.kind === "not") walkPolicy(node.operand,
|
|
680
|
-
if (node.kind === "exists") walkPolicy(node.where,
|
|
807
|
+
if (node.kind === "not") walkPolicy(node.operand, visit2);
|
|
808
|
+
if (node.kind === "exists") walkPolicy(node.where, visit2);
|
|
681
809
|
}
|
|
682
810
|
|
|
683
811
|
// src/predicate-v2/errors.ts
|
|
@@ -815,6 +943,10 @@ var Parser = class {
|
|
|
815
943
|
this.expect("rparen", "')'");
|
|
816
944
|
return inner;
|
|
817
945
|
}
|
|
946
|
+
if (this.peekKeyword("true") || this.peekKeyword("false")) {
|
|
947
|
+
const token = this.advance();
|
|
948
|
+
return { kind: "literal", value: token.value === "true" };
|
|
949
|
+
}
|
|
818
950
|
if (this.peekKeyword("capability") || this.peekKeyword("role")) {
|
|
819
951
|
const fn = this.peek().value;
|
|
820
952
|
const node = this.parseBooleanCall(fn);
|
|
@@ -998,6 +1130,8 @@ var PolicyCompiler = class {
|
|
|
998
1130
|
}
|
|
999
1131
|
compile(node, table, depth) {
|
|
1000
1132
|
switch (node.kind) {
|
|
1133
|
+
case "literal":
|
|
1134
|
+
return { kind: "const", value: node.value };
|
|
1001
1135
|
case "and":
|
|
1002
1136
|
case "or":
|
|
1003
1137
|
return { kind: node.kind, operands: node.operands.map((operand) => this.compile(operand, table, depth)) };
|
|
@@ -1177,21 +1311,21 @@ function andPred(...preds) {
|
|
|
1177
1311
|
if (operands.length === 1) return operands[0];
|
|
1178
1312
|
return { kind: "and", operands };
|
|
1179
1313
|
}
|
|
1180
|
-
function walkPred(pred,
|
|
1181
|
-
|
|
1314
|
+
function walkPred(pred, visit2) {
|
|
1315
|
+
visit2(pred);
|
|
1182
1316
|
switch (pred.kind) {
|
|
1183
1317
|
case "and":
|
|
1184
1318
|
case "or":
|
|
1185
|
-
for (const operand of pred.operands) walkPred(operand,
|
|
1319
|
+
for (const operand of pred.operands) walkPred(operand, visit2);
|
|
1186
1320
|
return;
|
|
1187
1321
|
case "not":
|
|
1188
|
-
walkPred(pred.operand,
|
|
1322
|
+
walkPred(pred.operand, visit2);
|
|
1189
1323
|
return;
|
|
1190
1324
|
case "exists":
|
|
1191
|
-
walkPred(pred.where,
|
|
1325
|
+
walkPred(pred.where, visit2);
|
|
1192
1326
|
return;
|
|
1193
1327
|
case "aggPredicate":
|
|
1194
|
-
if (pred.where) walkPred(pred.where,
|
|
1328
|
+
if (pred.where) walkPred(pred.where, visit2);
|
|
1195
1329
|
return;
|
|
1196
1330
|
default:
|
|
1197
1331
|
return;
|
|
@@ -1216,12 +1350,12 @@ function predContains(haystack, needle) {
|
|
|
1216
1350
|
// src/compiler-gql/inject.ts
|
|
1217
1351
|
function collectPinnedColumns(writePolicy) {
|
|
1218
1352
|
const key = (pin) => `${pin.column}\0${pin.claim}`;
|
|
1219
|
-
const
|
|
1353
|
+
const visit2 = (node) => {
|
|
1220
1354
|
if (node.kind === "and") {
|
|
1221
1355
|
const merged = [];
|
|
1222
1356
|
const seen2 = /* @__PURE__ */ new Set();
|
|
1223
1357
|
for (const operand of node.operands) {
|
|
1224
|
-
for (const pin of
|
|
1358
|
+
for (const pin of visit2(operand)) {
|
|
1225
1359
|
if (seen2.has(key(pin))) continue;
|
|
1226
1360
|
seen2.add(key(pin));
|
|
1227
1361
|
merged.push(pin);
|
|
@@ -1232,9 +1366,9 @@ function collectPinnedColumns(writePolicy) {
|
|
|
1232
1366
|
if (node.kind === "or") {
|
|
1233
1367
|
const [first, ...rest] = node.operands;
|
|
1234
1368
|
if (first === void 0) return [];
|
|
1235
|
-
let common =
|
|
1369
|
+
let common = visit2(first);
|
|
1236
1370
|
for (const operand of rest) {
|
|
1237
|
-
const branch = new Set(
|
|
1371
|
+
const branch = new Set(visit2(operand).map(key));
|
|
1238
1372
|
common = common.filter((pin) => branch.has(key(pin)));
|
|
1239
1373
|
if (common.length === 0) break;
|
|
1240
1374
|
}
|
|
@@ -1246,7 +1380,7 @@ function collectPinnedColumns(writePolicy) {
|
|
|
1246
1380
|
return [];
|
|
1247
1381
|
};
|
|
1248
1382
|
const seen = /* @__PURE__ */ new Set();
|
|
1249
|
-
return
|
|
1383
|
+
return visit2(writePolicy).filter((pin) => {
|
|
1250
1384
|
if (seen.has(pin.column)) return false;
|
|
1251
1385
|
seen.add(pin.column);
|
|
1252
1386
|
return true;
|
|
@@ -1340,6 +1474,11 @@ var PolicyInjector = class {
|
|
|
1340
1474
|
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table)),
|
|
1341
1475
|
...root.nodes ? { nodes: this.injectSelection(root.nodes, root.table) } : {}
|
|
1342
1476
|
};
|
|
1477
|
+
case "aggregateGroups":
|
|
1478
|
+
return {
|
|
1479
|
+
...root,
|
|
1480
|
+
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table))
|
|
1481
|
+
};
|
|
1343
1482
|
case "mutation":
|
|
1344
1483
|
return this.injectMutation(root);
|
|
1345
1484
|
}
|
|
@@ -1432,20 +1571,33 @@ function collectTableSet(roots, ctx) {
|
|
|
1432
1571
|
};
|
|
1433
1572
|
for (const root of roots) {
|
|
1434
1573
|
tables.add(root.table);
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1574
|
+
switch (root.kind) {
|
|
1575
|
+
case "select":
|
|
1576
|
+
case "byPk":
|
|
1577
|
+
visitPred(root.where, root.table);
|
|
1578
|
+
visitSelection(root.selection, root.table);
|
|
1579
|
+
break;
|
|
1580
|
+
case "aggregate":
|
|
1581
|
+
visitPred(root.where, root.table);
|
|
1582
|
+
if (root.nodes) visitSelection(root.nodes, root.table);
|
|
1583
|
+
break;
|
|
1584
|
+
case "aggregateGroups":
|
|
1585
|
+
visitPred(root.where, root.table);
|
|
1586
|
+
break;
|
|
1587
|
+
case "mutation":
|
|
1588
|
+
if (root.cowSynced !== void 0) tables.add(root.cowSynced);
|
|
1589
|
+
if (root.where) visitPred(root.where, root.table);
|
|
1590
|
+
if (root.insertCheck) visitPred(root.insertCheck, root.table);
|
|
1591
|
+
if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
|
|
1592
|
+
if (root.returning) visitSelection(root.returning, root.table);
|
|
1593
|
+
break;
|
|
1594
|
+
default: {
|
|
1595
|
+
const exhausted = root;
|
|
1596
|
+
throw new DocumentCompileError(
|
|
1597
|
+
`internal: unhandled root kind in tableSet collection ${JSON.stringify(exhausted)}`,
|
|
1598
|
+
"internal"
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1449
1601
|
}
|
|
1450
1602
|
}
|
|
1451
1603
|
return [...tables].sort();
|
|
@@ -1544,7 +1696,12 @@ function tableNames(table) {
|
|
|
1544
1696
|
sumFields: `${type}SumFields`,
|
|
1545
1697
|
avgFields: `${type}AvgFields`,
|
|
1546
1698
|
aggregateBoolExp: `${type}AggregateBoolExp`,
|
|
1547
|
-
aggregateBoolExpCount: `${type}AggregateBoolExpCount
|
|
1699
|
+
aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
|
|
1700
|
+
aggregateGroupsRoot: `${root}AggregateGroups`,
|
|
1701
|
+
groupByColumnEnum: `${type}GroupByColumn`,
|
|
1702
|
+
groupByInput: `${type}GroupBy`,
|
|
1703
|
+
groupKey: `${type}GroupKey`,
|
|
1704
|
+
aggregateGroup: `${type}AggregateGroup`
|
|
1548
1705
|
};
|
|
1549
1706
|
}
|
|
1550
1707
|
|
|
@@ -1555,7 +1712,12 @@ function typeNameForTable(table) {
|
|
|
1555
1712
|
}
|
|
1556
1713
|
function queryRootNames(table) {
|
|
1557
1714
|
const names = tableNames(table);
|
|
1558
|
-
return {
|
|
1715
|
+
return {
|
|
1716
|
+
select: names.selectRoot,
|
|
1717
|
+
byPk: names.byPkRoot,
|
|
1718
|
+
aggregate: names.aggregateRoot,
|
|
1719
|
+
aggregateGroups: names.aggregateGroupsRoot
|
|
1720
|
+
};
|
|
1559
1721
|
}
|
|
1560
1722
|
function mutationRootNames(table) {
|
|
1561
1723
|
const names = tableNames(table);
|
|
@@ -1648,7 +1810,14 @@ function resolveColumnEntries(table, enums, diagnostics) {
|
|
|
1648
1810
|
type = { enum: enumDef.name, values: [...enumDef.values] };
|
|
1649
1811
|
}
|
|
1650
1812
|
}
|
|
1651
|
-
return {
|
|
1813
|
+
return {
|
|
1814
|
+
name: column.name,
|
|
1815
|
+
columnName: column.columnName,
|
|
1816
|
+
type,
|
|
1817
|
+
nullable: column.nullable,
|
|
1818
|
+
deprecated: column.deprecated,
|
|
1819
|
+
onUpdateFn: column.onUpdateFn !== void 0
|
|
1820
|
+
};
|
|
1652
1821
|
});
|
|
1653
1822
|
}
|
|
1654
1823
|
function logicalColumn(entries, ref) {
|
|
@@ -1697,6 +1866,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1697
1866
|
primaryKey.push(resolved);
|
|
1698
1867
|
}
|
|
1699
1868
|
const active = entries.filter((c) => !c.deprecated);
|
|
1869
|
+
const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
|
|
1700
1870
|
const constraints = [];
|
|
1701
1871
|
if (primaryKey.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey] });
|
|
1702
1872
|
for (const column of table.columns) {
|
|
@@ -1717,14 +1887,15 @@ function buildCompileContext(rawCanonical) {
|
|
|
1717
1887
|
// Synced twins (and views reading them, resolved transitively below)
|
|
1718
1888
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
1719
1889
|
claimsGated: relationIsClaimsGated(table.table),
|
|
1720
|
-
columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
|
|
1890
|
+
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
1721
1891
|
primaryKey: isView ? primaryKey : primaryKey.length > 0 ? primaryKey : ["id"],
|
|
1722
1892
|
links: [],
|
|
1723
1893
|
filterable: entries.map((c) => c.name),
|
|
1724
1894
|
orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
1725
1895
|
insertable: isView ? [] : active.map((c) => c.name),
|
|
1726
|
-
settable: isView ? [] : active.map((c) => c.name),
|
|
1896
|
+
settable: isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
1727
1897
|
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
1898
|
+
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
1728
1899
|
constraints: isView ? [] : constraints,
|
|
1729
1900
|
dynamicFilterMaxDepth: 6,
|
|
1730
1901
|
dynamicFilterMaxNodes: 64
|
|
@@ -1745,7 +1916,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1745
1916
|
});
|
|
1746
1917
|
continue;
|
|
1747
1918
|
}
|
|
1748
|
-
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false })) : columnEntries.get(targetName);
|
|
1919
|
+
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
|
|
1749
1920
|
const onEntries = link.kind === "toMany" ? targetEntries : entries;
|
|
1750
1921
|
const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
|
|
1751
1922
|
const on = logicalColumn(onEntries, link.on);
|
|
@@ -1842,6 +2013,9 @@ function buildCompileContext(rawCanonical) {
|
|
|
1842
2013
|
queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
|
|
1843
2014
|
}
|
|
1844
2015
|
queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
|
|
2016
|
+
if (table.columns.some((column) => column.type !== "jsonb")) {
|
|
2017
|
+
queryRoots.set(roots.aggregateGroups, { kind: "aggregateGroups", table: table.table });
|
|
2018
|
+
}
|
|
1845
2019
|
const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
1846
2020
|
if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
|
|
1847
2021
|
for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
|
|
@@ -1977,6 +2151,16 @@ function computeOpsHash(opHashes) {
|
|
|
1977
2151
|
}
|
|
1978
2152
|
var DEFAULT_LIMIT = 100;
|
|
1979
2153
|
var MAX_LIMIT = 1e3;
|
|
2154
|
+
var MAX_GROUPS = 1e3;
|
|
2155
|
+
var DATE_BUCKET_BY_ENUM = {
|
|
2156
|
+
YEAR: "year",
|
|
2157
|
+
QUARTER: "quarter",
|
|
2158
|
+
MONTH: "month",
|
|
2159
|
+
WEEK: "week",
|
|
2160
|
+
DAY: "day",
|
|
2161
|
+
HOUR: "hour"
|
|
2162
|
+
};
|
|
2163
|
+
var BUCKETABLE_TYPES = /* @__PURE__ */ new Set(["timestamptz", "date"]);
|
|
1980
2164
|
var BOOLEXP_OPS = {
|
|
1981
2165
|
_eq: "eq",
|
|
1982
2166
|
_neq: "neq",
|
|
@@ -2008,6 +2192,14 @@ function fail(message, code, candidates = []) {
|
|
|
2008
2192
|
function argsByName(field) {
|
|
2009
2193
|
return new Map((field.arguments ?? []).map((arg) => [arg.name.value, arg]));
|
|
2010
2194
|
}
|
|
2195
|
+
function* aggregateBlockFields(selectionSet) {
|
|
2196
|
+
for (const node of selectionSet.selections) {
|
|
2197
|
+
if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
2198
|
+
if (node.name.value === "__typename") continue;
|
|
2199
|
+
if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
2200
|
+
yield node;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2011
2203
|
function assertKnownArgs(field, allowed) {
|
|
2012
2204
|
for (const arg of field.arguments ?? []) {
|
|
2013
2205
|
if (!allowed.includes(arg.name.value)) {
|
|
@@ -2308,11 +2500,8 @@ var OperationLowerer = class {
|
|
|
2308
2500
|
lowerAggregateSelection(selectionSet, table) {
|
|
2309
2501
|
const result = {};
|
|
2310
2502
|
let sawAggregate = false;
|
|
2311
|
-
for (const node of selectionSet
|
|
2312
|
-
if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
2503
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
2313
2504
|
const name = node.name.value;
|
|
2314
|
-
if (name === "__typename") continue;
|
|
2315
|
-
if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
2316
2505
|
if (name === "nodes") {
|
|
2317
2506
|
if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
|
|
2318
2507
|
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
@@ -2322,48 +2511,116 @@ var OperationLowerer = class {
|
|
|
2322
2511
|
if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
2323
2512
|
sawAggregate = true;
|
|
2324
2513
|
if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2514
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
2515
|
+
}
|
|
2516
|
+
if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
2517
|
+
return result;
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* The `{ key {…} aggregate {…} }` block of a grouped aggregate root. Same
|
|
2521
|
+
* `aggregate` sub-block as the ungrouped shape (shared via
|
|
2522
|
+
* `lowerAggregateFields`), but `key` replaces `nodes` — see the IR comment on
|
|
2523
|
+
* `aggregateGroupsRootSchema` for why grouped roots have no `nodes`.
|
|
2524
|
+
*/
|
|
2525
|
+
lowerGroupedAggregateSelection(selectionSet, table, groupBy) {
|
|
2526
|
+
const groupColumns = new Set(groupBy.map((entry) => entry.column));
|
|
2527
|
+
const result = { key: [] };
|
|
2528
|
+
let sawAggregate = false;
|
|
2529
|
+
let sawKey = false;
|
|
2530
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
2531
|
+
const name = node.name.value;
|
|
2532
|
+
if (name === "key") {
|
|
2533
|
+
if (sawKey) fail("one key block per grouped aggregate", "invalid-aggregate");
|
|
2534
|
+
sawKey = true;
|
|
2535
|
+
if (!node.selectionSet) fail("'key' needs a selection set", "invalid-aggregate");
|
|
2536
|
+
result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
|
|
2537
|
+
continue;
|
|
2538
|
+
}
|
|
2539
|
+
if (name === "nodes") {
|
|
2540
|
+
fail("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
|
|
2541
|
+
}
|
|
2542
|
+
if (name !== "aggregate") fail(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
|
|
2543
|
+
if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
2544
|
+
sawAggregate = true;
|
|
2545
|
+
if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
|
|
2546
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
2547
|
+
}
|
|
2548
|
+
const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
|
|
2549
|
+
if (result.key.length === 0 && !rendersAggregate) {
|
|
2550
|
+
fail(
|
|
2551
|
+
"grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
|
|
2552
|
+
"invalid-aggregate",
|
|
2553
|
+
["key", "aggregate"]
|
|
2554
|
+
);
|
|
2555
|
+
}
|
|
2556
|
+
return result;
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* `key { month officer }` → the projected group columns. Every field must be
|
|
2560
|
+
* one of the query's own group keys: any other column is indeterminate
|
|
2561
|
+
* within a group, so selecting it is a document error rather than an
|
|
2562
|
+
* arbitrary pick.
|
|
2563
|
+
*/
|
|
2564
|
+
lowerGroupKeySelection(selectionSet, table, groupColumns) {
|
|
2565
|
+
const key = [];
|
|
2566
|
+
for (const node of selectionSet.selections) {
|
|
2567
|
+
if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
2568
|
+
const name = node.name.value;
|
|
2569
|
+
if (name === "__typename") continue;
|
|
2570
|
+
if (!this.column(table, name)) {
|
|
2571
|
+
fail(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
|
|
2572
|
+
}
|
|
2573
|
+
if (!groupColumns.has(name)) {
|
|
2574
|
+
fail(
|
|
2575
|
+
`key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
|
|
2576
|
+
"invalid-group-key",
|
|
2577
|
+
[...groupColumns]
|
|
2578
|
+
);
|
|
2579
|
+
}
|
|
2580
|
+
key.push({ alias: node.alias?.value ?? name, column: name });
|
|
2581
|
+
}
|
|
2582
|
+
return key;
|
|
2583
|
+
}
|
|
2584
|
+
/** The `count` / `sum {…}` / `avg {…}` fields inside an `aggregate { … }` block. */
|
|
2585
|
+
lowerAggregateFields(selectionSet, table) {
|
|
2586
|
+
const result = {};
|
|
2587
|
+
for (const aggField of aggregateBlockFields(selectionSet)) {
|
|
2588
|
+
const aggName = aggField.name.value;
|
|
2589
|
+
if (aggName === "count") {
|
|
2590
|
+
assertKnownArgs(aggField, ["columns", "distinct"]);
|
|
2591
|
+
const args = argsByName(aggField);
|
|
2592
|
+
const count = {};
|
|
2593
|
+
const columns = args.get("columns");
|
|
2594
|
+
if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
|
|
2595
|
+
const distinct = args.get("distinct");
|
|
2596
|
+
if (distinct) {
|
|
2597
|
+
if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail("count(distinct:) must be a boolean literal", "invalid-aggregate");
|
|
2598
|
+
count.distinct = distinct.value.value;
|
|
2343
2599
|
}
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2600
|
+
result.count = count;
|
|
2601
|
+
continue;
|
|
2602
|
+
}
|
|
2603
|
+
if (aggName === "sum" || aggName === "avg") {
|
|
2604
|
+
if (!aggField.selectionSet) fail(`'${aggName}' needs a column selection`, "invalid-aggregate");
|
|
2605
|
+
const columns = [];
|
|
2606
|
+
for (const columnField of aggField.selectionSet.selections) {
|
|
2607
|
+
if (columnField.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
2608
|
+
if (columnField.name.value === "__typename") continue;
|
|
2609
|
+
const column = this.column(table, columnField.name.value);
|
|
2610
|
+
if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
|
|
2611
|
+
fail(
|
|
2612
|
+
`'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
|
|
2613
|
+
"invalid-aggregate",
|
|
2614
|
+
this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
|
|
2615
|
+
);
|
|
2359
2616
|
}
|
|
2360
|
-
|
|
2361
|
-
continue;
|
|
2617
|
+
columns.push(columnField.name.value);
|
|
2362
2618
|
}
|
|
2363
|
-
|
|
2619
|
+
result[aggName] = columns;
|
|
2620
|
+
continue;
|
|
2364
2621
|
}
|
|
2622
|
+
fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
|
|
2365
2623
|
}
|
|
2366
|
-
if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
2367
2624
|
return result;
|
|
2368
2625
|
}
|
|
2369
2626
|
// -------------------------------------------------------------------------
|
|
@@ -2373,19 +2630,49 @@ var OperationLowerer = class {
|
|
|
2373
2630
|
const roots = [];
|
|
2374
2631
|
for (const node of operation.selectionSet.selections) {
|
|
2375
2632
|
if (node.kind !== graphql.Kind.FIELD) fail("fragments are not supported at the operation root", "invalid-root");
|
|
2633
|
+
if (node.selectionSet) graphql.visit(node.selectionSet, {
|
|
2634
|
+
Directive(directive) {
|
|
2635
|
+
if (directive.name.value === "assertAffectedRows") {
|
|
2636
|
+
fail("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
});
|
|
2640
|
+
const assertion = this.parseAffectedRowAssertion(node, kind);
|
|
2376
2641
|
const gate = this.parseRootDirectives(node);
|
|
2377
2642
|
if (gate.omit) continue;
|
|
2378
2643
|
const root = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
|
|
2379
|
-
|
|
2644
|
+
const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
|
|
2645
|
+
roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
|
|
2380
2646
|
}
|
|
2381
2647
|
if (roots.length === 0) fail("operation has no roots (all statically skipped?)", "invalid-root");
|
|
2382
2648
|
return roots;
|
|
2383
2649
|
}
|
|
2650
|
+
parseAffectedRowAssertion(node, kind) {
|
|
2651
|
+
const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
|
|
2652
|
+
if (!assertions.length) return void 0;
|
|
2653
|
+
if (kind !== "mutation" || assertions.length !== 1) {
|
|
2654
|
+
fail("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
|
|
2655
|
+
}
|
|
2656
|
+
if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
|
|
2657
|
+
fail("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
|
|
2658
|
+
}
|
|
2659
|
+
const args = assertions[0].arguments;
|
|
2660
|
+
const arg = args?.[0];
|
|
2661
|
+
if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== graphql.Kind.INT) {
|
|
2662
|
+
fail("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
|
|
2663
|
+
}
|
|
2664
|
+
const count = Number(arg.value.value);
|
|
2665
|
+
if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
|
|
2666
|
+
fail("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
|
|
2667
|
+
}
|
|
2668
|
+
return count;
|
|
2669
|
+
}
|
|
2384
2670
|
parseRootDirectives(node) {
|
|
2385
2671
|
let includeIf;
|
|
2386
2672
|
let omit = false;
|
|
2387
2673
|
for (const directive of node.directives ?? []) {
|
|
2388
2674
|
const name = directive.name.value;
|
|
2675
|
+
if (name === "assertAffectedRows") continue;
|
|
2389
2676
|
if (name !== "include" && name !== "skip") {
|
|
2390
2677
|
fail(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
|
|
2391
2678
|
}
|
|
@@ -2434,6 +2721,9 @@ var OperationLowerer = class {
|
|
|
2434
2721
|
const pk = this.lowerPkArgs(node, table);
|
|
2435
2722
|
return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
|
|
2436
2723
|
}
|
|
2724
|
+
if (rootInfo.kind === "aggregateGroups") {
|
|
2725
|
+
return this.lowerAggregateGroupsRoot(node, { alias, table });
|
|
2726
|
+
}
|
|
2437
2727
|
assertKnownArgs(node, ["where"]);
|
|
2438
2728
|
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
2439
2729
|
const whereArg = args.get("where");
|
|
@@ -2446,6 +2736,108 @@ var OperationLowerer = class {
|
|
|
2446
2736
|
...aggregate
|
|
2447
2737
|
};
|
|
2448
2738
|
}
|
|
2739
|
+
/** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
|
|
2740
|
+
lowerAggregateGroupsRoot(node, { alias, table }) {
|
|
2741
|
+
assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
|
|
2742
|
+
if (!node.selectionSet) fail(`'${node.name.value}' needs a selection set`, "invalid-selection");
|
|
2743
|
+
const args = argsByName(node);
|
|
2744
|
+
const groupByArg = args.get("groupBy");
|
|
2745
|
+
if (!groupByArg) fail(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
|
|
2746
|
+
const whereArg = args.get("where");
|
|
2747
|
+
const groupBy = this.lowerGroupBy(groupByArg.value, table);
|
|
2748
|
+
const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
|
|
2749
|
+
const offset = this.lowerOffset(args.get("offset")?.value);
|
|
2750
|
+
return {
|
|
2751
|
+
kind: "aggregateGroups",
|
|
2752
|
+
alias,
|
|
2753
|
+
table,
|
|
2754
|
+
where: whereArg ? this.lowerBoolExp(whereArg.value, table) : CONST_TRUE,
|
|
2755
|
+
groupBy,
|
|
2756
|
+
key: aggregate.key,
|
|
2757
|
+
...aggregate.count ? { count: aggregate.count } : {},
|
|
2758
|
+
...aggregate.sum ? { sum: aggregate.sum } : {},
|
|
2759
|
+
...aggregate.avg ? { avg: aggregate.avg } : {},
|
|
2760
|
+
limit: this.lowerGroupLimit(args.get("limit")?.value),
|
|
2761
|
+
...offset ? { offset } : {}
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
/**
|
|
2765
|
+
* `groupBy: [{ column: startedAt, bucket: MONTH }, …]` → GroupByKeyIr[].
|
|
2766
|
+
*
|
|
2767
|
+
* Document-authored only: a whole-argument variable is rejected rather than
|
|
2768
|
+
* becoming a §4f slot. Group keys choose which expressions reach GROUP BY,
|
|
2769
|
+
* and dynamic-input.ts refuses aggregation shapes from client input on the
|
|
2770
|
+
* same reasoning — `where` stays dynamic, which is what runtime filtering
|
|
2771
|
+
* actually needs.
|
|
2772
|
+
*/
|
|
2773
|
+
lowerGroupBy(node, table) {
|
|
2774
|
+
if (node.kind === graphql.Kind.VARIABLE) {
|
|
2775
|
+
fail(
|
|
2776
|
+
`groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
|
|
2777
|
+
"dynamic-group-by"
|
|
2778
|
+
);
|
|
2779
|
+
}
|
|
2780
|
+
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
2781
|
+
if (items.length === 0) fail(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
|
|
2782
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2783
|
+
return items.map((item) => {
|
|
2784
|
+
if (item.kind !== graphql.Kind.OBJECT) {
|
|
2785
|
+
fail(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
|
|
2786
|
+
}
|
|
2787
|
+
const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
|
|
2788
|
+
for (const key of fields.keys()) {
|
|
2789
|
+
if (key !== "column" && key !== "bucket") {
|
|
2790
|
+
fail(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
const columnNode = fields.get("column");
|
|
2794
|
+
if (!columnNode) fail(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
|
|
2795
|
+
if (columnNode.kind !== graphql.Kind.ENUM) {
|
|
2796
|
+
fail(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
|
|
2797
|
+
}
|
|
2798
|
+
const column = this.column(table, columnNode.value);
|
|
2799
|
+
if (!column) {
|
|
2800
|
+
fail(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
2801
|
+
}
|
|
2802
|
+
if (column.type === "jsonb") {
|
|
2803
|
+
fail(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
|
|
2804
|
+
}
|
|
2805
|
+
const bucketNode = fields.get("bucket");
|
|
2806
|
+
let bucket;
|
|
2807
|
+
if (bucketNode) {
|
|
2808
|
+
if (bucketNode.kind !== graphql.Kind.ENUM) {
|
|
2809
|
+
fail(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
2810
|
+
}
|
|
2811
|
+
bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
|
|
2812
|
+
if (!bucket) {
|
|
2813
|
+
fail(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
2814
|
+
}
|
|
2815
|
+
if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
|
|
2816
|
+
fail(
|
|
2817
|
+
`groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
|
|
2818
|
+
"invalid-group-by"
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
if (seen.has(columnNode.value)) {
|
|
2823
|
+
fail(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
|
|
2824
|
+
}
|
|
2825
|
+
seen.add(columnNode.value);
|
|
2826
|
+
return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
|
|
2827
|
+
});
|
|
2828
|
+
}
|
|
2829
|
+
/** Like lowerLimit, but bounded by MAX_GROUPS and defaulting to it. */
|
|
2830
|
+
lowerGroupLimit(node) {
|
|
2831
|
+
const base = { default: MAX_GROUPS, max: MAX_GROUPS };
|
|
2832
|
+
if (!node) return base;
|
|
2833
|
+
if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
2834
|
+
if (node.kind === graphql.Kind.INT) {
|
|
2835
|
+
const value = Number(node.value);
|
|
2836
|
+
if (value < 1) fail("limit must be a positive integer", "invalid-limit");
|
|
2837
|
+
return { ...base, value: Math.min(value, MAX_GROUPS) };
|
|
2838
|
+
}
|
|
2839
|
+
fail("limit must be an integer literal or a variable", "invalid-limit");
|
|
2840
|
+
}
|
|
2449
2841
|
/** byPk-style pk arguments: one argument per primary-key column, exactly. */
|
|
2450
2842
|
lowerPkArgs(node, table) {
|
|
2451
2843
|
const meta = this.meta(table);
|
|
@@ -2955,7 +3347,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
2955
3347
|
const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
|
|
2956
3348
|
const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
|
|
2957
3349
|
operations[restricted.name] = {
|
|
2958
|
-
irVersion: IR_VERSION,
|
|
3350
|
+
irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? IR_VERSION : LEGACY_IR_VERSION,
|
|
2959
3351
|
name: restricted.name,
|
|
2960
3352
|
opHash: computeOpHash({ definition: flattened, schemaHash }),
|
|
2961
3353
|
kind: restricted.kind,
|
|
@@ -2982,7 +3374,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
2982
3374
|
}
|
|
2983
3375
|
if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
|
|
2984
3376
|
const artifact = {
|
|
2985
|
-
irVersion:
|
|
3377
|
+
irVersion: requiredArtifactIrVersion(Object.values(operations)),
|
|
2986
3378
|
namespace: ctx.namespace,
|
|
2987
3379
|
schemaHash,
|
|
2988
3380
|
opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
|