@sequenceholdings/orm 2.0.0 → 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 +57 -2
- package/dist/authoring.cjs +122 -15
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +20 -3
- package/dist/authoring.d.ts +20 -3
- package/dist/authoring.js +122 -15
- 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 +721 -132
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +723 -134
- 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 +482 -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 +483 -97
- package/dist/compiler.js.map +1 -1
- package/dist/index.cjs +414 -70
- 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 +404 -72
- 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 +130 -11
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.js +130 -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 +3 -3
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
|
|
@@ -1183,21 +1311,21 @@ function andPred(...preds) {
|
|
|
1183
1311
|
if (operands.length === 1) return operands[0];
|
|
1184
1312
|
return { kind: "and", operands };
|
|
1185
1313
|
}
|
|
1186
|
-
function walkPred(pred,
|
|
1187
|
-
|
|
1314
|
+
function walkPred(pred, visit2) {
|
|
1315
|
+
visit2(pred);
|
|
1188
1316
|
switch (pred.kind) {
|
|
1189
1317
|
case "and":
|
|
1190
1318
|
case "or":
|
|
1191
|
-
for (const operand of pred.operands) walkPred(operand,
|
|
1319
|
+
for (const operand of pred.operands) walkPred(operand, visit2);
|
|
1192
1320
|
return;
|
|
1193
1321
|
case "not":
|
|
1194
|
-
walkPred(pred.operand,
|
|
1322
|
+
walkPred(pred.operand, visit2);
|
|
1195
1323
|
return;
|
|
1196
1324
|
case "exists":
|
|
1197
|
-
walkPred(pred.where,
|
|
1325
|
+
walkPred(pred.where, visit2);
|
|
1198
1326
|
return;
|
|
1199
1327
|
case "aggPredicate":
|
|
1200
|
-
if (pred.where) walkPred(pred.where,
|
|
1328
|
+
if (pred.where) walkPred(pred.where, visit2);
|
|
1201
1329
|
return;
|
|
1202
1330
|
default:
|
|
1203
1331
|
return;
|
|
@@ -1222,12 +1350,12 @@ function predContains(haystack, needle) {
|
|
|
1222
1350
|
// src/compiler-gql/inject.ts
|
|
1223
1351
|
function collectPinnedColumns(writePolicy) {
|
|
1224
1352
|
const key = (pin) => `${pin.column}\0${pin.claim}`;
|
|
1225
|
-
const
|
|
1353
|
+
const visit2 = (node) => {
|
|
1226
1354
|
if (node.kind === "and") {
|
|
1227
1355
|
const merged = [];
|
|
1228
1356
|
const seen2 = /* @__PURE__ */ new Set();
|
|
1229
1357
|
for (const operand of node.operands) {
|
|
1230
|
-
for (const pin of
|
|
1358
|
+
for (const pin of visit2(operand)) {
|
|
1231
1359
|
if (seen2.has(key(pin))) continue;
|
|
1232
1360
|
seen2.add(key(pin));
|
|
1233
1361
|
merged.push(pin);
|
|
@@ -1238,9 +1366,9 @@ function collectPinnedColumns(writePolicy) {
|
|
|
1238
1366
|
if (node.kind === "or") {
|
|
1239
1367
|
const [first, ...rest] = node.operands;
|
|
1240
1368
|
if (first === void 0) return [];
|
|
1241
|
-
let common =
|
|
1369
|
+
let common = visit2(first);
|
|
1242
1370
|
for (const operand of rest) {
|
|
1243
|
-
const branch = new Set(
|
|
1371
|
+
const branch = new Set(visit2(operand).map(key));
|
|
1244
1372
|
common = common.filter((pin) => branch.has(key(pin)));
|
|
1245
1373
|
if (common.length === 0) break;
|
|
1246
1374
|
}
|
|
@@ -1252,7 +1380,7 @@ function collectPinnedColumns(writePolicy) {
|
|
|
1252
1380
|
return [];
|
|
1253
1381
|
};
|
|
1254
1382
|
const seen = /* @__PURE__ */ new Set();
|
|
1255
|
-
return
|
|
1383
|
+
return visit2(writePolicy).filter((pin) => {
|
|
1256
1384
|
if (seen.has(pin.column)) return false;
|
|
1257
1385
|
seen.add(pin.column);
|
|
1258
1386
|
return true;
|
|
@@ -1346,6 +1474,11 @@ var PolicyInjector = class {
|
|
|
1346
1474
|
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table)),
|
|
1347
1475
|
...root.nodes ? { nodes: this.injectSelection(root.nodes, root.table) } : {}
|
|
1348
1476
|
};
|
|
1477
|
+
case "aggregateGroups":
|
|
1478
|
+
return {
|
|
1479
|
+
...root,
|
|
1480
|
+
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table))
|
|
1481
|
+
};
|
|
1349
1482
|
case "mutation":
|
|
1350
1483
|
return this.injectMutation(root);
|
|
1351
1484
|
}
|
|
@@ -1438,20 +1571,33 @@ function collectTableSet(roots, ctx) {
|
|
|
1438
1571
|
};
|
|
1439
1572
|
for (const root of roots) {
|
|
1440
1573
|
tables.add(root.table);
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
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
|
+
}
|
|
1455
1601
|
}
|
|
1456
1602
|
}
|
|
1457
1603
|
return [...tables].sort();
|
|
@@ -1550,7 +1696,12 @@ function tableNames(table) {
|
|
|
1550
1696
|
sumFields: `${type}SumFields`,
|
|
1551
1697
|
avgFields: `${type}AvgFields`,
|
|
1552
1698
|
aggregateBoolExp: `${type}AggregateBoolExp`,
|
|
1553
|
-
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`
|
|
1554
1705
|
};
|
|
1555
1706
|
}
|
|
1556
1707
|
|
|
@@ -1561,7 +1712,12 @@ function typeNameForTable(table) {
|
|
|
1561
1712
|
}
|
|
1562
1713
|
function queryRootNames(table) {
|
|
1563
1714
|
const names = tableNames(table);
|
|
1564
|
-
return {
|
|
1715
|
+
return {
|
|
1716
|
+
select: names.selectRoot,
|
|
1717
|
+
byPk: names.byPkRoot,
|
|
1718
|
+
aggregate: names.aggregateRoot,
|
|
1719
|
+
aggregateGroups: names.aggregateGroupsRoot
|
|
1720
|
+
};
|
|
1565
1721
|
}
|
|
1566
1722
|
function mutationRootNames(table) {
|
|
1567
1723
|
const names = tableNames(table);
|
|
@@ -1654,7 +1810,14 @@ function resolveColumnEntries(table, enums, diagnostics) {
|
|
|
1654
1810
|
type = { enum: enumDef.name, values: [...enumDef.values] };
|
|
1655
1811
|
}
|
|
1656
1812
|
}
|
|
1657
|
-
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
|
+
};
|
|
1658
1821
|
});
|
|
1659
1822
|
}
|
|
1660
1823
|
function logicalColumn(entries, ref) {
|
|
@@ -1703,6 +1866,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1703
1866
|
primaryKey.push(resolved);
|
|
1704
1867
|
}
|
|
1705
1868
|
const active = entries.filter((c) => !c.deprecated);
|
|
1869
|
+
const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
|
|
1706
1870
|
const constraints = [];
|
|
1707
1871
|
if (primaryKey.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey] });
|
|
1708
1872
|
for (const column of table.columns) {
|
|
@@ -1723,14 +1887,15 @@ function buildCompileContext(rawCanonical) {
|
|
|
1723
1887
|
// Synced twins (and views reading them, resolved transitively below)
|
|
1724
1888
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
1725
1889
|
claimsGated: relationIsClaimsGated(table.table),
|
|
1726
|
-
columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
|
|
1890
|
+
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
1727
1891
|
primaryKey: isView ? primaryKey : primaryKey.length > 0 ? primaryKey : ["id"],
|
|
1728
1892
|
links: [],
|
|
1729
1893
|
filterable: entries.map((c) => c.name),
|
|
1730
1894
|
orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
1731
1895
|
insertable: isView ? [] : active.map((c) => c.name),
|
|
1732
|
-
settable: isView ? [] : active.map((c) => c.name),
|
|
1896
|
+
settable: isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
1733
1897
|
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
1898
|
+
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
1734
1899
|
constraints: isView ? [] : constraints,
|
|
1735
1900
|
dynamicFilterMaxDepth: 6,
|
|
1736
1901
|
dynamicFilterMaxNodes: 64
|
|
@@ -1751,7 +1916,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1751
1916
|
});
|
|
1752
1917
|
continue;
|
|
1753
1918
|
}
|
|
1754
|
-
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);
|
|
1755
1920
|
const onEntries = link.kind === "toMany" ? targetEntries : entries;
|
|
1756
1921
|
const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
|
|
1757
1922
|
const on = logicalColumn(onEntries, link.on);
|
|
@@ -1848,6 +2013,9 @@ function buildCompileContext(rawCanonical) {
|
|
|
1848
2013
|
queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
|
|
1849
2014
|
}
|
|
1850
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
|
+
}
|
|
1851
2019
|
const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
1852
2020
|
if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
|
|
1853
2021
|
for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
|
|
@@ -1983,6 +2151,16 @@ function computeOpsHash(opHashes) {
|
|
|
1983
2151
|
}
|
|
1984
2152
|
var DEFAULT_LIMIT = 100;
|
|
1985
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"]);
|
|
1986
2164
|
var BOOLEXP_OPS = {
|
|
1987
2165
|
_eq: "eq",
|
|
1988
2166
|
_neq: "neq",
|
|
@@ -2014,6 +2192,14 @@ function fail(message, code, candidates = []) {
|
|
|
2014
2192
|
function argsByName(field) {
|
|
2015
2193
|
return new Map((field.arguments ?? []).map((arg) => [arg.name.value, arg]));
|
|
2016
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
|
+
}
|
|
2017
2203
|
function assertKnownArgs(field, allowed) {
|
|
2018
2204
|
for (const arg of field.arguments ?? []) {
|
|
2019
2205
|
if (!allowed.includes(arg.name.value)) {
|
|
@@ -2314,11 +2500,8 @@ var OperationLowerer = class {
|
|
|
2314
2500
|
lowerAggregateSelection(selectionSet, table) {
|
|
2315
2501
|
const result = {};
|
|
2316
2502
|
let sawAggregate = false;
|
|
2317
|
-
for (const node of selectionSet
|
|
2318
|
-
if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
2503
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
2319
2504
|
const name = node.name.value;
|
|
2320
|
-
if (name === "__typename") continue;
|
|
2321
|
-
if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
2322
2505
|
if (name === "nodes") {
|
|
2323
2506
|
if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
|
|
2324
2507
|
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
@@ -2328,48 +2511,116 @@ var OperationLowerer = class {
|
|
|
2328
2511
|
if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
2329
2512
|
sawAggregate = true;
|
|
2330
2513
|
if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
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;
|
|
2349
2599
|
}
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
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
|
+
);
|
|
2365
2616
|
}
|
|
2366
|
-
|
|
2367
|
-
continue;
|
|
2617
|
+
columns.push(columnField.name.value);
|
|
2368
2618
|
}
|
|
2369
|
-
|
|
2619
|
+
result[aggName] = columns;
|
|
2620
|
+
continue;
|
|
2370
2621
|
}
|
|
2622
|
+
fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
|
|
2371
2623
|
}
|
|
2372
|
-
if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
2373
2624
|
return result;
|
|
2374
2625
|
}
|
|
2375
2626
|
// -------------------------------------------------------------------------
|
|
@@ -2379,19 +2630,49 @@ var OperationLowerer = class {
|
|
|
2379
2630
|
const roots = [];
|
|
2380
2631
|
for (const node of operation.selectionSet.selections) {
|
|
2381
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);
|
|
2382
2641
|
const gate = this.parseRootDirectives(node);
|
|
2383
2642
|
if (gate.omit) continue;
|
|
2384
2643
|
const root = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
|
|
2385
|
-
|
|
2644
|
+
const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
|
|
2645
|
+
roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
|
|
2386
2646
|
}
|
|
2387
2647
|
if (roots.length === 0) fail("operation has no roots (all statically skipped?)", "invalid-root");
|
|
2388
2648
|
return roots;
|
|
2389
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
|
+
}
|
|
2390
2670
|
parseRootDirectives(node) {
|
|
2391
2671
|
let includeIf;
|
|
2392
2672
|
let omit = false;
|
|
2393
2673
|
for (const directive of node.directives ?? []) {
|
|
2394
2674
|
const name = directive.name.value;
|
|
2675
|
+
if (name === "assertAffectedRows") continue;
|
|
2395
2676
|
if (name !== "include" && name !== "skip") {
|
|
2396
2677
|
fail(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
|
|
2397
2678
|
}
|
|
@@ -2440,6 +2721,9 @@ var OperationLowerer = class {
|
|
|
2440
2721
|
const pk = this.lowerPkArgs(node, table);
|
|
2441
2722
|
return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
|
|
2442
2723
|
}
|
|
2724
|
+
if (rootInfo.kind === "aggregateGroups") {
|
|
2725
|
+
return this.lowerAggregateGroupsRoot(node, { alias, table });
|
|
2726
|
+
}
|
|
2443
2727
|
assertKnownArgs(node, ["where"]);
|
|
2444
2728
|
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
2445
2729
|
const whereArg = args.get("where");
|
|
@@ -2452,6 +2736,108 @@ var OperationLowerer = class {
|
|
|
2452
2736
|
...aggregate
|
|
2453
2737
|
};
|
|
2454
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
|
+
}
|
|
2455
2841
|
/** byPk-style pk arguments: one argument per primary-key column, exactly. */
|
|
2456
2842
|
lowerPkArgs(node, table) {
|
|
2457
2843
|
const meta = this.meta(table);
|
|
@@ -2961,7 +3347,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
2961
3347
|
const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
|
|
2962
3348
|
const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
|
|
2963
3349
|
operations[restricted.name] = {
|
|
2964
|
-
irVersion: IR_VERSION,
|
|
3350
|
+
irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? IR_VERSION : LEGACY_IR_VERSION,
|
|
2965
3351
|
name: restricted.name,
|
|
2966
3352
|
opHash: computeOpHash({ definition: flattened, schemaHash }),
|
|
2967
3353
|
kind: restricted.kind,
|
|
@@ -2988,7 +3374,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
2988
3374
|
}
|
|
2989
3375
|
if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
|
|
2990
3376
|
const artifact = {
|
|
2991
|
-
irVersion:
|
|
3377
|
+
irVersion: requiredArtifactIrVersion(Object.values(operations)),
|
|
2992
3378
|
namespace: ctx.namespace,
|
|
2993
3379
|
schemaHash,
|
|
2994
3380
|
opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
|