@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/cli.cjs
CHANGED
|
@@ -39,9 +39,34 @@ var PredicateParseError = class extends Error {
|
|
|
39
39
|
}
|
|
40
40
|
name = "PredicateParseError";
|
|
41
41
|
};
|
|
42
|
+
|
|
43
|
+
// src/constants.ts
|
|
44
|
+
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
45
|
+
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
46
|
+
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
47
|
+
var MAX_IDENTIFIER_LENGTH = 63;
|
|
48
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
|
|
49
|
+
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
50
|
+
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
51
|
+
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
52
|
+
var ORM_SCHEMA_PREFIX = "orm_";
|
|
53
|
+
var CONSUMER_ROLE_SUFFIX = "_consumer";
|
|
54
|
+
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
55
|
+
var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
|
|
56
|
+
var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
57
|
+
var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
|
|
58
|
+
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
59
|
+
var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
|
|
60
|
+
var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
|
|
61
|
+
var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
|
|
62
|
+
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
63
|
+
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
64
|
+
|
|
65
|
+
// src/canonical/v2.ts
|
|
42
66
|
var CANONICAL_FORMAT = 2;
|
|
43
67
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
44
68
|
var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
69
|
+
var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
45
70
|
var COLUMN_TYPES = [
|
|
46
71
|
"uuid",
|
|
47
72
|
"text",
|
|
@@ -62,18 +87,33 @@ var canonicalEnumSchema = zod.z.object({
|
|
|
62
87
|
var canonicalColumnSchema = zod.z.object({
|
|
63
88
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
64
89
|
name: zod.z.string().min(1).max(63),
|
|
65
|
-
columnName:
|
|
90
|
+
columnName: columnIdentifier,
|
|
66
91
|
type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
|
|
67
92
|
nullable: zod.z.boolean().default(true),
|
|
68
93
|
unique: zod.z.boolean().default(false),
|
|
69
94
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
70
95
|
default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
|
|
71
96
|
defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
|
|
97
|
+
/**
|
|
98
|
+
* Stamp this column from the database clock on UPDATE (`now()`). The
|
|
99
|
+
* column stays insertable (defaults fill gaps) but is omitted from
|
|
100
|
+
* SetInput / settable so callers cannot backdate it.
|
|
101
|
+
*/
|
|
102
|
+
onUpdateFn: zod.z.enum(["now()"]).optional(),
|
|
72
103
|
description: zod.z.string().max(500).optional(),
|
|
73
104
|
/** Migration hints — same semantics as v1. */
|
|
74
|
-
renamedFrom:
|
|
105
|
+
renamedFrom: columnIdentifier.optional(),
|
|
75
106
|
cast: zod.z.string().max(200).optional(),
|
|
76
107
|
deprecated: zod.z.boolean().default(false)
|
|
108
|
+
}).superRefine((column, ctx) => {
|
|
109
|
+
if (column.onUpdateFn === void 0) return;
|
|
110
|
+
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
111
|
+
ctx.addIssue({
|
|
112
|
+
code: "custom",
|
|
113
|
+
path: ["onUpdateFn"],
|
|
114
|
+
message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
|
|
115
|
+
});
|
|
116
|
+
}
|
|
77
117
|
});
|
|
78
118
|
var canonicalLinkSchema = zod.z.object({
|
|
79
119
|
name: zod.z.string().min(1).max(63),
|
|
@@ -81,15 +121,15 @@ var canonicalLinkSchema = zod.z.object({
|
|
|
81
121
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
82
122
|
target: identifier.optional(),
|
|
83
123
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
84
|
-
on:
|
|
124
|
+
on: columnIdentifier,
|
|
85
125
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
86
|
-
references:
|
|
126
|
+
references: columnIdentifier.optional(),
|
|
87
127
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
88
128
|
enforce: zod.z.boolean().default(false),
|
|
89
129
|
description: zod.z.string().max(500).optional()
|
|
90
130
|
});
|
|
91
131
|
var canonicalIndexSchema = zod.z.object({
|
|
92
|
-
columns: zod.z.array(
|
|
132
|
+
columns: zod.z.array(columnIdentifier).min(1),
|
|
93
133
|
unique: zod.z.boolean().default(false),
|
|
94
134
|
name: identifier.optional()
|
|
95
135
|
});
|
|
@@ -337,6 +377,13 @@ var canonicalCowMutationSchema = zod.z.object({
|
|
|
337
377
|
}
|
|
338
378
|
}
|
|
339
379
|
});
|
|
380
|
+
var canonicalForeignKeySchema = zod.z.object({
|
|
381
|
+
name: identifier,
|
|
382
|
+
columns: zod.z.array(columnIdentifier).min(1).max(8),
|
|
383
|
+
target: identifier,
|
|
384
|
+
references: zod.z.array(columnIdentifier).min(1).max(8),
|
|
385
|
+
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
|
|
386
|
+
}).strict();
|
|
340
387
|
var canonicalTableSchema = zod.z.object({
|
|
341
388
|
table: identifier,
|
|
342
389
|
description: zod.z.string().max(2e3).optional(),
|
|
@@ -345,7 +392,7 @@ var canonicalTableSchema = zod.z.object({
|
|
|
345
392
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
346
393
|
source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
347
394
|
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
348
|
-
primaryKey: zod.z.array(
|
|
395
|
+
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
349
396
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
350
397
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
351
398
|
policies: canonicalPoliciesSchema.default({}),
|
|
@@ -356,14 +403,16 @@ var canonicalTableSchema = zod.z.object({
|
|
|
356
403
|
*/
|
|
357
404
|
actions: zod.z.array(canonicalActionSchema).default([]),
|
|
358
405
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
359
|
-
cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
|
|
406
|
+
cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
|
|
407
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
408
|
+
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
|
|
360
409
|
});
|
|
361
410
|
var canonicalViewSchema = zod.z.object({
|
|
362
411
|
view: identifier,
|
|
363
412
|
description: zod.z.string().max(2e3).optional(),
|
|
364
413
|
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
365
414
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
366
|
-
primaryKey: zod.z.array(
|
|
415
|
+
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
367
416
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
368
417
|
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
369
418
|
query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
|
|
@@ -376,6 +425,9 @@ var canonicalViewSchema = zod.z.object({
|
|
|
376
425
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
377
426
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
|
|
378
427
|
}
|
|
428
|
+
if (column.onUpdateFn !== void 0) {
|
|
429
|
+
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare onUpdateFn` });
|
|
430
|
+
}
|
|
379
431
|
if (column.unique) {
|
|
380
432
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
|
|
381
433
|
}
|
|
@@ -409,6 +461,45 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
409
461
|
ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
|
|
410
462
|
}
|
|
411
463
|
seen.add(table.table);
|
|
464
|
+
if (table.source !== void 0) {
|
|
465
|
+
for (const [columnIndex, column] of table.columns.entries()) {
|
|
466
|
+
if (column.onUpdateFn !== void 0) {
|
|
467
|
+
ctx.addIssue({
|
|
468
|
+
code: "custom",
|
|
469
|
+
path: ["tables", index3, "columns", columnIndex],
|
|
470
|
+
message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
477
|
+
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
478
|
+
const names = /* @__PURE__ */ new Set([
|
|
479
|
+
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
480
|
+
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
481
|
+
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
482
|
+
]);
|
|
483
|
+
for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
|
|
484
|
+
const fail2 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
|
|
485
|
+
if (names.has(fk.name)) fail2(`duplicate foreign key '${fk.name}'`);
|
|
486
|
+
names.add(fk.name);
|
|
487
|
+
const target = fkTables.get(fk.target);
|
|
488
|
+
if (table.source || !target || target.source) {
|
|
489
|
+
fail2("foreign keys must connect owned tables in the same namespace");
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (fk.columns.length !== fk.references.length) fail2("foreign key columns and references must have the same number of columns");
|
|
493
|
+
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail2("foreign key columns must not repeat");
|
|
494
|
+
for (const [position, name] of fk.columns.entries()) {
|
|
495
|
+
const sourceColumn = table.columns.find((column) => column.columnName === name);
|
|
496
|
+
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
497
|
+
if (!sourceColumn || !targetColumn) fail2("foreign key references an unknown column");
|
|
498
|
+
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail2("foreign key column types must match");
|
|
499
|
+
}
|
|
500
|
+
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index4) => index4.unique).map((index4) => index4.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
501
|
+
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");
|
|
502
|
+
}
|
|
412
503
|
}
|
|
413
504
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
414
505
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
@@ -504,7 +595,7 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
504
595
|
const visiting = /* @__PURE__ */ new Set();
|
|
505
596
|
const visited = /* @__PURE__ */ new Set();
|
|
506
597
|
let cycleReported = false;
|
|
507
|
-
const
|
|
598
|
+
const visit2 = (name, path) => {
|
|
508
599
|
if (visited.has(name) || cycleReported) return;
|
|
509
600
|
if (visiting.has(name)) {
|
|
510
601
|
cycleReported = true;
|
|
@@ -518,12 +609,12 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
518
609
|
visiting.add(name);
|
|
519
610
|
const view = viewByName.get(name);
|
|
520
611
|
for (const dependency of view?.dependencies ?? []) {
|
|
521
|
-
if (viewByName.has(dependency))
|
|
612
|
+
if (viewByName.has(dependency)) visit2(dependency, [...path, name]);
|
|
522
613
|
}
|
|
523
614
|
visiting.delete(name);
|
|
524
615
|
visited.add(name);
|
|
525
616
|
};
|
|
526
|
-
for (const view of views)
|
|
617
|
+
for (const view of views) visit2(view.view, []);
|
|
527
618
|
});
|
|
528
619
|
function canonicalRelations(definition) {
|
|
529
620
|
const views = (definition.views ?? []).map((view) => ({
|
|
@@ -544,12 +635,13 @@ function normalizeCanonical(definition) {
|
|
|
544
635
|
...definition,
|
|
545
636
|
enums: sortByName(definition.enums),
|
|
546
637
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
|
|
547
|
-
const { cowMutations, ...rest } = t2;
|
|
638
|
+
const { cowMutations, foreignKeys, ...rest } = t2;
|
|
548
639
|
return {
|
|
549
640
|
...rest,
|
|
550
641
|
links: sortByName(t2.links),
|
|
551
642
|
indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
552
643
|
actions: sortByName(t2.actions),
|
|
644
|
+
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
553
645
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
554
646
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
555
647
|
};
|
|
@@ -564,25 +656,6 @@ function normalizeCanonical(definition) {
|
|
|
564
656
|
}
|
|
565
657
|
return normalized;
|
|
566
658
|
}
|
|
567
|
-
|
|
568
|
-
// src/constants.ts
|
|
569
|
-
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
570
|
-
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
571
|
-
var MAX_IDENTIFIER_LENGTH = 63;
|
|
572
|
-
var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
|
|
573
|
-
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
574
|
-
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
575
|
-
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
576
|
-
var ORM_SCHEMA_PREFIX = "orm_";
|
|
577
|
-
var CONSUMER_ROLE_SUFFIX = "_consumer";
|
|
578
|
-
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
579
|
-
var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
|
|
580
|
-
var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
581
|
-
var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
|
|
582
|
-
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
583
|
-
var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
|
|
584
|
-
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
585
|
-
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
586
659
|
function stableJson(value) {
|
|
587
660
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
588
661
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
@@ -1008,6 +1081,22 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1008
1081
|
} else if (columnLevelPk.length > 0) {
|
|
1009
1082
|
candidate.primaryKey = columnLevelPk;
|
|
1010
1083
|
}
|
|
1084
|
+
if (table.config.foreignKeys?.length) {
|
|
1085
|
+
if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
|
|
1086
|
+
candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
|
|
1087
|
+
const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
|
|
1088
|
+
throw new OrmDefinitionError(message, table.file);
|
|
1089
|
+
} });
|
|
1090
|
+
if (!isSeqTable(target) || !relationsByName.has(target.tableName)) throw new OrmDefinitionError("foreign key target must be a table in this namespace", table.file);
|
|
1091
|
+
return {
|
|
1092
|
+
name: fk.name,
|
|
1093
|
+
columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
|
|
1094
|
+
target: target.tableName,
|
|
1095
|
+
references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
|
|
1096
|
+
...fk.deferrable ? { deferrable: fk.deferrable } : {}
|
|
1097
|
+
};
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1011
1100
|
for (const index3 of table.config.indexes ?? []) {
|
|
1012
1101
|
const entry = {
|
|
1013
1102
|
columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
|
|
@@ -1060,6 +1149,12 @@ function buildCanonicalView({
|
|
|
1060
1149
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1061
1150
|
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
|
|
1062
1151
|
}
|
|
1152
|
+
if (column.onUpdateFn !== void 0) {
|
|
1153
|
+
throw new OrmDefinitionError(
|
|
1154
|
+
`view "${view.viewName}": column "${propertyName}" cannot declare onUpdateFn`,
|
|
1155
|
+
view.file
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1063
1158
|
if (column.unique) {
|
|
1064
1159
|
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
|
|
1065
1160
|
}
|
|
@@ -1222,7 +1317,16 @@ async function loadNamespaceFromDir(dir, options = {}) {
|
|
|
1222
1317
|
const jiti$1 = jiti.createJiti((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)), {
|
|
1223
1318
|
interopDefault: true,
|
|
1224
1319
|
fsCache: false,
|
|
1225
|
-
...options.ormModulePath !== void 0 ? {
|
|
1320
|
+
...options.ormModulePath !== void 0 ? {
|
|
1321
|
+
alias: {
|
|
1322
|
+
// The public /drizzle barrel re-exports this pinned dependency.
|
|
1323
|
+
// Explicitly map it before the root alias: otherwise jiti appends
|
|
1324
|
+
// "/drizzle" to the overridden module's filename.
|
|
1325
|
+
"@sequenceholdings/orm/drizzle": module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))).resolve("drizzle-orm"),
|
|
1326
|
+
"@sequenceholdings/orm": options.ormModulePath,
|
|
1327
|
+
"@sequenceholdings/orm/authoring": options.ormModulePath
|
|
1328
|
+
}
|
|
1329
|
+
} : {}
|
|
1226
1330
|
});
|
|
1227
1331
|
const context = beginAuthoringContext();
|
|
1228
1332
|
try {
|
|
@@ -1313,7 +1417,7 @@ var manifestOperationSchema = zod.z.object({
|
|
|
1313
1417
|
kind: zod.z.enum(["query", "mutation"]),
|
|
1314
1418
|
requires: manifestRequiresSchema
|
|
1315
1419
|
});
|
|
1316
|
-
zod.z.object({
|
|
1420
|
+
var operationsManifestSchema = zod.z.object({
|
|
1317
1421
|
manifestVersion: zod.z.literal(MANIFEST_VERSION),
|
|
1318
1422
|
schemaHash: zod.z.string().min(1),
|
|
1319
1423
|
opsHash: zod.z.string().regex(OP_HASH_RE),
|
|
@@ -1432,6 +1536,7 @@ var ORDER_BY_ENUM_VALUES = [
|
|
|
1432
1536
|
"DESC_NULLS_LAST"
|
|
1433
1537
|
];
|
|
1434
1538
|
var PLATFORM_USER_TYPENAME = "PlatformUser";
|
|
1539
|
+
var DATE_BUCKET_ENUM = "DateBucket";
|
|
1435
1540
|
function tableNames(table) {
|
|
1436
1541
|
const root = camelCase(table);
|
|
1437
1542
|
const rootPascal = pascalCase(table);
|
|
@@ -1464,7 +1569,12 @@ function tableNames(table) {
|
|
|
1464
1569
|
sumFields: `${type}SumFields`,
|
|
1465
1570
|
avgFields: `${type}AvgFields`,
|
|
1466
1571
|
aggregateBoolExp: `${type}AggregateBoolExp`,
|
|
1467
|
-
aggregateBoolExpCount: `${type}AggregateBoolExpCount
|
|
1572
|
+
aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
|
|
1573
|
+
aggregateGroupsRoot: `${root}AggregateGroups`,
|
|
1574
|
+
groupByColumnEnum: `${type}GroupByColumn`,
|
|
1575
|
+
groupByInput: `${type}GroupBy`,
|
|
1576
|
+
groupKey: `${type}GroupKey`,
|
|
1577
|
+
aggregateGroup: `${type}AggregateGroup`
|
|
1468
1578
|
};
|
|
1469
1579
|
}
|
|
1470
1580
|
|
|
@@ -1497,7 +1607,18 @@ ${body}
|
|
|
1497
1607
|
}
|
|
1498
1608
|
`;
|
|
1499
1609
|
}
|
|
1500
|
-
var IR_VERSION =
|
|
1610
|
+
var IR_VERSION = 2;
|
|
1611
|
+
var LEGACY_IR_VERSION = 1;
|
|
1612
|
+
var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
|
|
1613
|
+
var operationVersionSchema = zod.z.object({ irVersion: irVersionSchema });
|
|
1614
|
+
function requiredArtifactIrVersion(operations) {
|
|
1615
|
+
let required = LEGACY_IR_VERSION;
|
|
1616
|
+
for (const operation of operations) {
|
|
1617
|
+
const { irVersion } = operationVersionSchema.parse(operation);
|
|
1618
|
+
if (irVersion > required) required = irVersion;
|
|
1619
|
+
}
|
|
1620
|
+
return required;
|
|
1621
|
+
}
|
|
1501
1622
|
var CLAIMS_CONTRACT = {
|
|
1502
1623
|
sub: "string",
|
|
1503
1624
|
org: "string?",
|
|
@@ -1624,6 +1745,33 @@ var aggregateRootSchema = zod.z.object({
|
|
|
1624
1745
|
nodes: selectionIrSchema.optional(),
|
|
1625
1746
|
includeIf: includeIfSchema.optional()
|
|
1626
1747
|
});
|
|
1748
|
+
var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
|
|
1749
|
+
var groupByKeyIrSchema = zod.z.object({
|
|
1750
|
+
column: zod.z.string(),
|
|
1751
|
+
bucket: zod.z.enum(DATE_BUCKETS).optional()
|
|
1752
|
+
});
|
|
1753
|
+
var aggregateGroupsRootSchema = zod.z.object({
|
|
1754
|
+
kind: zod.z.literal("aggregateGroups"),
|
|
1755
|
+
alias: zod.z.string(),
|
|
1756
|
+
table: zod.z.string(),
|
|
1757
|
+
/** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
|
|
1758
|
+
where: predIrSchema,
|
|
1759
|
+
groupBy: zod.z.array(groupByKeyIrSchema).min(1),
|
|
1760
|
+
/**
|
|
1761
|
+
* The `key { … }` projection: which group columns appear in each entry's key
|
|
1762
|
+
* object, in selection order. A subset of `groupBy` (the compiler rejects a
|
|
1763
|
+
* key field that is not a group key — it would be indeterminate within the
|
|
1764
|
+
* group). Empty when the document selects only `aggregate`.
|
|
1765
|
+
*/
|
|
1766
|
+
key: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })).default([]),
|
|
1767
|
+
count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
|
|
1768
|
+
sum: zod.z.array(zod.z.string()).optional(),
|
|
1769
|
+
avg: zod.z.array(zod.z.string()).optional(),
|
|
1770
|
+
/** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
|
|
1771
|
+
limit: limitIrSchema,
|
|
1772
|
+
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
|
|
1773
|
+
includeIf: includeIfSchema.optional()
|
|
1774
|
+
});
|
|
1627
1775
|
var writeValuesSchema = zod.z.union([
|
|
1628
1776
|
zod.z.object({ slot: zod.z.string() }),
|
|
1629
1777
|
zod.z.object({ entries: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })) })
|
|
@@ -1669,12 +1817,15 @@ var mutationRootSchema = zod.z.object({
|
|
|
1669
1817
|
insertCheck: predIrSchema.optional(),
|
|
1670
1818
|
returning: selectionIrSchema.nullable(),
|
|
1671
1819
|
affectedRows: zod.z.boolean().default(false),
|
|
1820
|
+
/** v2: abort the transaction when the actual write count differs, regardless of selection. */
|
|
1821
|
+
assertAffectedRows: zod.z.number().int().min(0).max(2147483647).optional(),
|
|
1672
1822
|
includeIf: includeIfSchema.optional()
|
|
1673
1823
|
});
|
|
1674
1824
|
var rootIrSchema = zod.z.discriminatedUnion("kind", [
|
|
1675
1825
|
selectRootSchema,
|
|
1676
1826
|
byPkRootSchema,
|
|
1677
1827
|
aggregateRootSchema,
|
|
1828
|
+
aggregateGroupsRootSchema,
|
|
1678
1829
|
mutationRootSchema
|
|
1679
1830
|
]);
|
|
1680
1831
|
var INPUT_REF_KINDS = [
|
|
@@ -1704,7 +1855,7 @@ var variableIrSchema = zod.z.object({
|
|
|
1704
1855
|
defaultValue: variableDefaultIrSchema.optional()
|
|
1705
1856
|
});
|
|
1706
1857
|
var compiledOperationSchema = zod.z.object({
|
|
1707
|
-
irVersion:
|
|
1858
|
+
irVersion: irVersionSchema,
|
|
1708
1859
|
name: zod.z.string(),
|
|
1709
1860
|
opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
1710
1861
|
kind: zod.z.enum(["query", "mutation"]),
|
|
@@ -1716,6 +1867,13 @@ var compiledOperationSchema = zod.z.object({
|
|
|
1716
1867
|
/** Every table the op touches — data-token scoping + audit. */
|
|
1717
1868
|
tableSet: zod.z.array(zod.z.string()).min(1),
|
|
1718
1869
|
roots: zod.z.array(rootIrSchema).min(1)
|
|
1870
|
+
}).superRefine((operation, ctx) => {
|
|
1871
|
+
for (const [index3, root] of operation.roots.entries()) {
|
|
1872
|
+
if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
|
|
1873
|
+
if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
|
|
1874
|
+
ctx.addIssue({ code: "custom", path: ["roots", index3], message: "affected-row assertions require unconditional mutation roots and IR v2" });
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1719
1877
|
});
|
|
1720
1878
|
var tableMetaIrSchema = zod.z.object({
|
|
1721
1879
|
table: zod.z.string(),
|
|
@@ -1752,18 +1910,29 @@ var tableMetaIrSchema = zod.z.object({
|
|
|
1752
1910
|
insertable: zod.z.array(zod.z.string()),
|
|
1753
1911
|
settable: zod.z.array(zod.z.string()),
|
|
1754
1912
|
appendable: zod.z.array(zod.z.string()),
|
|
1913
|
+
/**
|
|
1914
|
+
* Logical columns stamped with Postgres `now()` on UPDATE (canonical
|
|
1915
|
+
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
1916
|
+
*/
|
|
1917
|
+
touchOnUpdate: zod.z.array(zod.z.string()).optional(),
|
|
1755
1918
|
constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
|
|
1756
1919
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
1757
1920
|
dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
|
|
1758
1921
|
dynamicFilterMaxNodes: zod.z.number().int().positive().default(64)
|
|
1759
1922
|
});
|
|
1760
1923
|
var compiledArtifactSchema = zod.z.object({
|
|
1761
|
-
irVersion:
|
|
1924
|
+
irVersion: irVersionSchema,
|
|
1762
1925
|
namespace: zod.z.string(),
|
|
1763
1926
|
schemaHash: zod.z.string(),
|
|
1764
1927
|
opsHash: zod.z.string(),
|
|
1765
1928
|
tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
|
|
1766
1929
|
operations: zod.z.record(zod.z.string(), compiledOperationSchema)
|
|
1930
|
+
}).superRefine((artifact, ctx) => {
|
|
1931
|
+
for (const [name, operation] of Object.entries(artifact.operations)) {
|
|
1932
|
+
if (operation.irVersion > artifact.irVersion) {
|
|
1933
|
+
ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1767
1936
|
});
|
|
1768
1937
|
var IrVersionError = class extends Error {
|
|
1769
1938
|
constructor(found) {
|
|
@@ -1774,19 +1943,19 @@ var IrVersionError = class extends Error {
|
|
|
1774
1943
|
function normalizeIr(raw) {
|
|
1775
1944
|
if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
|
|
1776
1945
|
const version = raw.irVersion;
|
|
1777
|
-
if (version !== IR_VERSION) throw new IrVersionError(version);
|
|
1946
|
+
if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
|
|
1778
1947
|
return compiledArtifactSchema.parse(raw);
|
|
1779
1948
|
}
|
|
1780
1949
|
|
|
1781
1950
|
// src/predicate-v2/ast.ts
|
|
1782
1951
|
var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
1783
|
-
function walkPolicy(node,
|
|
1784
|
-
|
|
1952
|
+
function walkPolicy(node, visit2) {
|
|
1953
|
+
visit2(node);
|
|
1785
1954
|
if (node.kind === "and" || node.kind === "or") {
|
|
1786
|
-
for (const operand of node.operands) walkPolicy(operand,
|
|
1955
|
+
for (const operand of node.operands) walkPolicy(operand, visit2);
|
|
1787
1956
|
}
|
|
1788
|
-
if (node.kind === "not") walkPolicy(node.operand,
|
|
1789
|
-
if (node.kind === "exists") walkPolicy(node.where,
|
|
1957
|
+
if (node.kind === "not") walkPolicy(node.operand, visit2);
|
|
1958
|
+
if (node.kind === "exists") walkPolicy(node.where, visit2);
|
|
1790
1959
|
}
|
|
1791
1960
|
|
|
1792
1961
|
// src/predicate-v2/errors.ts
|
|
@@ -1924,6 +2093,10 @@ var Parser = class {
|
|
|
1924
2093
|
this.expect("rparen", "')'");
|
|
1925
2094
|
return inner;
|
|
1926
2095
|
}
|
|
2096
|
+
if (this.peekKeyword("true") || this.peekKeyword("false")) {
|
|
2097
|
+
const token = this.advance();
|
|
2098
|
+
return { kind: "literal", value: token.value === "true" };
|
|
2099
|
+
}
|
|
1927
2100
|
if (this.peekKeyword("capability") || this.peekKeyword("role")) {
|
|
1928
2101
|
const fn = this.peek().value;
|
|
1929
2102
|
const node = this.parseBooleanCall(fn);
|
|
@@ -2107,6 +2280,8 @@ var PolicyCompiler = class {
|
|
|
2107
2280
|
}
|
|
2108
2281
|
compile(node, table, depth) {
|
|
2109
2282
|
switch (node.kind) {
|
|
2283
|
+
case "literal":
|
|
2284
|
+
return { kind: "const", value: node.value };
|
|
2110
2285
|
case "and":
|
|
2111
2286
|
case "or":
|
|
2112
2287
|
return { kind: node.kind, operands: node.operands.map((operand) => this.compile(operand, table, depth)) };
|
|
@@ -2286,21 +2461,21 @@ function andPred(...preds) {
|
|
|
2286
2461
|
if (operands.length === 1) return operands[0];
|
|
2287
2462
|
return { kind: "and", operands };
|
|
2288
2463
|
}
|
|
2289
|
-
function walkPred(pred,
|
|
2290
|
-
|
|
2464
|
+
function walkPred(pred, visit2) {
|
|
2465
|
+
visit2(pred);
|
|
2291
2466
|
switch (pred.kind) {
|
|
2292
2467
|
case "and":
|
|
2293
2468
|
case "or":
|
|
2294
|
-
for (const operand of pred.operands) walkPred(operand,
|
|
2469
|
+
for (const operand of pred.operands) walkPred(operand, visit2);
|
|
2295
2470
|
return;
|
|
2296
2471
|
case "not":
|
|
2297
|
-
walkPred(pred.operand,
|
|
2472
|
+
walkPred(pred.operand, visit2);
|
|
2298
2473
|
return;
|
|
2299
2474
|
case "exists":
|
|
2300
|
-
walkPred(pred.where,
|
|
2475
|
+
walkPred(pred.where, visit2);
|
|
2301
2476
|
return;
|
|
2302
2477
|
case "aggPredicate":
|
|
2303
|
-
if (pred.where) walkPred(pred.where,
|
|
2478
|
+
if (pred.where) walkPred(pred.where, visit2);
|
|
2304
2479
|
return;
|
|
2305
2480
|
default:
|
|
2306
2481
|
return;
|
|
@@ -2317,12 +2492,12 @@ function predContainsExists(pred) {
|
|
|
2317
2492
|
// src/compiler-gql/inject.ts
|
|
2318
2493
|
function collectPinnedColumns(writePolicy) {
|
|
2319
2494
|
const key = (pin) => `${pin.column}\0${pin.claim}`;
|
|
2320
|
-
const
|
|
2495
|
+
const visit2 = (node) => {
|
|
2321
2496
|
if (node.kind === "and") {
|
|
2322
2497
|
const merged = [];
|
|
2323
2498
|
const seen2 = /* @__PURE__ */ new Set();
|
|
2324
2499
|
for (const operand of node.operands) {
|
|
2325
|
-
for (const pin of
|
|
2500
|
+
for (const pin of visit2(operand)) {
|
|
2326
2501
|
if (seen2.has(key(pin))) continue;
|
|
2327
2502
|
seen2.add(key(pin));
|
|
2328
2503
|
merged.push(pin);
|
|
@@ -2333,9 +2508,9 @@ function collectPinnedColumns(writePolicy) {
|
|
|
2333
2508
|
if (node.kind === "or") {
|
|
2334
2509
|
const [first, ...rest] = node.operands;
|
|
2335
2510
|
if (first === void 0) return [];
|
|
2336
|
-
let common =
|
|
2511
|
+
let common = visit2(first);
|
|
2337
2512
|
for (const operand of rest) {
|
|
2338
|
-
const branch = new Set(
|
|
2513
|
+
const branch = new Set(visit2(operand).map(key));
|
|
2339
2514
|
common = common.filter((pin) => branch.has(key(pin)));
|
|
2340
2515
|
if (common.length === 0) break;
|
|
2341
2516
|
}
|
|
@@ -2347,7 +2522,7 @@ function collectPinnedColumns(writePolicy) {
|
|
|
2347
2522
|
return [];
|
|
2348
2523
|
};
|
|
2349
2524
|
const seen = /* @__PURE__ */ new Set();
|
|
2350
|
-
return
|
|
2525
|
+
return visit2(writePolicy).filter((pin) => {
|
|
2351
2526
|
if (seen.has(pin.column)) return false;
|
|
2352
2527
|
seen.add(pin.column);
|
|
2353
2528
|
return true;
|
|
@@ -2441,6 +2616,11 @@ var PolicyInjector = class {
|
|
|
2441
2616
|
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table)),
|
|
2442
2617
|
...root.nodes ? { nodes: this.injectSelection(root.nodes, root.table) } : {}
|
|
2443
2618
|
};
|
|
2619
|
+
case "aggregateGroups":
|
|
2620
|
+
return {
|
|
2621
|
+
...root,
|
|
2622
|
+
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table))
|
|
2623
|
+
};
|
|
2444
2624
|
case "mutation":
|
|
2445
2625
|
return this.injectMutation(root);
|
|
2446
2626
|
}
|
|
@@ -2533,20 +2713,33 @@ function collectTableSet(roots, ctx) {
|
|
|
2533
2713
|
};
|
|
2534
2714
|
for (const root of roots) {
|
|
2535
2715
|
tables.add(root.table);
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2716
|
+
switch (root.kind) {
|
|
2717
|
+
case "select":
|
|
2718
|
+
case "byPk":
|
|
2719
|
+
visitPred(root.where, root.table);
|
|
2720
|
+
visitSelection(root.selection, root.table);
|
|
2721
|
+
break;
|
|
2722
|
+
case "aggregate":
|
|
2723
|
+
visitPred(root.where, root.table);
|
|
2724
|
+
if (root.nodes) visitSelection(root.nodes, root.table);
|
|
2725
|
+
break;
|
|
2726
|
+
case "aggregateGroups":
|
|
2727
|
+
visitPred(root.where, root.table);
|
|
2728
|
+
break;
|
|
2729
|
+
case "mutation":
|
|
2730
|
+
if (root.cowSynced !== void 0) tables.add(root.cowSynced);
|
|
2731
|
+
if (root.where) visitPred(root.where, root.table);
|
|
2732
|
+
if (root.insertCheck) visitPred(root.insertCheck, root.table);
|
|
2733
|
+
if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
|
|
2734
|
+
if (root.returning) visitSelection(root.returning, root.table);
|
|
2735
|
+
break;
|
|
2736
|
+
default: {
|
|
2737
|
+
const exhausted = root;
|
|
2738
|
+
throw new DocumentCompileError(
|
|
2739
|
+
`internal: unhandled root kind in tableSet collection ${JSON.stringify(exhausted)}`,
|
|
2740
|
+
"internal"
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2550
2743
|
}
|
|
2551
2744
|
}
|
|
2552
2745
|
return [...tables].sort();
|
|
@@ -2564,7 +2757,12 @@ function typeNameForTable(table) {
|
|
|
2564
2757
|
}
|
|
2565
2758
|
function queryRootNames(table) {
|
|
2566
2759
|
const names = tableNames(table);
|
|
2567
|
-
return {
|
|
2760
|
+
return {
|
|
2761
|
+
select: names.selectRoot,
|
|
2762
|
+
byPk: names.byPkRoot,
|
|
2763
|
+
aggregate: names.aggregateRoot,
|
|
2764
|
+
aggregateGroups: names.aggregateGroupsRoot
|
|
2765
|
+
};
|
|
2568
2766
|
}
|
|
2569
2767
|
function mutationRootNames(table) {
|
|
2570
2768
|
const names = tableNames(table);
|
|
@@ -2645,7 +2843,14 @@ function resolveColumnEntries(table, enums, diagnostics) {
|
|
|
2645
2843
|
type = { enum: enumDef.name, values: [...enumDef.values] };
|
|
2646
2844
|
}
|
|
2647
2845
|
}
|
|
2648
|
-
return {
|
|
2846
|
+
return {
|
|
2847
|
+
name: column.name,
|
|
2848
|
+
columnName: column.columnName,
|
|
2849
|
+
type,
|
|
2850
|
+
nullable: column.nullable,
|
|
2851
|
+
deprecated: column.deprecated,
|
|
2852
|
+
onUpdateFn: column.onUpdateFn !== void 0
|
|
2853
|
+
};
|
|
2649
2854
|
});
|
|
2650
2855
|
}
|
|
2651
2856
|
function logicalColumn(entries, ref) {
|
|
@@ -2694,6 +2899,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2694
2899
|
primaryKey2.push(resolved);
|
|
2695
2900
|
}
|
|
2696
2901
|
const active = entries.filter((c) => !c.deprecated);
|
|
2902
|
+
const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
|
|
2697
2903
|
const constraints = [];
|
|
2698
2904
|
if (primaryKey2.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey2] });
|
|
2699
2905
|
for (const column of table.columns) {
|
|
@@ -2714,14 +2920,15 @@ function buildCompileContext(rawCanonical) {
|
|
|
2714
2920
|
// Synced twins (and views reading them, resolved transitively below)
|
|
2715
2921
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
2716
2922
|
claimsGated: relationIsClaimsGated(table.table),
|
|
2717
|
-
columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
|
|
2923
|
+
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
2718
2924
|
primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
2719
2925
|
links: [],
|
|
2720
2926
|
filterable: entries.map((c) => c.name),
|
|
2721
2927
|
orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
2722
2928
|
insertable: isView ? [] : active.map((c) => c.name),
|
|
2723
|
-
settable: isView ? [] : active.map((c) => c.name),
|
|
2929
|
+
settable: isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
2724
2930
|
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
2931
|
+
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
2725
2932
|
constraints: isView ? [] : constraints,
|
|
2726
2933
|
dynamicFilterMaxDepth: 6,
|
|
2727
2934
|
dynamicFilterMaxNodes: 64
|
|
@@ -2742,7 +2949,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2742
2949
|
});
|
|
2743
2950
|
continue;
|
|
2744
2951
|
}
|
|
2745
|
-
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false })) : columnEntries.get(targetName);
|
|
2952
|
+
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
|
|
2746
2953
|
const onEntries = link.kind === "toMany" ? targetEntries : entries;
|
|
2747
2954
|
const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
|
|
2748
2955
|
const on = logicalColumn(onEntries, link.on);
|
|
@@ -2839,6 +3046,9 @@ function buildCompileContext(rawCanonical) {
|
|
|
2839
3046
|
queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
|
|
2840
3047
|
}
|
|
2841
3048
|
queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
|
|
3049
|
+
if (table.columns.some((column) => column.type !== "jsonb")) {
|
|
3050
|
+
queryRoots.set(roots.aggregateGroups, { kind: "aggregateGroups", table: table.table });
|
|
3051
|
+
}
|
|
2842
3052
|
const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
2843
3053
|
if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
|
|
2844
3054
|
for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
|
|
@@ -2974,6 +3184,16 @@ function computeOpsHash(opHashes) {
|
|
|
2974
3184
|
}
|
|
2975
3185
|
var DEFAULT_LIMIT = 100;
|
|
2976
3186
|
var MAX_LIMIT = 1e3;
|
|
3187
|
+
var MAX_GROUPS = 1e3;
|
|
3188
|
+
var DATE_BUCKET_BY_ENUM = {
|
|
3189
|
+
YEAR: "year",
|
|
3190
|
+
QUARTER: "quarter",
|
|
3191
|
+
MONTH: "month",
|
|
3192
|
+
WEEK: "week",
|
|
3193
|
+
DAY: "day",
|
|
3194
|
+
HOUR: "hour"
|
|
3195
|
+
};
|
|
3196
|
+
var BUCKETABLE_TYPES = /* @__PURE__ */ new Set(["timestamptz", "date"]);
|
|
2977
3197
|
var BOOLEXP_OPS = {
|
|
2978
3198
|
_eq: "eq",
|
|
2979
3199
|
_neq: "neq",
|
|
@@ -3005,6 +3225,14 @@ function fail(message, code, candidates = []) {
|
|
|
3005
3225
|
function argsByName(field) {
|
|
3006
3226
|
return new Map((field.arguments ?? []).map((arg) => [arg.name.value, arg]));
|
|
3007
3227
|
}
|
|
3228
|
+
function* aggregateBlockFields(selectionSet) {
|
|
3229
|
+
for (const node of selectionSet.selections) {
|
|
3230
|
+
if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
3231
|
+
if (node.name.value === "__typename") continue;
|
|
3232
|
+
if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
3233
|
+
yield node;
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3008
3236
|
function assertKnownArgs(field, allowed) {
|
|
3009
3237
|
for (const arg of field.arguments ?? []) {
|
|
3010
3238
|
if (!allowed.includes(arg.name.value)) {
|
|
@@ -3305,11 +3533,8 @@ var OperationLowerer = class {
|
|
|
3305
3533
|
lowerAggregateSelection(selectionSet, table) {
|
|
3306
3534
|
const result = {};
|
|
3307
3535
|
let sawAggregate = false;
|
|
3308
|
-
for (const node of selectionSet
|
|
3309
|
-
if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
3536
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
3310
3537
|
const name = node.name.value;
|
|
3311
|
-
if (name === "__typename") continue;
|
|
3312
|
-
if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
3313
3538
|
if (name === "nodes") {
|
|
3314
3539
|
if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
|
|
3315
3540
|
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
@@ -3319,48 +3544,116 @@ var OperationLowerer = class {
|
|
|
3319
3544
|
if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
3320
3545
|
sawAggregate = true;
|
|
3321
3546
|
if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3547
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
3548
|
+
}
|
|
3549
|
+
if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
3550
|
+
return result;
|
|
3551
|
+
}
|
|
3552
|
+
/**
|
|
3553
|
+
* The `{ key {…} aggregate {…} }` block of a grouped aggregate root. Same
|
|
3554
|
+
* `aggregate` sub-block as the ungrouped shape (shared via
|
|
3555
|
+
* `lowerAggregateFields`), but `key` replaces `nodes` — see the IR comment on
|
|
3556
|
+
* `aggregateGroupsRootSchema` for why grouped roots have no `nodes`.
|
|
3557
|
+
*/
|
|
3558
|
+
lowerGroupedAggregateSelection(selectionSet, table, groupBy) {
|
|
3559
|
+
const groupColumns = new Set(groupBy.map((entry) => entry.column));
|
|
3560
|
+
const result = { key: [] };
|
|
3561
|
+
let sawAggregate = false;
|
|
3562
|
+
let sawKey = false;
|
|
3563
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
3564
|
+
const name = node.name.value;
|
|
3565
|
+
if (name === "key") {
|
|
3566
|
+
if (sawKey) fail("one key block per grouped aggregate", "invalid-aggregate");
|
|
3567
|
+
sawKey = true;
|
|
3568
|
+
if (!node.selectionSet) fail("'key' needs a selection set", "invalid-aggregate");
|
|
3569
|
+
result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
|
|
3570
|
+
continue;
|
|
3571
|
+
}
|
|
3572
|
+
if (name === "nodes") {
|
|
3573
|
+
fail("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
|
|
3574
|
+
}
|
|
3575
|
+
if (name !== "aggregate") fail(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
|
|
3576
|
+
if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
3577
|
+
sawAggregate = true;
|
|
3578
|
+
if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
|
|
3579
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
3580
|
+
}
|
|
3581
|
+
const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
|
|
3582
|
+
if (result.key.length === 0 && !rendersAggregate) {
|
|
3583
|
+
fail(
|
|
3584
|
+
"grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
|
|
3585
|
+
"invalid-aggregate",
|
|
3586
|
+
["key", "aggregate"]
|
|
3587
|
+
);
|
|
3588
|
+
}
|
|
3589
|
+
return result;
|
|
3590
|
+
}
|
|
3591
|
+
/**
|
|
3592
|
+
* `key { month officer }` → the projected group columns. Every field must be
|
|
3593
|
+
* one of the query's own group keys: any other column is indeterminate
|
|
3594
|
+
* within a group, so selecting it is a document error rather than an
|
|
3595
|
+
* arbitrary pick.
|
|
3596
|
+
*/
|
|
3597
|
+
lowerGroupKeySelection(selectionSet, table, groupColumns) {
|
|
3598
|
+
const key = [];
|
|
3599
|
+
for (const node of selectionSet.selections) {
|
|
3600
|
+
if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
3601
|
+
const name = node.name.value;
|
|
3602
|
+
if (name === "__typename") continue;
|
|
3603
|
+
if (!this.column(table, name)) {
|
|
3604
|
+
fail(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
|
|
3605
|
+
}
|
|
3606
|
+
if (!groupColumns.has(name)) {
|
|
3607
|
+
fail(
|
|
3608
|
+
`key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
|
|
3609
|
+
"invalid-group-key",
|
|
3610
|
+
[...groupColumns]
|
|
3611
|
+
);
|
|
3612
|
+
}
|
|
3613
|
+
key.push({ alias: node.alias?.value ?? name, column: name });
|
|
3614
|
+
}
|
|
3615
|
+
return key;
|
|
3616
|
+
}
|
|
3617
|
+
/** The `count` / `sum {…}` / `avg {…}` fields inside an `aggregate { … }` block. */
|
|
3618
|
+
lowerAggregateFields(selectionSet, table) {
|
|
3619
|
+
const result = {};
|
|
3620
|
+
for (const aggField of aggregateBlockFields(selectionSet)) {
|
|
3621
|
+
const aggName = aggField.name.value;
|
|
3622
|
+
if (aggName === "count") {
|
|
3623
|
+
assertKnownArgs(aggField, ["columns", "distinct"]);
|
|
3624
|
+
const args = argsByName(aggField);
|
|
3625
|
+
const count = {};
|
|
3626
|
+
const columns = args.get("columns");
|
|
3627
|
+
if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
|
|
3628
|
+
const distinct = args.get("distinct");
|
|
3629
|
+
if (distinct) {
|
|
3630
|
+
if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail("count(distinct:) must be a boolean literal", "invalid-aggregate");
|
|
3631
|
+
count.distinct = distinct.value.value;
|
|
3340
3632
|
}
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3633
|
+
result.count = count;
|
|
3634
|
+
continue;
|
|
3635
|
+
}
|
|
3636
|
+
if (aggName === "sum" || aggName === "avg") {
|
|
3637
|
+
if (!aggField.selectionSet) fail(`'${aggName}' needs a column selection`, "invalid-aggregate");
|
|
3638
|
+
const columns = [];
|
|
3639
|
+
for (const columnField of aggField.selectionSet.selections) {
|
|
3640
|
+
if (columnField.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
3641
|
+
if (columnField.name.value === "__typename") continue;
|
|
3642
|
+
const column = this.column(table, columnField.name.value);
|
|
3643
|
+
if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
|
|
3644
|
+
fail(
|
|
3645
|
+
`'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
|
|
3646
|
+
"invalid-aggregate",
|
|
3647
|
+
this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
|
|
3648
|
+
);
|
|
3356
3649
|
}
|
|
3357
|
-
|
|
3358
|
-
continue;
|
|
3650
|
+
columns.push(columnField.name.value);
|
|
3359
3651
|
}
|
|
3360
|
-
|
|
3652
|
+
result[aggName] = columns;
|
|
3653
|
+
continue;
|
|
3361
3654
|
}
|
|
3655
|
+
fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
|
|
3362
3656
|
}
|
|
3363
|
-
if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
3364
3657
|
return result;
|
|
3365
3658
|
}
|
|
3366
3659
|
// -------------------------------------------------------------------------
|
|
@@ -3370,19 +3663,49 @@ var OperationLowerer = class {
|
|
|
3370
3663
|
const roots = [];
|
|
3371
3664
|
for (const node of operation.selectionSet.selections) {
|
|
3372
3665
|
if (node.kind !== graphql.Kind.FIELD) fail("fragments are not supported at the operation root", "invalid-root");
|
|
3666
|
+
if (node.selectionSet) graphql.visit(node.selectionSet, {
|
|
3667
|
+
Directive(directive) {
|
|
3668
|
+
if (directive.name.value === "assertAffectedRows") {
|
|
3669
|
+
fail("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
});
|
|
3673
|
+
const assertion = this.parseAffectedRowAssertion(node, kind);
|
|
3373
3674
|
const gate = this.parseRootDirectives(node);
|
|
3374
3675
|
if (gate.omit) continue;
|
|
3375
3676
|
const root = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
|
|
3376
|
-
|
|
3677
|
+
const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
|
|
3678
|
+
roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
|
|
3377
3679
|
}
|
|
3378
3680
|
if (roots.length === 0) fail("operation has no roots (all statically skipped?)", "invalid-root");
|
|
3379
3681
|
return roots;
|
|
3380
3682
|
}
|
|
3683
|
+
parseAffectedRowAssertion(node, kind) {
|
|
3684
|
+
const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
|
|
3685
|
+
if (!assertions.length) return void 0;
|
|
3686
|
+
if (kind !== "mutation" || assertions.length !== 1) {
|
|
3687
|
+
fail("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
|
|
3688
|
+
}
|
|
3689
|
+
if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
|
|
3690
|
+
fail("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
|
|
3691
|
+
}
|
|
3692
|
+
const args = assertions[0].arguments;
|
|
3693
|
+
const arg = args?.[0];
|
|
3694
|
+
if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== graphql.Kind.INT) {
|
|
3695
|
+
fail("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
|
|
3696
|
+
}
|
|
3697
|
+
const count = Number(arg.value.value);
|
|
3698
|
+
if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
|
|
3699
|
+
fail("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
|
|
3700
|
+
}
|
|
3701
|
+
return count;
|
|
3702
|
+
}
|
|
3381
3703
|
parseRootDirectives(node) {
|
|
3382
3704
|
let includeIf;
|
|
3383
3705
|
let omit = false;
|
|
3384
3706
|
for (const directive of node.directives ?? []) {
|
|
3385
3707
|
const name = directive.name.value;
|
|
3708
|
+
if (name === "assertAffectedRows") continue;
|
|
3386
3709
|
if (name !== "include" && name !== "skip") {
|
|
3387
3710
|
fail(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
|
|
3388
3711
|
}
|
|
@@ -3431,6 +3754,9 @@ var OperationLowerer = class {
|
|
|
3431
3754
|
const pk = this.lowerPkArgs(node, table);
|
|
3432
3755
|
return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
|
|
3433
3756
|
}
|
|
3757
|
+
if (rootInfo.kind === "aggregateGroups") {
|
|
3758
|
+
return this.lowerAggregateGroupsRoot(node, { alias, table });
|
|
3759
|
+
}
|
|
3434
3760
|
assertKnownArgs(node, ["where"]);
|
|
3435
3761
|
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
3436
3762
|
const whereArg = args.get("where");
|
|
@@ -3443,6 +3769,108 @@ var OperationLowerer = class {
|
|
|
3443
3769
|
...aggregate
|
|
3444
3770
|
};
|
|
3445
3771
|
}
|
|
3772
|
+
/** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
|
|
3773
|
+
lowerAggregateGroupsRoot(node, { alias, table }) {
|
|
3774
|
+
assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
|
|
3775
|
+
if (!node.selectionSet) fail(`'${node.name.value}' needs a selection set`, "invalid-selection");
|
|
3776
|
+
const args = argsByName(node);
|
|
3777
|
+
const groupByArg = args.get("groupBy");
|
|
3778
|
+
if (!groupByArg) fail(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
|
|
3779
|
+
const whereArg = args.get("where");
|
|
3780
|
+
const groupBy = this.lowerGroupBy(groupByArg.value, table);
|
|
3781
|
+
const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
|
|
3782
|
+
const offset = this.lowerOffset(args.get("offset")?.value);
|
|
3783
|
+
return {
|
|
3784
|
+
kind: "aggregateGroups",
|
|
3785
|
+
alias,
|
|
3786
|
+
table,
|
|
3787
|
+
where: whereArg ? this.lowerBoolExp(whereArg.value, table) : CONST_TRUE,
|
|
3788
|
+
groupBy,
|
|
3789
|
+
key: aggregate.key,
|
|
3790
|
+
...aggregate.count ? { count: aggregate.count } : {},
|
|
3791
|
+
...aggregate.sum ? { sum: aggregate.sum } : {},
|
|
3792
|
+
...aggregate.avg ? { avg: aggregate.avg } : {},
|
|
3793
|
+
limit: this.lowerGroupLimit(args.get("limit")?.value),
|
|
3794
|
+
...offset ? { offset } : {}
|
|
3795
|
+
};
|
|
3796
|
+
}
|
|
3797
|
+
/**
|
|
3798
|
+
* `groupBy: [{ column: startedAt, bucket: MONTH }, …]` → GroupByKeyIr[].
|
|
3799
|
+
*
|
|
3800
|
+
* Document-authored only: a whole-argument variable is rejected rather than
|
|
3801
|
+
* becoming a §4f slot. Group keys choose which expressions reach GROUP BY,
|
|
3802
|
+
* and dynamic-input.ts refuses aggregation shapes from client input on the
|
|
3803
|
+
* same reasoning — `where` stays dynamic, which is what runtime filtering
|
|
3804
|
+
* actually needs.
|
|
3805
|
+
*/
|
|
3806
|
+
lowerGroupBy(node, table) {
|
|
3807
|
+
if (node.kind === graphql.Kind.VARIABLE) {
|
|
3808
|
+
fail(
|
|
3809
|
+
`groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
|
|
3810
|
+
"dynamic-group-by"
|
|
3811
|
+
);
|
|
3812
|
+
}
|
|
3813
|
+
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
3814
|
+
if (items.length === 0) fail(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
|
|
3815
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3816
|
+
return items.map((item) => {
|
|
3817
|
+
if (item.kind !== graphql.Kind.OBJECT) {
|
|
3818
|
+
fail(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
|
|
3819
|
+
}
|
|
3820
|
+
const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
|
|
3821
|
+
for (const key of fields.keys()) {
|
|
3822
|
+
if (key !== "column" && key !== "bucket") {
|
|
3823
|
+
fail(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
const columnNode = fields.get("column");
|
|
3827
|
+
if (!columnNode) fail(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
|
|
3828
|
+
if (columnNode.kind !== graphql.Kind.ENUM) {
|
|
3829
|
+
fail(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
|
|
3830
|
+
}
|
|
3831
|
+
const column = this.column(table, columnNode.value);
|
|
3832
|
+
if (!column) {
|
|
3833
|
+
fail(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
3834
|
+
}
|
|
3835
|
+
if (column.type === "jsonb") {
|
|
3836
|
+
fail(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
|
|
3837
|
+
}
|
|
3838
|
+
const bucketNode = fields.get("bucket");
|
|
3839
|
+
let bucket;
|
|
3840
|
+
if (bucketNode) {
|
|
3841
|
+
if (bucketNode.kind !== graphql.Kind.ENUM) {
|
|
3842
|
+
fail(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
3843
|
+
}
|
|
3844
|
+
bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
|
|
3845
|
+
if (!bucket) {
|
|
3846
|
+
fail(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
3847
|
+
}
|
|
3848
|
+
if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
|
|
3849
|
+
fail(
|
|
3850
|
+
`groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
|
|
3851
|
+
"invalid-group-by"
|
|
3852
|
+
);
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
if (seen.has(columnNode.value)) {
|
|
3856
|
+
fail(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
|
|
3857
|
+
}
|
|
3858
|
+
seen.add(columnNode.value);
|
|
3859
|
+
return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
/** Like lowerLimit, but bounded by MAX_GROUPS and defaulting to it. */
|
|
3863
|
+
lowerGroupLimit(node) {
|
|
3864
|
+
const base = { default: MAX_GROUPS, max: MAX_GROUPS };
|
|
3865
|
+
if (!node) return base;
|
|
3866
|
+
if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
3867
|
+
if (node.kind === graphql.Kind.INT) {
|
|
3868
|
+
const value = Number(node.value);
|
|
3869
|
+
if (value < 1) fail("limit must be a positive integer", "invalid-limit");
|
|
3870
|
+
return { ...base, value: Math.min(value, MAX_GROUPS) };
|
|
3871
|
+
}
|
|
3872
|
+
fail("limit must be an integer literal or a variable", "invalid-limit");
|
|
3873
|
+
}
|
|
3446
3874
|
/** byPk-style pk arguments: one argument per primary-key column, exactly. */
|
|
3447
3875
|
lowerPkArgs(node, table) {
|
|
3448
3876
|
const meta = this.meta(table);
|
|
@@ -3952,7 +4380,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
3952
4380
|
const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
|
|
3953
4381
|
const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
|
|
3954
4382
|
operations[restricted.name] = {
|
|
3955
|
-
irVersion: IR_VERSION,
|
|
4383
|
+
irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? IR_VERSION : LEGACY_IR_VERSION,
|
|
3956
4384
|
name: restricted.name,
|
|
3957
4385
|
opHash: computeOpHash({ definition: flattened, schemaHash }),
|
|
3958
4386
|
kind: restricted.kind,
|
|
@@ -3979,7 +4407,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
3979
4407
|
}
|
|
3980
4408
|
if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
|
|
3981
4409
|
const artifact = {
|
|
3982
|
-
irVersion:
|
|
4410
|
+
irVersion: requiredArtifactIrVersion(Object.values(operations)),
|
|
3983
4411
|
namespace: ctx.namespace,
|
|
3984
4412
|
schemaHash,
|
|
3985
4413
|
opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
|
|
@@ -4179,12 +4607,12 @@ function castParts(cast) {
|
|
|
4179
4607
|
|
|
4180
4608
|
// src/predicate/ast.ts
|
|
4181
4609
|
var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
4182
|
-
function walkPredicate(predicate,
|
|
4183
|
-
|
|
4610
|
+
function walkPredicate(predicate, visit2) {
|
|
4611
|
+
visit2(predicate);
|
|
4184
4612
|
if (predicate.kind === "and" || predicate.kind === "or") {
|
|
4185
|
-
for (const operand of predicate.operands) walkPredicate(operand,
|
|
4613
|
+
for (const operand of predicate.operands) walkPredicate(operand, visit2);
|
|
4186
4614
|
}
|
|
4187
|
-
if (predicate.kind === "not") walkPredicate(predicate.operand,
|
|
4615
|
+
if (predicate.kind === "not") walkPredicate(predicate.operand, visit2);
|
|
4188
4616
|
}
|
|
4189
4617
|
function predicateOperands(predicate) {
|
|
4190
4618
|
const operands = [];
|
|
@@ -5159,7 +5587,7 @@ var FACTORIES2 = {
|
|
|
5159
5587
|
var SCHEMA_IMPORT_KEY2 = "$schema";
|
|
5160
5588
|
var enumImportKey = (name) => `$enum_${name}`;
|
|
5161
5589
|
var columnTablesV2 = (definition) => definition.tables.filter((table) => !table.source);
|
|
5162
|
-
function buildDrizzleEntitiesV2(definition) {
|
|
5590
|
+
function buildDrizzleEntitiesV2(definition, options = {}) {
|
|
5163
5591
|
const schemaName = ormSchemaName(definition.namespace);
|
|
5164
5592
|
const schema = pgCore.pgSchema(schemaName);
|
|
5165
5593
|
const imports = { [SCHEMA_IMPORT_KEY2]: schema };
|
|
@@ -5174,11 +5602,19 @@ function buildDrizzleEntitiesV2(definition) {
|
|
|
5174
5602
|
}
|
|
5175
5603
|
const byName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
5176
5604
|
for (const table of sortByEnforcedLinks2(columnTablesV2(definition))) {
|
|
5177
|
-
const drizzleTable = buildTable2(schema, table, built, enums, byName);
|
|
5605
|
+
const drizzleTable = buildTable2(schema, table, built, enums, byName, options.introspection ?? false);
|
|
5178
5606
|
built.set(table.table, drizzleTable);
|
|
5179
5607
|
imports[table.table] = drizzleTable;
|
|
5180
5608
|
tables[table.table] = drizzleTable;
|
|
5181
5609
|
}
|
|
5610
|
+
if (options.introspection) {
|
|
5611
|
+
for (const table of definition.tables.filter((table2) => table2.source)) {
|
|
5612
|
+
const columns = Object.fromEntries(
|
|
5613
|
+
table.columns.map((column) => [column.columnName, buildViewColumnV2({ view: table.table, column, enums })])
|
|
5614
|
+
);
|
|
5615
|
+
imports[table.table] = schema.view(table.table, columns).existing();
|
|
5616
|
+
}
|
|
5617
|
+
}
|
|
5182
5618
|
for (const view of definition.views ?? []) {
|
|
5183
5619
|
const columns = Object.fromEntries(
|
|
5184
5620
|
view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
|
|
@@ -5422,9 +5858,10 @@ function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, b
|
|
|
5422
5858
|
function generateSnapshotV2(definition, previousId3) {
|
|
5423
5859
|
const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
|
|
5424
5860
|
const snapshot = api.generateDrizzleJson(imports, previousId3, [schemaName]);
|
|
5425
|
-
if ((definition.views?.length ?? 0) > 0) {
|
|
5861
|
+
if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
|
|
5426
5862
|
const extended = snapshot;
|
|
5427
5863
|
extended._sequenceOrm = {
|
|
5864
|
+
...definition.tables.some((table) => table.foreignKeys?.length) ? { foreignKeys: Object.fromEntries(definition.tables.flatMap((table) => (table.foreignKeys ?? []).map((fk) => [`${table.table}.${fk.name}`, structuredClone(fk)]))) } : {},
|
|
5428
5865
|
viewDependencies: Object.fromEntries(
|
|
5429
5866
|
(definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
|
|
5430
5867
|
)
|
|
@@ -5442,7 +5879,7 @@ function buildViewColumnV2({
|
|
|
5442
5879
|
if (!column.nullable) builder = builder.notNull();
|
|
5443
5880
|
return builder;
|
|
5444
5881
|
}
|
|
5445
|
-
function buildTable2(schema, table, built, enums, byName) {
|
|
5882
|
+
function buildTable2(schema, table, built, enums, byName, introspection) {
|
|
5446
5883
|
const primaryKeyColumns2 = table.primaryKey ?? [];
|
|
5447
5884
|
if (primaryKeyColumns2.length === 0) {
|
|
5448
5885
|
throw new OrmDefinitionError(`table '${table.table}' declares no primaryKey`);
|
|
@@ -5458,13 +5895,22 @@ function buildTable2(schema, table, built, enums, byName) {
|
|
|
5458
5895
|
const columns = Object.fromEntries(
|
|
5459
5896
|
table.columns.map((column) => [
|
|
5460
5897
|
column.columnName,
|
|
5461
|
-
buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums })
|
|
5898
|
+
buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums, introspection })
|
|
5462
5899
|
])
|
|
5463
5900
|
);
|
|
5464
5901
|
const drizzleTable = schema.table(table.table, columns, (self) => [
|
|
5465
5902
|
...buildCompositePrimaryKey(table, primaryKeyColumns2, self),
|
|
5466
5903
|
...buildIndexes2(table, self),
|
|
5467
|
-
...buildForeignKeys2(table, self, built, byName)
|
|
5904
|
+
...buildForeignKeys2(table, self, built, byName),
|
|
5905
|
+
...(table.foreignKeys ?? []).map((fk) => {
|
|
5906
|
+
const target = fk.target === table.table ? self : built.get(fk.target);
|
|
5907
|
+
if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
|
|
5908
|
+
return pgCore.foreignKey({
|
|
5909
|
+
name: fk.name,
|
|
5910
|
+
columns: fk.columns.map((column) => self[column]),
|
|
5911
|
+
foreignColumns: fk.references.map((column) => target[column])
|
|
5912
|
+
});
|
|
5913
|
+
})
|
|
5468
5914
|
]);
|
|
5469
5915
|
return drizzleTable.enableRLS();
|
|
5470
5916
|
}
|
|
@@ -5481,7 +5927,8 @@ function buildColumnV2({
|
|
|
5481
5927
|
column,
|
|
5482
5928
|
singlePrimaryKey,
|
|
5483
5929
|
compositeMembers,
|
|
5484
|
-
enums
|
|
5930
|
+
enums,
|
|
5931
|
+
introspection
|
|
5485
5932
|
}) {
|
|
5486
5933
|
const factory = resolveColumnFactory(table, column, enums);
|
|
5487
5934
|
let builder = factory(column.columnName);
|
|
@@ -5502,7 +5949,7 @@ function buildColumnV2({
|
|
|
5502
5949
|
}
|
|
5503
5950
|
builder = builder.default(drizzleOrm.sql.raw(column.defaultFn));
|
|
5504
5951
|
} else if (column.default !== void 0 && column.default !== null) {
|
|
5505
|
-
builder = builder.default(column.default);
|
|
5952
|
+
builder = introspection && column.type === "jsonb" && typeof column.default === "string" ? builder.default(drizzleOrm.sql.raw(`${quoteLiteral(column.default)}::jsonb`)) : builder.default(column.default);
|
|
5506
5953
|
}
|
|
5507
5954
|
return builder;
|
|
5508
5955
|
}
|
|
@@ -5687,7 +6134,8 @@ async function planMigrationV2({
|
|
|
5687
6134
|
assertValidViewDependencies(canonical);
|
|
5688
6135
|
const hints = applyMigrationHintsV2(previous, canonical);
|
|
5689
6136
|
const snapshot = generateSnapshotV2(canonical, previousId(previous));
|
|
5690
|
-
const
|
|
6137
|
+
const generatedBaseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
|
|
6138
|
+
const baseDdl = orderForeignKeyAdditions(canonical, generatedBaseDdl);
|
|
5691
6139
|
const previousViews = snapshotViews(hints.adjusted);
|
|
5692
6140
|
const currentViews = snapshotViews(snapshot);
|
|
5693
6141
|
const previousDependencies = snapshotViewDependencies(hints.adjusted);
|
|
@@ -5724,6 +6172,7 @@ async function planMigrationV2({
|
|
|
5724
6172
|
...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
|
|
5725
6173
|
...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
|
|
5726
6174
|
...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
|
|
6175
|
+
...foreignKeyTimingStatements(canonical, previous, baseDdl).map((sql4) => ({ kind: "ddl", sql: sql4 })),
|
|
5727
6176
|
...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
|
|
5728
6177
|
...createOrder.map((name) => {
|
|
5729
6178
|
const view = currentViewDefinitions.get(name);
|
|
@@ -5733,6 +6182,29 @@ async function planMigrationV2({
|
|
|
5733
6182
|
];
|
|
5734
6183
|
return { namespace: canonical.namespace, schema, statements, snapshot };
|
|
5735
6184
|
}
|
|
6185
|
+
function orderForeignKeyAdditions(canonical, statements) {
|
|
6186
|
+
if (!canonical.tables.some((table) => table.foreignKeys?.length)) return [...statements];
|
|
6187
|
+
const addsForeignKey = (statement) => /^ALTER TABLE[\s\S]* ADD CONSTRAINT[\s\S]* FOREIGN KEY\b/.test(statement);
|
|
6188
|
+
return [...statements.filter((statement) => !addsForeignKey(statement)), ...statements.filter(addsForeignKey)];
|
|
6189
|
+
}
|
|
6190
|
+
function foreignKeyTimingStatements(canonical, previous, baseDdl) {
|
|
6191
|
+
const prior = previous._sequenceOrm?.foreignKeys ?? {};
|
|
6192
|
+
const result = [];
|
|
6193
|
+
for (const table of canonical.tables) {
|
|
6194
|
+
for (const fk of table.foreignKeys ?? []) {
|
|
6195
|
+
const before = prior[`${table.table}.${fk.name}`];
|
|
6196
|
+
const recreated = baseDdl.some((statement) => statement.includes(`ALTER TABLE ${quoteIdentifier(ormSchemaName(canonical.namespace))}.${quoteIdentifier(table.table)} ADD CONSTRAINT ${quoteIdentifier(fk.name)}`));
|
|
6197
|
+
if (fk.deferrable ? !before || before.deferrable !== fk.deferrable || recreated : before?.deferrable && !recreated) {
|
|
6198
|
+
result.push(foreignKeyTimingSql(ormSchemaName(canonical.namespace), table.table, fk.name, fk.deferrable));
|
|
6199
|
+
}
|
|
6200
|
+
}
|
|
6201
|
+
}
|
|
6202
|
+
return result;
|
|
6203
|
+
}
|
|
6204
|
+
function foreignKeyTimingSql(schema, table, name, timing) {
|
|
6205
|
+
const clause = timing === void 0 ? "NOT DEFERRABLE" : timing === "initially-deferred" ? "DEFERRABLE INITIALLY DEFERRED" : "DEFERRABLE INITIALLY IMMEDIATE";
|
|
6206
|
+
return `ALTER TABLE ${quoteIdentifier(schema)}.${quoteIdentifier(table)} ALTER CONSTRAINT ${quoteIdentifier(name)} ${clause}`;
|
|
6207
|
+
}
|
|
5736
6208
|
function validateViewCatalogStatement({
|
|
5737
6209
|
canonical,
|
|
5738
6210
|
view,
|
|
@@ -6853,8 +7325,12 @@ function generateSdl(canonical, options = {}) {
|
|
|
6853
7325
|
'"""Gates an operation behind a platform capability or custom role \u2014 checked by the data plane before execution."""\ndirective @requires(capability: String, role: String) on QUERY | MUTATION'
|
|
6854
7326
|
);
|
|
6855
7327
|
blocks.push("directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT");
|
|
7328
|
+
blocks.push(
|
|
7329
|
+
'"""Abort the entire mutation unless this root affects exactly the document-authored number of rows. Mutation roots only; cannot be conditional."""\ndirective @assertAffectedRows(exactly: Int!) on FIELD'
|
|
7330
|
+
);
|
|
6856
7331
|
for (const scalar2 of CUSTOM_SCALARS) blocks.push(`scalar ${scalar2}`);
|
|
6857
7332
|
blocks.push(renderEnum(ORDER_BY_ENUM, [...ORDER_BY_ENUM_VALUES]));
|
|
7333
|
+
blocks.push(renderEnum(DATE_BUCKET_ENUM, DATE_BUCKETS.map((bucket) => bucket.toUpperCase())));
|
|
6858
7334
|
for (const enumName of [...usedEnums].sort()) {
|
|
6859
7335
|
const entry = enumByName.get(enumName);
|
|
6860
7336
|
if (!entry) continue;
|
|
@@ -6884,9 +7360,24 @@ function generateSdl(canonical, options = {}) {
|
|
|
6884
7360
|
blocks.push(renderQueryRoot(tables, namesByTable));
|
|
6885
7361
|
const mutationRoot = renderMutationRoot(tables, namesByTable);
|
|
6886
7362
|
if (mutationRoot !== null) blocks.push(mutationRoot);
|
|
7363
|
+
assertNoDuplicateTypeNames(blocks);
|
|
6887
7364
|
return `${blocks.join("\n\n")}
|
|
6888
7365
|
`;
|
|
6889
7366
|
}
|
|
7367
|
+
var TYPE_DECLARATION_RE = /^(?:type|input|enum|scalar|interface|union)\s+([_A-Za-z][_0-9A-Za-z]*)/;
|
|
7368
|
+
function assertNoDuplicateTypeNames(blocks) {
|
|
7369
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7370
|
+
for (const block of blocks) {
|
|
7371
|
+
const declared = TYPE_DECLARATION_RE.exec(block)?.[1];
|
|
7372
|
+
if (declared === void 0) continue;
|
|
7373
|
+
if (seen.has(declared)) {
|
|
7374
|
+
throw new SdlGenerationError(
|
|
7375
|
+
`Generated schema declares "${declared}" twice. A canonical enum or table name is PascalCased into this GraphQL type name and collides with another declaration (for example an enum named "date_bucket" renders as "DateBucket", the shared bucket enum). Rename the offending enum or table.`
|
|
7376
|
+
);
|
|
7377
|
+
}
|
|
7378
|
+
seen.add(declared);
|
|
7379
|
+
}
|
|
7380
|
+
}
|
|
6890
7381
|
function assertEnumValue(value, context) {
|
|
6891
7382
|
if (!GRAPHQL_NAME_RE2.test(value) || RESERVED_ENUM_VALUES.has(value)) {
|
|
6892
7383
|
throw new SdlGenerationError(
|
|
@@ -6975,7 +7466,8 @@ function renderTableBlocks(ctx) {
|
|
|
6975
7466
|
renderSelectColumnEnum(ctx),
|
|
6976
7467
|
renderOrderByInput(ctx),
|
|
6977
7468
|
renderBoolExp(ctx),
|
|
6978
|
-
...renderAggregateTypes(ctx)
|
|
7469
|
+
...renderAggregateTypes(ctx),
|
|
7470
|
+
...renderGroupByTypes(ctx)
|
|
6979
7471
|
];
|
|
6980
7472
|
if (ctx.emitAggregateBoolExp) blocks.push(...renderAggregateBoolExp(ctx));
|
|
6981
7473
|
const pk = primaryKeyColumns(table);
|
|
@@ -7112,6 +7604,32 @@ ${sumLines.join("\n")}
|
|
|
7112
7604
|
}
|
|
7113
7605
|
return blocks;
|
|
7114
7606
|
}
|
|
7607
|
+
function groupableColumns(table) {
|
|
7608
|
+
return table.columns.filter((column) => column.type !== "jsonb");
|
|
7609
|
+
}
|
|
7610
|
+
function renderGroupByTypes(ctx) {
|
|
7611
|
+
const { table, names } = ctx;
|
|
7612
|
+
const groupable = groupableColumns(table);
|
|
7613
|
+
if (groupable.length === 0) return [];
|
|
7614
|
+
const keyLines = groupable.map((column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`);
|
|
7615
|
+
return [
|
|
7616
|
+
renderEnum(names.groupByColumnEnum, groupable.map((column) => fieldName(column.name))),
|
|
7617
|
+
`input ${names.groupByInput} {
|
|
7618
|
+
column: ${names.groupByColumnEnum}!
|
|
7619
|
+
bucket: ${DATE_BUCKET_ENUM}
|
|
7620
|
+
}`,
|
|
7621
|
+
`type ${names.groupKey} {
|
|
7622
|
+
${keyLines.join("\n")}
|
|
7623
|
+
}`,
|
|
7624
|
+
`type ${names.aggregateGroup} {
|
|
7625
|
+
key: ${names.groupKey}!
|
|
7626
|
+
aggregate: ${names.aggregateFields}
|
|
7627
|
+
}`
|
|
7628
|
+
];
|
|
7629
|
+
}
|
|
7630
|
+
function groupRootArgs(names) {
|
|
7631
|
+
return `where: ${names.boolExp}, groupBy: [${names.groupByInput}!]!, limit: Int, offset: Int`;
|
|
7632
|
+
}
|
|
7115
7633
|
function renderAggregateBoolExp(ctx) {
|
|
7116
7634
|
const { names } = ctx;
|
|
7117
7635
|
return [
|
|
@@ -7137,6 +7655,9 @@ function constraintValues(table) {
|
|
|
7137
7655
|
}
|
|
7138
7656
|
return [...values].sort();
|
|
7139
7657
|
}
|
|
7658
|
+
function clientSettableColumns(table) {
|
|
7659
|
+
return table.columns.filter((column) => column.onUpdateFn === void 0);
|
|
7660
|
+
}
|
|
7140
7661
|
function renderWriteBlocks(ctx, access) {
|
|
7141
7662
|
const { table, names, pinned } = ctx;
|
|
7142
7663
|
const blocks = [];
|
|
@@ -7158,7 +7679,7 @@ ${insertLines.join("\n")}
|
|
|
7158
7679
|
}`);
|
|
7159
7680
|
}
|
|
7160
7681
|
if (access.update) {
|
|
7161
|
-
const setLines = table.
|
|
7682
|
+
const setLines = clientSettableColumns(table).map(
|
|
7162
7683
|
(column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
|
|
7163
7684
|
);
|
|
7164
7685
|
blocks.push(`input ${names.setInput} {
|
|
@@ -7185,7 +7706,7 @@ ${incLines.join("\n")}
|
|
|
7185
7706
|
if (access.insert && allowsOnConflict(table) && constraints.length > 0) {
|
|
7186
7707
|
for (const value of constraints) assertEnumValue(value, `constraint enum for table "${table.table}"`);
|
|
7187
7708
|
blocks.push(renderEnum(names.constraintEnum, constraints));
|
|
7188
|
-
const updateColumns = table.
|
|
7709
|
+
const updateColumns = clientSettableColumns(table).map((column) => fieldName(column.name));
|
|
7189
7710
|
blocks.push(renderEnum(names.updateColumnEnum, updateColumns));
|
|
7190
7711
|
blocks.push(
|
|
7191
7712
|
`input ${names.onConflict} {
|
|
@@ -7217,6 +7738,9 @@ function renderQueryRoot(tables, namesByTable) {
|
|
|
7217
7738
|
lines.push(` ${names.byPkRoot}(${args}): ${names.typename}`);
|
|
7218
7739
|
}
|
|
7219
7740
|
lines.push(` ${names.aggregateRoot}(${rootArgs(names)}): ${names.aggregate}!`);
|
|
7741
|
+
if (groupableColumns(table).length > 0) {
|
|
7742
|
+
lines.push(` ${names.aggregateGroupsRoot}(${groupRootArgs(names)}): [${names.aggregateGroup}!]!`);
|
|
7743
|
+
}
|
|
7220
7744
|
}
|
|
7221
7745
|
lines.push("}");
|
|
7222
7746
|
return lines.join("\n");
|
|
@@ -7285,6 +7809,40 @@ function renderCowMutationFields(table, names) {
|
|
|
7285
7809
|
}
|
|
7286
7810
|
return lines;
|
|
7287
7811
|
}
|
|
7812
|
+
var ORM_MIGRATION_EXPORT_FILE = "orm.migration.json";
|
|
7813
|
+
var ORM_MIGRATION_EXPORT_MAX_BYTES = ORM_REGISTRATION_MAX_BYTES;
|
|
7814
|
+
var hash = zod.z.string().regex(/^sha256:[0-9a-f]{64}$/);
|
|
7815
|
+
var ormMigrationPayloadSchema = zod.z.object({
|
|
7816
|
+
canonical_definition: canonicalDefinitionSchema,
|
|
7817
|
+
documents: zod.z.array(zod.z.object({ path: zod.z.string().min(1).max(2048), contents: zod.z.string().max(1e6) }).strict()).max(5e3),
|
|
7818
|
+
client_manifest: zod.z.object({ ops_hash: hash, operations: zod.z.record(zod.z.string(), hash) }).strict(),
|
|
7819
|
+
sdl: zod.z.string().refine((value) => Buffer.byteLength(value, "utf8") <= MAX_GENERATED_SDL_BYTES, {
|
|
7820
|
+
message: `sdl exceeds ${MAX_GENERATED_SDL_BYTES} bytes`
|
|
7821
|
+
}),
|
|
7822
|
+
schema_snapshot: zod.z.json(),
|
|
7823
|
+
// The existing ORM committed-migration validator and replay engine remain
|
|
7824
|
+
// authoritative. This envelope hashes the provenance, never authorizes SQL.
|
|
7825
|
+
migration: zod.z.json().optional()
|
|
7826
|
+
}).strict();
|
|
7827
|
+
zod.z.object({
|
|
7828
|
+
schema_version: zod.z.literal(1),
|
|
7829
|
+
schema_hash: hash,
|
|
7830
|
+
payload: ormMigrationPayloadSchema,
|
|
7831
|
+
checksum: zod.z.string().regex(/^[0-9a-f]{64}$/)
|
|
7832
|
+
}).strict();
|
|
7833
|
+
function checksum(value) {
|
|
7834
|
+
return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
|
|
7835
|
+
}
|
|
7836
|
+
function createOrmMigrationExport({ schemaHash, payload }) {
|
|
7837
|
+
const contents = { schema_version: 1, schema_hash: hash.parse(schemaHash), payload: ormMigrationPayloadSchema.parse(payload) };
|
|
7838
|
+
return { ...contents, checksum: checksum(contents) };
|
|
7839
|
+
}
|
|
7840
|
+
function renderOrmMigrationExport(exported) {
|
|
7841
|
+
const text4 = `${JSON.stringify(exported, null, 2)}
|
|
7842
|
+
`;
|
|
7843
|
+
if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
|
|
7844
|
+
return text4;
|
|
7845
|
+
}
|
|
7288
7846
|
|
|
7289
7847
|
// src/cli.ts
|
|
7290
7848
|
var BASE_URL_ENV = "SEQUENCE_ORM_BASE_URL";
|
|
@@ -7293,11 +7851,15 @@ var ORM_MODULE_PATH_ENV = "SEQUENCE_ORM_MODULE_PATH";
|
|
|
7293
7851
|
var USAGE = `usage:
|
|
7294
7852
|
seq-studio orm init <dir> scaffold a namespace package (v2: TypeScript schema + GraphQL documents)
|
|
7295
7853
|
seq-studio orm generate [dir] write schema.graphql + operations.manifest.json + typePolicies.gen.ts (+ consumer graphql-codegen when codegen.ts is present)
|
|
7854
|
+
seq-studio orm generate [dir] --migration-export [--check]
|
|
7855
|
+
export reviewed ORM inputs for a coordinated pipeline migration; --check verifies without writing
|
|
7296
7856
|
seq-studio orm validate [dir] parse + validate definitions and verify committed migrations are in sync
|
|
7297
7857
|
seq-studio orm plan [dir] -e <env> offline dry run: compile definitions and diff against the registry (no database, no writes)
|
|
7298
7858
|
seq-studio orm apply [dir] -e <env> author the migration, register, apply, and refresh the generated outputs (the everyday command)
|
|
7299
7859
|
seq-studio orm apply --dry-run -e <env> rehearse the migration on a copy of the environment's real data, then discard it (no real apply)
|
|
7300
7860
|
seq-studio orm apply --no-create -e <env> apply only if the namespace is already registered (CI guard; never provisions a new namespace)
|
|
7861
|
+
seq-studio orm execute <operation> [dir] --variables-file <file> -e <env>
|
|
7862
|
+
execute one named persisted operation as the signed-in caller
|
|
7301
7863
|
seq-studio orm diff [dir] write/verify the committed migration standalone (--check is the offline CI gate; --allow-destructive consents; --rebase records a v1 -> v2 conversion as a verified no-op)
|
|
7302
7864
|
seq-studio orm migrate-from-yaml <dir> convert a v1 YAML namespace to the v2 TypeScript format (migrations/ preserved)
|
|
7303
7865
|
|
|
@@ -7305,6 +7867,8 @@ var USAGE = `usage:
|
|
|
7305
7867
|
v2 = sequence.config.ts + schema/*.ts + graphql/**; v1 (legacy) = one <table>.yaml per table.
|
|
7306
7868
|
Environments: see \`seq-studio envs list\` (discovered after you authenticate).
|
|
7307
7869
|
Authenticate with: seq-studio login
|
|
7870
|
+
Execute endpoint rollout: SEQUENCE_ORM_ATLAS_EXECUTE_V2=1 opts into /api/data/v2;
|
|
7871
|
+
otherwise execute uses /_data/v2. Server rollout and authorization still apply.
|
|
7308
7872
|
`;
|
|
7309
7873
|
var tokenProvider = null;
|
|
7310
7874
|
function setTokenProvider(provider) {
|
|
@@ -7321,6 +7885,10 @@ async function runCli(argv) {
|
|
|
7321
7885
|
console.log(USAGE);
|
|
7322
7886
|
return command ? 0 : 1;
|
|
7323
7887
|
}
|
|
7888
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
7889
|
+
console.log(USAGE);
|
|
7890
|
+
return 0;
|
|
7891
|
+
}
|
|
7324
7892
|
try {
|
|
7325
7893
|
switch (command) {
|
|
7326
7894
|
case "init":
|
|
@@ -7358,6 +7926,8 @@ async function runCli(argv) {
|
|
|
7358
7926
|
const dir = positional(rest) ?? ".";
|
|
7359
7927
|
return await detectProjectFormat(dir) === "v2" ? await applyV2Command(rest, dir) : await applyCommand(rest);
|
|
7360
7928
|
}
|
|
7929
|
+
case "execute":
|
|
7930
|
+
return await executeV2Command(rest);
|
|
7361
7931
|
default:
|
|
7362
7932
|
console.error(`unknown command: ${command}`);
|
|
7363
7933
|
console.log(USAGE);
|
|
@@ -7368,13 +7938,14 @@ async function runCli(argv) {
|
|
|
7368
7938
|
return 1;
|
|
7369
7939
|
}
|
|
7370
7940
|
}
|
|
7941
|
+
var NON_DEFINITION_YAML_FILES = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "pnpm-workspace.yaml"]);
|
|
7371
7942
|
async function detectProjectFormat(dir) {
|
|
7372
7943
|
const entries = await promises.readdir(dir, { withFileTypes: true }).catch(() => {
|
|
7373
7944
|
throw new OrmDefinitionError(`namespace directory not found: ${dir}`);
|
|
7374
7945
|
});
|
|
7375
7946
|
const hasConfig = entries.some((entry) => entry.isFile() && entry.name === NAMESPACE_CONFIG_FILE);
|
|
7376
7947
|
const hasYaml = entries.some(
|
|
7377
|
-
(entry) => entry.isFile() && DEFINITION_FILE_EXTENSIONS.some((extension) => entry.name.endsWith(extension))
|
|
7948
|
+
(entry) => entry.isFile() && !NON_DEFINITION_YAML_FILES.has(entry.name) && DEFINITION_FILE_EXTENSIONS.some((extension) => entry.name.endsWith(extension))
|
|
7378
7949
|
);
|
|
7379
7950
|
if (hasConfig && hasYaml) {
|
|
7380
7951
|
throw new OrmDefinitionError(
|
|
@@ -7512,6 +8083,12 @@ function printDiagnostics(diagnostics) {
|
|
|
7512
8083
|
}
|
|
7513
8084
|
async function generateCommand(rest) {
|
|
7514
8085
|
const dir = positional(rest) ?? ".";
|
|
8086
|
+
const migrationExport = rest.includes("--migration-export");
|
|
8087
|
+
const check = rest.includes("--check");
|
|
8088
|
+
if (check && !migrationExport) {
|
|
8089
|
+
console.error("generate --check requires --migration-export");
|
|
8090
|
+
return 1;
|
|
8091
|
+
}
|
|
7515
8092
|
if (await detectProjectFormat(dir) === "v1") {
|
|
7516
8093
|
console.error(`generate needs a v2 project (${NAMESPACE_CONFIG_FILE}) \u2014 convert with: seq-studio orm migrate-from-yaml ${dir}`);
|
|
7517
8094
|
return 1;
|
|
@@ -7519,6 +8096,36 @@ async function generateCommand(rest) {
|
|
|
7519
8096
|
const compiled = await compileV2Project(dir);
|
|
7520
8097
|
printDiagnostics(compiled.ok ? compiled.project.diagnostics : compiled.diagnostics);
|
|
7521
8098
|
if (!compiled.ok) return 1;
|
|
8099
|
+
if (migrationExport) {
|
|
8100
|
+
if (await validateV2Command(dir) !== 0) return 1;
|
|
8101
|
+
const { loaded: loaded2, sdl, documents, manifest: manifest2 } = compiled.project;
|
|
8102
|
+
const workspace = await readMigrationWorkspace(dir, loaded2.canonical.namespace);
|
|
8103
|
+
const migration = await loadCommittedTipMigrationV2({ dir, loaded: loaded2, workspace });
|
|
8104
|
+
const exported = createOrmMigrationExport({ schemaHash: manifest2.schemaHash, payload: {
|
|
8105
|
+
canonical_definition: loaded2.canonical,
|
|
8106
|
+
documents,
|
|
8107
|
+
sdl,
|
|
8108
|
+
client_manifest: { ops_hash: manifest2.opsHash, operations: Object.fromEntries(Object.entries(manifest2.operations).map(([name, operation]) => [name, operation.opHash])) },
|
|
8109
|
+
schema_snapshot: workspace.tipSnapshot,
|
|
8110
|
+
...migration ? { migration } : {}
|
|
8111
|
+
} });
|
|
8112
|
+
const text4 = renderOrmMigrationExport(exported);
|
|
8113
|
+
const path$1 = path.join(dir, ORM_MIGRATION_EXPORT_FILE);
|
|
8114
|
+
if (check) {
|
|
8115
|
+
const committed = await promises.readFile(path$1, "utf8").catch((error) => {
|
|
8116
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
|
|
8117
|
+
throw error;
|
|
8118
|
+
});
|
|
8119
|
+
if (committed !== text4) {
|
|
8120
|
+
console.error(`${ORM_MIGRATION_EXPORT_FILE} is missing or stale; run seq-studio orm generate ${dir} --migration-export and commit it`);
|
|
8121
|
+
return 1;
|
|
8122
|
+
}
|
|
8123
|
+
console.log(`${ORM_MIGRATION_EXPORT_FILE} matches the current definitions, operations, and committed migration`);
|
|
8124
|
+
return 0;
|
|
8125
|
+
}
|
|
8126
|
+
await promises.writeFile(path$1, text4);
|
|
8127
|
+
console.log(`wrote ${ORM_MIGRATION_EXPORT_FILE} (${exported.checksum.slice(0, 12)})`);
|
|
8128
|
+
}
|
|
7522
8129
|
await writeGenerateOutputs(dir, compiled.project);
|
|
7523
8130
|
const { loaded, manifest } = compiled.project;
|
|
7524
8131
|
console.log(
|
|
@@ -7569,6 +8176,80 @@ async function runConsumerCodegen(dir) {
|
|
|
7569
8176
|
console.log(`ran consumer graphql-codegen (${CODEGEN_CONFIG_FILE})`);
|
|
7570
8177
|
return 0;
|
|
7571
8178
|
}
|
|
8179
|
+
function isVariablesObject(value) {
|
|
8180
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8181
|
+
}
|
|
8182
|
+
function parseExecuteArgs(rest) {
|
|
8183
|
+
const args = [...rest];
|
|
8184
|
+
const variablesIndex = args.indexOf("--variables-file");
|
|
8185
|
+
if (variablesIndex === -1) {
|
|
8186
|
+
throw new OrmDefinitionError("execute requires --variables-file <file>");
|
|
8187
|
+
}
|
|
8188
|
+
const variablesFile = args[variablesIndex + 1];
|
|
8189
|
+
if (!variablesFile) {
|
|
8190
|
+
throw new OrmDefinitionError("execute requires --variables-file <file>");
|
|
8191
|
+
}
|
|
8192
|
+
args.splice(variablesIndex, 2);
|
|
8193
|
+
const envIndex = args.indexOf("--env");
|
|
8194
|
+
if (envIndex !== -1) args.splice(envIndex, 2);
|
|
8195
|
+
if (args.some((arg) => arg.startsWith("-"))) {
|
|
8196
|
+
throw new OrmDefinitionError("execute accepts only <operation>, [dir], --variables-file, and --env");
|
|
8197
|
+
}
|
|
8198
|
+
const operation = args[0];
|
|
8199
|
+
if (!operation || args.length > 2) {
|
|
8200
|
+
throw new OrmDefinitionError("usage: seq-studio orm execute <operation> [dir] --variables-file <file> -e <env>");
|
|
8201
|
+
}
|
|
8202
|
+
return {
|
|
8203
|
+
operation,
|
|
8204
|
+
dir: args[1] ?? ".",
|
|
8205
|
+
variablesFile
|
|
8206
|
+
};
|
|
8207
|
+
}
|
|
8208
|
+
async function readExecuteVariables(variablesFile) {
|
|
8209
|
+
let parsed;
|
|
8210
|
+
try {
|
|
8211
|
+
parsed = JSON.parse(await promises.readFile(path.resolve(variablesFile), "utf8"));
|
|
8212
|
+
} catch (error) {
|
|
8213
|
+
throw new OrmDefinitionError(
|
|
8214
|
+
`could not read JSON variables from ${variablesFile}: ${error instanceof Error ? error.message : String(error)}`
|
|
8215
|
+
);
|
|
8216
|
+
}
|
|
8217
|
+
if (!isVariablesObject(parsed)) {
|
|
8218
|
+
throw new OrmDefinitionError("--variables-file must contain a JSON object");
|
|
8219
|
+
}
|
|
8220
|
+
return parsed;
|
|
8221
|
+
}
|
|
8222
|
+
async function executeV2Command(rest) {
|
|
8223
|
+
const { dir, operation, variablesFile } = parseExecuteArgs(rest);
|
|
8224
|
+
if (await detectProjectFormat(dir) !== "v2") {
|
|
8225
|
+
throw new OrmDefinitionError("execute supports TypeScript ORM v2 namespaces only");
|
|
8226
|
+
}
|
|
8227
|
+
const loaded = await loadV2(dir);
|
|
8228
|
+
const manifestPath = path.join(dir, OPERATIONS_MANIFEST_FILE);
|
|
8229
|
+
let manifest;
|
|
8230
|
+
try {
|
|
8231
|
+
manifest = operationsManifestSchema.parse(JSON.parse(await promises.readFile(manifestPath, "utf8")));
|
|
8232
|
+
} catch (error) {
|
|
8233
|
+
throw new OrmDefinitionError(
|
|
8234
|
+
`could not read ${OPERATIONS_MANIFEST_FILE}: ${error instanceof Error ? error.message : String(error)} \u2014 run \`seq-studio orm generate ${dir}\` first`
|
|
8235
|
+
);
|
|
8236
|
+
}
|
|
8237
|
+
const selected = manifest.operations[operation];
|
|
8238
|
+
if (!selected) {
|
|
8239
|
+
throw new OrmDefinitionError(
|
|
8240
|
+
`operation "${operation}" is not in ${OPERATIONS_MANIFEST_FILE}; run \`seq-studio orm generate ${dir}\` after editing GraphQL documents`
|
|
8241
|
+
);
|
|
8242
|
+
}
|
|
8243
|
+
const variables = await readExecuteVariables(variablesFile);
|
|
8244
|
+
const executePrefix = process.env["SEQUENCE_ORM_ATLAS_EXECUTE_V2"] === "1" ? "/api/data/v2" : "/_data/v2";
|
|
8245
|
+
const response = await apiRequestStrict(
|
|
8246
|
+
"POST",
|
|
8247
|
+
`${executePrefix}/${loaded.canonical.namespace}`,
|
|
8248
|
+
{ op: selected.hash, variables }
|
|
8249
|
+
);
|
|
8250
|
+
console.log(JSON.stringify(response, null, 2));
|
|
8251
|
+
return 0;
|
|
8252
|
+
}
|
|
7572
8253
|
async function validateV2Command(dir) {
|
|
7573
8254
|
const compiled = await compileV2Project(dir);
|
|
7574
8255
|
printDiagnostics(compiled.ok ? compiled.project.diagnostics : compiled.diagnostics);
|
|
@@ -8078,6 +8759,7 @@ function columnProperty(table, column, singlePrimaryKey) {
|
|
|
8078
8759
|
}
|
|
8079
8760
|
chain += `.default(${JSON.stringify(column.default)})`;
|
|
8080
8761
|
}
|
|
8762
|
+
if (column.onUpdateFn !== void 0) chain += `.onUpdate(${quote(column.onUpdateFn)})`;
|
|
8081
8763
|
if (column.description !== void 0) chain += `.description(${quote(column.description)})`;
|
|
8082
8764
|
if (column.renamedFrom !== void 0) chain += `.renamedFrom(${quote(column.renamedFrom)})`;
|
|
8083
8765
|
if (column.cast !== void 0) chain += `.cast(${quote(column.cast)})`;
|