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