@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.js
CHANGED
|
@@ -6,8 +6,8 @@ import { createJiti } from 'jiti';
|
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { sql } from 'drizzle-orm';
|
|
9
|
-
import { pgSchema, PgDialect, QueryBuilder, getViewConfig, extractUsedTable, primaryKey, uniqueIndex, index,
|
|
10
|
-
import { buildSchema, parse, validate, Kind, valueFromASTUntyped, print } from 'graphql';
|
|
9
|
+
import { pgSchema, PgDialect, QueryBuilder, getViewConfig, extractUsedTable, foreignKey, primaryKey, uniqueIndex, index, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
|
|
10
|
+
import { buildSchema, parse, validate, Kind, valueFromASTUntyped, visit, print } from 'graphql';
|
|
11
11
|
import { generateDrizzleJson, upPgSnapshot, generateMigration } from 'drizzle-kit/api';
|
|
12
12
|
import { load } from 'js-yaml';
|
|
13
13
|
import { capabilityPinsSchema, customRolesBlockSchema, validateCapabilityPins } from '@sequenceholdings/custom-roles-manifest';
|
|
@@ -36,9 +36,34 @@ var PredicateParseError = class extends Error {
|
|
|
36
36
|
}
|
|
37
37
|
name = "PredicateParseError";
|
|
38
38
|
};
|
|
39
|
+
|
|
40
|
+
// src/constants.ts
|
|
41
|
+
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
42
|
+
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
43
|
+
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
44
|
+
var MAX_IDENTIFIER_LENGTH = 63;
|
|
45
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
|
|
46
|
+
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
47
|
+
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
48
|
+
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
49
|
+
var ORM_SCHEMA_PREFIX = "orm_";
|
|
50
|
+
var CONSUMER_ROLE_SUFFIX = "_consumer";
|
|
51
|
+
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
52
|
+
var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
|
|
53
|
+
var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
54
|
+
var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
|
|
55
|
+
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
56
|
+
var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
|
|
57
|
+
var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
|
|
58
|
+
var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
|
|
59
|
+
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
60
|
+
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
61
|
+
|
|
62
|
+
// src/canonical/v2.ts
|
|
39
63
|
var CANONICAL_FORMAT = 2;
|
|
40
64
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
41
65
|
var identifier = z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
66
|
+
var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
42
67
|
var COLUMN_TYPES = [
|
|
43
68
|
"uuid",
|
|
44
69
|
"text",
|
|
@@ -59,18 +84,33 @@ var canonicalEnumSchema = z.object({
|
|
|
59
84
|
var canonicalColumnSchema = z.object({
|
|
60
85
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
61
86
|
name: z.string().min(1).max(63),
|
|
62
|
-
columnName:
|
|
87
|
+
columnName: columnIdentifier,
|
|
63
88
|
type: z.union([columnTypeSchema, z.object({ enum: identifier })]),
|
|
64
89
|
nullable: z.boolean().default(true),
|
|
65
90
|
unique: z.boolean().default(false),
|
|
66
91
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
67
92
|
default: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
|
|
68
93
|
defaultFn: z.enum(["gen_random_uuid()", "now()"]).optional(),
|
|
94
|
+
/**
|
|
95
|
+
* Stamp this column from the database clock on UPDATE (`now()`). The
|
|
96
|
+
* column stays insertable (defaults fill gaps) but is omitted from
|
|
97
|
+
* SetInput / settable so callers cannot backdate it.
|
|
98
|
+
*/
|
|
99
|
+
onUpdateFn: z.enum(["now()"]).optional(),
|
|
69
100
|
description: z.string().max(500).optional(),
|
|
70
101
|
/** Migration hints — same semantics as v1. */
|
|
71
|
-
renamedFrom:
|
|
102
|
+
renamedFrom: columnIdentifier.optional(),
|
|
72
103
|
cast: z.string().max(200).optional(),
|
|
73
104
|
deprecated: z.boolean().default(false)
|
|
105
|
+
}).superRefine((column, ctx) => {
|
|
106
|
+
if (column.onUpdateFn === void 0) return;
|
|
107
|
+
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
108
|
+
ctx.addIssue({
|
|
109
|
+
code: "custom",
|
|
110
|
+
path: ["onUpdateFn"],
|
|
111
|
+
message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
|
|
112
|
+
});
|
|
113
|
+
}
|
|
74
114
|
});
|
|
75
115
|
var canonicalLinkSchema = z.object({
|
|
76
116
|
name: z.string().min(1).max(63),
|
|
@@ -78,15 +118,15 @@ var canonicalLinkSchema = z.object({
|
|
|
78
118
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
79
119
|
target: identifier.optional(),
|
|
80
120
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
81
|
-
on:
|
|
121
|
+
on: columnIdentifier,
|
|
82
122
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
83
|
-
references:
|
|
123
|
+
references: columnIdentifier.optional(),
|
|
84
124
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
85
125
|
enforce: z.boolean().default(false),
|
|
86
126
|
description: z.string().max(500).optional()
|
|
87
127
|
});
|
|
88
128
|
var canonicalIndexSchema = z.object({
|
|
89
|
-
columns: z.array(
|
|
129
|
+
columns: z.array(columnIdentifier).min(1),
|
|
90
130
|
unique: z.boolean().default(false),
|
|
91
131
|
name: identifier.optional()
|
|
92
132
|
});
|
|
@@ -334,6 +374,13 @@ var canonicalCowMutationSchema = z.object({
|
|
|
334
374
|
}
|
|
335
375
|
}
|
|
336
376
|
});
|
|
377
|
+
var canonicalForeignKeySchema = z.object({
|
|
378
|
+
name: identifier,
|
|
379
|
+
columns: z.array(columnIdentifier).min(1).max(8),
|
|
380
|
+
target: identifier,
|
|
381
|
+
references: z.array(columnIdentifier).min(1).max(8),
|
|
382
|
+
deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional()
|
|
383
|
+
}).strict();
|
|
337
384
|
var canonicalTableSchema = z.object({
|
|
338
385
|
table: identifier,
|
|
339
386
|
description: z.string().max(2e3).optional(),
|
|
@@ -342,7 +389,7 @@ var canonicalTableSchema = z.object({
|
|
|
342
389
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
343
390
|
source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
344
391
|
columns: z.array(canonicalColumnSchema).default([]),
|
|
345
|
-
primaryKey: z.array(
|
|
392
|
+
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
346
393
|
links: z.array(canonicalLinkSchema).default([]),
|
|
347
394
|
indexes: z.array(canonicalIndexSchema).default([]),
|
|
348
395
|
policies: canonicalPoliciesSchema.default({}),
|
|
@@ -353,14 +400,16 @@ var canonicalTableSchema = z.object({
|
|
|
353
400
|
*/
|
|
354
401
|
actions: z.array(canonicalActionSchema).default([]),
|
|
355
402
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
356
|
-
cowMutations: z.array(canonicalCowMutationSchema).optional()
|
|
403
|
+
cowMutations: z.array(canonicalCowMutationSchema).optional(),
|
|
404
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
405
|
+
foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional()
|
|
357
406
|
});
|
|
358
407
|
var canonicalViewSchema = z.object({
|
|
359
408
|
view: identifier,
|
|
360
409
|
description: z.string().max(2e3).optional(),
|
|
361
410
|
columns: z.array(canonicalColumnSchema).min(1),
|
|
362
411
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
363
|
-
primaryKey: z.array(
|
|
412
|
+
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
364
413
|
links: z.array(canonicalLinkSchema).default([]),
|
|
365
414
|
policies: z.object({ read: policyExpressionSchema }).strict(),
|
|
366
415
|
query: z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
|
|
@@ -373,6 +422,9 @@ var canonicalViewSchema = z.object({
|
|
|
373
422
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
374
423
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
|
|
375
424
|
}
|
|
425
|
+
if (column.onUpdateFn !== void 0) {
|
|
426
|
+
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare onUpdateFn` });
|
|
427
|
+
}
|
|
376
428
|
if (column.unique) {
|
|
377
429
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
|
|
378
430
|
}
|
|
@@ -406,6 +458,45 @@ var canonicalDefinitionSchema = z.object({
|
|
|
406
458
|
ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
|
|
407
459
|
}
|
|
408
460
|
seen.add(table.table);
|
|
461
|
+
if (table.source !== void 0) {
|
|
462
|
+
for (const [columnIndex, column] of table.columns.entries()) {
|
|
463
|
+
if (column.onUpdateFn !== void 0) {
|
|
464
|
+
ctx.addIssue({
|
|
465
|
+
code: "custom",
|
|
466
|
+
path: ["tables", index3, "columns", columnIndex],
|
|
467
|
+
message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
474
|
+
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
475
|
+
const names = /* @__PURE__ */ new Set([
|
|
476
|
+
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
477
|
+
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
478
|
+
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
479
|
+
]);
|
|
480
|
+
for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
|
|
481
|
+
const fail2 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
|
|
482
|
+
if (names.has(fk.name)) fail2(`duplicate foreign key '${fk.name}'`);
|
|
483
|
+
names.add(fk.name);
|
|
484
|
+
const target = fkTables.get(fk.target);
|
|
485
|
+
if (table.source || !target || target.source) {
|
|
486
|
+
fail2("foreign keys must connect owned tables in the same namespace");
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (fk.columns.length !== fk.references.length) fail2("foreign key columns and references must have the same number of columns");
|
|
490
|
+
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail2("foreign key columns must not repeat");
|
|
491
|
+
for (const [position, name] of fk.columns.entries()) {
|
|
492
|
+
const sourceColumn = table.columns.find((column) => column.columnName === name);
|
|
493
|
+
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
494
|
+
if (!sourceColumn || !targetColumn) fail2("foreign key references an unknown column");
|
|
495
|
+
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail2("foreign key column types must match");
|
|
496
|
+
}
|
|
497
|
+
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index4) => index4.unique).map((index4) => index4.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
498
|
+
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");
|
|
499
|
+
}
|
|
409
500
|
}
|
|
410
501
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
411
502
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
@@ -501,7 +592,7 @@ var canonicalDefinitionSchema = z.object({
|
|
|
501
592
|
const visiting = /* @__PURE__ */ new Set();
|
|
502
593
|
const visited = /* @__PURE__ */ new Set();
|
|
503
594
|
let cycleReported = false;
|
|
504
|
-
const
|
|
595
|
+
const visit2 = (name, path) => {
|
|
505
596
|
if (visited.has(name) || cycleReported) return;
|
|
506
597
|
if (visiting.has(name)) {
|
|
507
598
|
cycleReported = true;
|
|
@@ -515,12 +606,12 @@ var canonicalDefinitionSchema = z.object({
|
|
|
515
606
|
visiting.add(name);
|
|
516
607
|
const view = viewByName.get(name);
|
|
517
608
|
for (const dependency of view?.dependencies ?? []) {
|
|
518
|
-
if (viewByName.has(dependency))
|
|
609
|
+
if (viewByName.has(dependency)) visit2(dependency, [...path, name]);
|
|
519
610
|
}
|
|
520
611
|
visiting.delete(name);
|
|
521
612
|
visited.add(name);
|
|
522
613
|
};
|
|
523
|
-
for (const view of views)
|
|
614
|
+
for (const view of views) visit2(view.view, []);
|
|
524
615
|
});
|
|
525
616
|
function canonicalRelations(definition) {
|
|
526
617
|
const views = (definition.views ?? []).map((view) => ({
|
|
@@ -541,12 +632,13 @@ function normalizeCanonical(definition) {
|
|
|
541
632
|
...definition,
|
|
542
633
|
enums: sortByName(definition.enums),
|
|
543
634
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
|
|
544
|
-
const { cowMutations, ...rest } = t2;
|
|
635
|
+
const { cowMutations, foreignKeys, ...rest } = t2;
|
|
545
636
|
return {
|
|
546
637
|
...rest,
|
|
547
638
|
links: sortByName(t2.links),
|
|
548
639
|
indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
549
640
|
actions: sortByName(t2.actions),
|
|
641
|
+
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
550
642
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
551
643
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
552
644
|
};
|
|
@@ -561,25 +653,6 @@ function normalizeCanonical(definition) {
|
|
|
561
653
|
}
|
|
562
654
|
return normalized;
|
|
563
655
|
}
|
|
564
|
-
|
|
565
|
-
// src/constants.ts
|
|
566
|
-
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
567
|
-
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
568
|
-
var MAX_IDENTIFIER_LENGTH = 63;
|
|
569
|
-
var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
|
|
570
|
-
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
571
|
-
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
572
|
-
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
573
|
-
var ORM_SCHEMA_PREFIX = "orm_";
|
|
574
|
-
var CONSUMER_ROLE_SUFFIX = "_consumer";
|
|
575
|
-
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
576
|
-
var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
|
|
577
|
-
var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
578
|
-
var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
|
|
579
|
-
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
580
|
-
var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
|
|
581
|
-
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
582
|
-
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
583
656
|
function stableJson(value) {
|
|
584
657
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
585
658
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
@@ -1005,6 +1078,22 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1005
1078
|
} else if (columnLevelPk.length > 0) {
|
|
1006
1079
|
candidate.primaryKey = columnLevelPk;
|
|
1007
1080
|
}
|
|
1081
|
+
if (table.config.foreignKeys?.length) {
|
|
1082
|
+
if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
|
|
1083
|
+
candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
|
|
1084
|
+
const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
|
|
1085
|
+
throw new OrmDefinitionError(message, table.file);
|
|
1086
|
+
} });
|
|
1087
|
+
if (!isSeqTable(target) || !relationsByName.has(target.tableName)) throw new OrmDefinitionError("foreign key target must be a table in this namespace", table.file);
|
|
1088
|
+
return {
|
|
1089
|
+
name: fk.name,
|
|
1090
|
+
columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
|
|
1091
|
+
target: target.tableName,
|
|
1092
|
+
references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
|
|
1093
|
+
...fk.deferrable ? { deferrable: fk.deferrable } : {}
|
|
1094
|
+
};
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1008
1097
|
for (const index3 of table.config.indexes ?? []) {
|
|
1009
1098
|
const entry = {
|
|
1010
1099
|
columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
|
|
@@ -1057,6 +1146,12 @@ function buildCanonicalView({
|
|
|
1057
1146
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1058
1147
|
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
|
|
1059
1148
|
}
|
|
1149
|
+
if (column.onUpdateFn !== void 0) {
|
|
1150
|
+
throw new OrmDefinitionError(
|
|
1151
|
+
`view "${view.viewName}": column "${propertyName}" cannot declare onUpdateFn`,
|
|
1152
|
+
view.file
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1060
1155
|
if (column.unique) {
|
|
1061
1156
|
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
|
|
1062
1157
|
}
|
|
@@ -1219,7 +1314,16 @@ async function loadNamespaceFromDir(dir, options = {}) {
|
|
|
1219
1314
|
const jiti = createJiti(import.meta.url, {
|
|
1220
1315
|
interopDefault: true,
|
|
1221
1316
|
fsCache: false,
|
|
1222
|
-
...options.ormModulePath !== void 0 ? {
|
|
1317
|
+
...options.ormModulePath !== void 0 ? {
|
|
1318
|
+
alias: {
|
|
1319
|
+
// The public /drizzle barrel re-exports this pinned dependency.
|
|
1320
|
+
// Explicitly map it before the root alias: otherwise jiti appends
|
|
1321
|
+
// "/drizzle" to the overridden module's filename.
|
|
1322
|
+
"@sequenceholdings/orm/drizzle": createRequire(import.meta.url).resolve("drizzle-orm"),
|
|
1323
|
+
"@sequenceholdings/orm": options.ormModulePath,
|
|
1324
|
+
"@sequenceholdings/orm/authoring": options.ormModulePath
|
|
1325
|
+
}
|
|
1326
|
+
} : {}
|
|
1223
1327
|
});
|
|
1224
1328
|
const context = beginAuthoringContext();
|
|
1225
1329
|
try {
|
|
@@ -1310,7 +1414,7 @@ var manifestOperationSchema = z.object({
|
|
|
1310
1414
|
kind: z.enum(["query", "mutation"]),
|
|
1311
1415
|
requires: manifestRequiresSchema
|
|
1312
1416
|
});
|
|
1313
|
-
z.object({
|
|
1417
|
+
var operationsManifestSchema = z.object({
|
|
1314
1418
|
manifestVersion: z.literal(MANIFEST_VERSION),
|
|
1315
1419
|
schemaHash: z.string().min(1),
|
|
1316
1420
|
opsHash: z.string().regex(OP_HASH_RE),
|
|
@@ -1429,6 +1533,7 @@ var ORDER_BY_ENUM_VALUES = [
|
|
|
1429
1533
|
"DESC_NULLS_LAST"
|
|
1430
1534
|
];
|
|
1431
1535
|
var PLATFORM_USER_TYPENAME = "PlatformUser";
|
|
1536
|
+
var DATE_BUCKET_ENUM = "DateBucket";
|
|
1432
1537
|
function tableNames(table) {
|
|
1433
1538
|
const root = camelCase(table);
|
|
1434
1539
|
const rootPascal = pascalCase(table);
|
|
@@ -1461,7 +1566,12 @@ function tableNames(table) {
|
|
|
1461
1566
|
sumFields: `${type}SumFields`,
|
|
1462
1567
|
avgFields: `${type}AvgFields`,
|
|
1463
1568
|
aggregateBoolExp: `${type}AggregateBoolExp`,
|
|
1464
|
-
aggregateBoolExpCount: `${type}AggregateBoolExpCount
|
|
1569
|
+
aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
|
|
1570
|
+
aggregateGroupsRoot: `${root}AggregateGroups`,
|
|
1571
|
+
groupByColumnEnum: `${type}GroupByColumn`,
|
|
1572
|
+
groupByInput: `${type}GroupBy`,
|
|
1573
|
+
groupKey: `${type}GroupKey`,
|
|
1574
|
+
aggregateGroup: `${type}AggregateGroup`
|
|
1465
1575
|
};
|
|
1466
1576
|
}
|
|
1467
1577
|
|
|
@@ -1494,7 +1604,18 @@ ${body}
|
|
|
1494
1604
|
}
|
|
1495
1605
|
`;
|
|
1496
1606
|
}
|
|
1497
|
-
var IR_VERSION =
|
|
1607
|
+
var IR_VERSION = 2;
|
|
1608
|
+
var LEGACY_IR_VERSION = 1;
|
|
1609
|
+
var irVersionSchema = z.union([z.literal(LEGACY_IR_VERSION), z.literal(IR_VERSION)]);
|
|
1610
|
+
var operationVersionSchema = z.object({ irVersion: irVersionSchema });
|
|
1611
|
+
function requiredArtifactIrVersion(operations) {
|
|
1612
|
+
let required = LEGACY_IR_VERSION;
|
|
1613
|
+
for (const operation of operations) {
|
|
1614
|
+
const { irVersion } = operationVersionSchema.parse(operation);
|
|
1615
|
+
if (irVersion > required) required = irVersion;
|
|
1616
|
+
}
|
|
1617
|
+
return required;
|
|
1618
|
+
}
|
|
1498
1619
|
var CLAIMS_CONTRACT = {
|
|
1499
1620
|
sub: "string",
|
|
1500
1621
|
org: "string?",
|
|
@@ -1621,6 +1742,33 @@ var aggregateRootSchema = z.object({
|
|
|
1621
1742
|
nodes: selectionIrSchema.optional(),
|
|
1622
1743
|
includeIf: includeIfSchema.optional()
|
|
1623
1744
|
});
|
|
1745
|
+
var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
|
|
1746
|
+
var groupByKeyIrSchema = z.object({
|
|
1747
|
+
column: z.string(),
|
|
1748
|
+
bucket: z.enum(DATE_BUCKETS).optional()
|
|
1749
|
+
});
|
|
1750
|
+
var aggregateGroupsRootSchema = z.object({
|
|
1751
|
+
kind: z.literal("aggregateGroups"),
|
|
1752
|
+
alias: z.string(),
|
|
1753
|
+
table: z.string(),
|
|
1754
|
+
/** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
|
|
1755
|
+
where: predIrSchema,
|
|
1756
|
+
groupBy: z.array(groupByKeyIrSchema).min(1),
|
|
1757
|
+
/**
|
|
1758
|
+
* The `key { … }` projection: which group columns appear in each entry's key
|
|
1759
|
+
* object, in selection order. A subset of `groupBy` (the compiler rejects a
|
|
1760
|
+
* key field that is not a group key — it would be indeterminate within the
|
|
1761
|
+
* group). Empty when the document selects only `aggregate`.
|
|
1762
|
+
*/
|
|
1763
|
+
key: z.array(z.object({ alias: z.string(), column: z.string() })).default([]),
|
|
1764
|
+
count: z.object({ columns: z.array(z.string()).optional(), distinct: z.boolean().optional() }).optional(),
|
|
1765
|
+
sum: z.array(z.string()).optional(),
|
|
1766
|
+
avg: z.array(z.string()).optional(),
|
|
1767
|
+
/** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
|
|
1768
|
+
limit: limitIrSchema,
|
|
1769
|
+
offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
|
|
1770
|
+
includeIf: includeIfSchema.optional()
|
|
1771
|
+
});
|
|
1624
1772
|
var writeValuesSchema = z.union([
|
|
1625
1773
|
z.object({ slot: z.string() }),
|
|
1626
1774
|
z.object({ entries: z.array(z.object({ column: z.string(), value: valueIrSchema })) })
|
|
@@ -1666,12 +1814,15 @@ var mutationRootSchema = z.object({
|
|
|
1666
1814
|
insertCheck: predIrSchema.optional(),
|
|
1667
1815
|
returning: selectionIrSchema.nullable(),
|
|
1668
1816
|
affectedRows: z.boolean().default(false),
|
|
1817
|
+
/** v2: abort the transaction when the actual write count differs, regardless of selection. */
|
|
1818
|
+
assertAffectedRows: z.number().int().min(0).max(2147483647).optional(),
|
|
1669
1819
|
includeIf: includeIfSchema.optional()
|
|
1670
1820
|
});
|
|
1671
1821
|
var rootIrSchema = z.discriminatedUnion("kind", [
|
|
1672
1822
|
selectRootSchema,
|
|
1673
1823
|
byPkRootSchema,
|
|
1674
1824
|
aggregateRootSchema,
|
|
1825
|
+
aggregateGroupsRootSchema,
|
|
1675
1826
|
mutationRootSchema
|
|
1676
1827
|
]);
|
|
1677
1828
|
var INPUT_REF_KINDS = [
|
|
@@ -1701,7 +1852,7 @@ var variableIrSchema = z.object({
|
|
|
1701
1852
|
defaultValue: variableDefaultIrSchema.optional()
|
|
1702
1853
|
});
|
|
1703
1854
|
var compiledOperationSchema = z.object({
|
|
1704
|
-
irVersion:
|
|
1855
|
+
irVersion: irVersionSchema,
|
|
1705
1856
|
name: z.string(),
|
|
1706
1857
|
opHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
1707
1858
|
kind: z.enum(["query", "mutation"]),
|
|
@@ -1713,6 +1864,13 @@ var compiledOperationSchema = z.object({
|
|
|
1713
1864
|
/** Every table the op touches — data-token scoping + audit. */
|
|
1714
1865
|
tableSet: z.array(z.string()).min(1),
|
|
1715
1866
|
roots: z.array(rootIrSchema).min(1)
|
|
1867
|
+
}).superRefine((operation, ctx) => {
|
|
1868
|
+
for (const [index3, root] of operation.roots.entries()) {
|
|
1869
|
+
if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
|
|
1870
|
+
if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
|
|
1871
|
+
ctx.addIssue({ code: "custom", path: ["roots", index3], message: "affected-row assertions require unconditional mutation roots and IR v2" });
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1716
1874
|
});
|
|
1717
1875
|
var tableMetaIrSchema = z.object({
|
|
1718
1876
|
table: z.string(),
|
|
@@ -1749,18 +1907,29 @@ var tableMetaIrSchema = z.object({
|
|
|
1749
1907
|
insertable: z.array(z.string()),
|
|
1750
1908
|
settable: z.array(z.string()),
|
|
1751
1909
|
appendable: z.array(z.string()),
|
|
1910
|
+
/**
|
|
1911
|
+
* Logical columns stamped with Postgres `now()` on UPDATE (canonical
|
|
1912
|
+
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
1913
|
+
*/
|
|
1914
|
+
touchOnUpdate: z.array(z.string()).optional(),
|
|
1752
1915
|
constraints: z.array(z.object({ name: z.string(), columns: z.array(z.string()) })),
|
|
1753
1916
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
1754
1917
|
dynamicFilterMaxDepth: z.number().int().positive().default(6),
|
|
1755
1918
|
dynamicFilterMaxNodes: z.number().int().positive().default(64)
|
|
1756
1919
|
});
|
|
1757
1920
|
var compiledArtifactSchema = z.object({
|
|
1758
|
-
irVersion:
|
|
1921
|
+
irVersion: irVersionSchema,
|
|
1759
1922
|
namespace: z.string(),
|
|
1760
1923
|
schemaHash: z.string(),
|
|
1761
1924
|
opsHash: z.string(),
|
|
1762
1925
|
tables: z.record(z.string(), tableMetaIrSchema),
|
|
1763
1926
|
operations: z.record(z.string(), compiledOperationSchema)
|
|
1927
|
+
}).superRefine((artifact, ctx) => {
|
|
1928
|
+
for (const [name, operation] of Object.entries(artifact.operations)) {
|
|
1929
|
+
if (operation.irVersion > artifact.irVersion) {
|
|
1930
|
+
ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1764
1933
|
});
|
|
1765
1934
|
var IrVersionError = class extends Error {
|
|
1766
1935
|
constructor(found) {
|
|
@@ -1771,19 +1940,19 @@ var IrVersionError = class extends Error {
|
|
|
1771
1940
|
function normalizeIr(raw) {
|
|
1772
1941
|
if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
|
|
1773
1942
|
const version = raw.irVersion;
|
|
1774
|
-
if (version !== IR_VERSION) throw new IrVersionError(version);
|
|
1943
|
+
if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
|
|
1775
1944
|
return compiledArtifactSchema.parse(raw);
|
|
1776
1945
|
}
|
|
1777
1946
|
|
|
1778
1947
|
// src/predicate-v2/ast.ts
|
|
1779
1948
|
var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
1780
|
-
function walkPolicy(node,
|
|
1781
|
-
|
|
1949
|
+
function walkPolicy(node, visit2) {
|
|
1950
|
+
visit2(node);
|
|
1782
1951
|
if (node.kind === "and" || node.kind === "or") {
|
|
1783
|
-
for (const operand of node.operands) walkPolicy(operand,
|
|
1952
|
+
for (const operand of node.operands) walkPolicy(operand, visit2);
|
|
1784
1953
|
}
|
|
1785
|
-
if (node.kind === "not") walkPolicy(node.operand,
|
|
1786
|
-
if (node.kind === "exists") walkPolicy(node.where,
|
|
1954
|
+
if (node.kind === "not") walkPolicy(node.operand, visit2);
|
|
1955
|
+
if (node.kind === "exists") walkPolicy(node.where, visit2);
|
|
1787
1956
|
}
|
|
1788
1957
|
|
|
1789
1958
|
// src/predicate-v2/errors.ts
|
|
@@ -1921,6 +2090,10 @@ var Parser = class {
|
|
|
1921
2090
|
this.expect("rparen", "')'");
|
|
1922
2091
|
return inner;
|
|
1923
2092
|
}
|
|
2093
|
+
if (this.peekKeyword("true") || this.peekKeyword("false")) {
|
|
2094
|
+
const token = this.advance();
|
|
2095
|
+
return { kind: "literal", value: token.value === "true" };
|
|
2096
|
+
}
|
|
1924
2097
|
if (this.peekKeyword("capability") || this.peekKeyword("role")) {
|
|
1925
2098
|
const fn = this.peek().value;
|
|
1926
2099
|
const node = this.parseBooleanCall(fn);
|
|
@@ -2104,6 +2277,8 @@ var PolicyCompiler = class {
|
|
|
2104
2277
|
}
|
|
2105
2278
|
compile(node, table, depth) {
|
|
2106
2279
|
switch (node.kind) {
|
|
2280
|
+
case "literal":
|
|
2281
|
+
return { kind: "const", value: node.value };
|
|
2107
2282
|
case "and":
|
|
2108
2283
|
case "or":
|
|
2109
2284
|
return { kind: node.kind, operands: node.operands.map((operand) => this.compile(operand, table, depth)) };
|
|
@@ -2283,21 +2458,21 @@ function andPred(...preds) {
|
|
|
2283
2458
|
if (operands.length === 1) return operands[0];
|
|
2284
2459
|
return { kind: "and", operands };
|
|
2285
2460
|
}
|
|
2286
|
-
function walkPred(pred,
|
|
2287
|
-
|
|
2461
|
+
function walkPred(pred, visit2) {
|
|
2462
|
+
visit2(pred);
|
|
2288
2463
|
switch (pred.kind) {
|
|
2289
2464
|
case "and":
|
|
2290
2465
|
case "or":
|
|
2291
|
-
for (const operand of pred.operands) walkPred(operand,
|
|
2466
|
+
for (const operand of pred.operands) walkPred(operand, visit2);
|
|
2292
2467
|
return;
|
|
2293
2468
|
case "not":
|
|
2294
|
-
walkPred(pred.operand,
|
|
2469
|
+
walkPred(pred.operand, visit2);
|
|
2295
2470
|
return;
|
|
2296
2471
|
case "exists":
|
|
2297
|
-
walkPred(pred.where,
|
|
2472
|
+
walkPred(pred.where, visit2);
|
|
2298
2473
|
return;
|
|
2299
2474
|
case "aggPredicate":
|
|
2300
|
-
if (pred.where) walkPred(pred.where,
|
|
2475
|
+
if (pred.where) walkPred(pred.where, visit2);
|
|
2301
2476
|
return;
|
|
2302
2477
|
default:
|
|
2303
2478
|
return;
|
|
@@ -2314,12 +2489,12 @@ function predContainsExists(pred) {
|
|
|
2314
2489
|
// src/compiler-gql/inject.ts
|
|
2315
2490
|
function collectPinnedColumns(writePolicy) {
|
|
2316
2491
|
const key = (pin) => `${pin.column}\0${pin.claim}`;
|
|
2317
|
-
const
|
|
2492
|
+
const visit2 = (node) => {
|
|
2318
2493
|
if (node.kind === "and") {
|
|
2319
2494
|
const merged = [];
|
|
2320
2495
|
const seen2 = /* @__PURE__ */ new Set();
|
|
2321
2496
|
for (const operand of node.operands) {
|
|
2322
|
-
for (const pin of
|
|
2497
|
+
for (const pin of visit2(operand)) {
|
|
2323
2498
|
if (seen2.has(key(pin))) continue;
|
|
2324
2499
|
seen2.add(key(pin));
|
|
2325
2500
|
merged.push(pin);
|
|
@@ -2330,9 +2505,9 @@ function collectPinnedColumns(writePolicy) {
|
|
|
2330
2505
|
if (node.kind === "or") {
|
|
2331
2506
|
const [first, ...rest] = node.operands;
|
|
2332
2507
|
if (first === void 0) return [];
|
|
2333
|
-
let common =
|
|
2508
|
+
let common = visit2(first);
|
|
2334
2509
|
for (const operand of rest) {
|
|
2335
|
-
const branch = new Set(
|
|
2510
|
+
const branch = new Set(visit2(operand).map(key));
|
|
2336
2511
|
common = common.filter((pin) => branch.has(key(pin)));
|
|
2337
2512
|
if (common.length === 0) break;
|
|
2338
2513
|
}
|
|
@@ -2344,7 +2519,7 @@ function collectPinnedColumns(writePolicy) {
|
|
|
2344
2519
|
return [];
|
|
2345
2520
|
};
|
|
2346
2521
|
const seen = /* @__PURE__ */ new Set();
|
|
2347
|
-
return
|
|
2522
|
+
return visit2(writePolicy).filter((pin) => {
|
|
2348
2523
|
if (seen.has(pin.column)) return false;
|
|
2349
2524
|
seen.add(pin.column);
|
|
2350
2525
|
return true;
|
|
@@ -2438,6 +2613,11 @@ var PolicyInjector = class {
|
|
|
2438
2613
|
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table)),
|
|
2439
2614
|
...root.nodes ? { nodes: this.injectSelection(root.nodes, root.table) } : {}
|
|
2440
2615
|
};
|
|
2616
|
+
case "aggregateGroups":
|
|
2617
|
+
return {
|
|
2618
|
+
...root,
|
|
2619
|
+
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table))
|
|
2620
|
+
};
|
|
2441
2621
|
case "mutation":
|
|
2442
2622
|
return this.injectMutation(root);
|
|
2443
2623
|
}
|
|
@@ -2530,20 +2710,33 @@ function collectTableSet(roots, ctx) {
|
|
|
2530
2710
|
};
|
|
2531
2711
|
for (const root of roots) {
|
|
2532
2712
|
tables.add(root.table);
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2713
|
+
switch (root.kind) {
|
|
2714
|
+
case "select":
|
|
2715
|
+
case "byPk":
|
|
2716
|
+
visitPred(root.where, root.table);
|
|
2717
|
+
visitSelection(root.selection, root.table);
|
|
2718
|
+
break;
|
|
2719
|
+
case "aggregate":
|
|
2720
|
+
visitPred(root.where, root.table);
|
|
2721
|
+
if (root.nodes) visitSelection(root.nodes, root.table);
|
|
2722
|
+
break;
|
|
2723
|
+
case "aggregateGroups":
|
|
2724
|
+
visitPred(root.where, root.table);
|
|
2725
|
+
break;
|
|
2726
|
+
case "mutation":
|
|
2727
|
+
if (root.cowSynced !== void 0) tables.add(root.cowSynced);
|
|
2728
|
+
if (root.where) visitPred(root.where, root.table);
|
|
2729
|
+
if (root.insertCheck) visitPred(root.insertCheck, root.table);
|
|
2730
|
+
if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
|
|
2731
|
+
if (root.returning) visitSelection(root.returning, root.table);
|
|
2732
|
+
break;
|
|
2733
|
+
default: {
|
|
2734
|
+
const exhausted = root;
|
|
2735
|
+
throw new DocumentCompileError(
|
|
2736
|
+
`internal: unhandled root kind in tableSet collection ${JSON.stringify(exhausted)}`,
|
|
2737
|
+
"internal"
|
|
2738
|
+
);
|
|
2739
|
+
}
|
|
2547
2740
|
}
|
|
2548
2741
|
}
|
|
2549
2742
|
return [...tables].sort();
|
|
@@ -2561,7 +2754,12 @@ function typeNameForTable(table) {
|
|
|
2561
2754
|
}
|
|
2562
2755
|
function queryRootNames(table) {
|
|
2563
2756
|
const names = tableNames(table);
|
|
2564
|
-
return {
|
|
2757
|
+
return {
|
|
2758
|
+
select: names.selectRoot,
|
|
2759
|
+
byPk: names.byPkRoot,
|
|
2760
|
+
aggregate: names.aggregateRoot,
|
|
2761
|
+
aggregateGroups: names.aggregateGroupsRoot
|
|
2762
|
+
};
|
|
2565
2763
|
}
|
|
2566
2764
|
function mutationRootNames(table) {
|
|
2567
2765
|
const names = tableNames(table);
|
|
@@ -2642,7 +2840,14 @@ function resolveColumnEntries(table, enums, diagnostics) {
|
|
|
2642
2840
|
type = { enum: enumDef.name, values: [...enumDef.values] };
|
|
2643
2841
|
}
|
|
2644
2842
|
}
|
|
2645
|
-
return {
|
|
2843
|
+
return {
|
|
2844
|
+
name: column.name,
|
|
2845
|
+
columnName: column.columnName,
|
|
2846
|
+
type,
|
|
2847
|
+
nullable: column.nullable,
|
|
2848
|
+
deprecated: column.deprecated,
|
|
2849
|
+
onUpdateFn: column.onUpdateFn !== void 0
|
|
2850
|
+
};
|
|
2646
2851
|
});
|
|
2647
2852
|
}
|
|
2648
2853
|
function logicalColumn(entries, ref) {
|
|
@@ -2691,6 +2896,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2691
2896
|
primaryKey2.push(resolved);
|
|
2692
2897
|
}
|
|
2693
2898
|
const active = entries.filter((c) => !c.deprecated);
|
|
2899
|
+
const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
|
|
2694
2900
|
const constraints = [];
|
|
2695
2901
|
if (primaryKey2.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey2] });
|
|
2696
2902
|
for (const column of table.columns) {
|
|
@@ -2711,14 +2917,15 @@ function buildCompileContext(rawCanonical) {
|
|
|
2711
2917
|
// Synced twins (and views reading them, resolved transitively below)
|
|
2712
2918
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
2713
2919
|
claimsGated: relationIsClaimsGated(table.table),
|
|
2714
|
-
columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
|
|
2920
|
+
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
2715
2921
|
primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
2716
2922
|
links: [],
|
|
2717
2923
|
filterable: entries.map((c) => c.name),
|
|
2718
2924
|
orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
2719
2925
|
insertable: isView ? [] : active.map((c) => c.name),
|
|
2720
|
-
settable: isView ? [] : active.map((c) => c.name),
|
|
2926
|
+
settable: isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
2721
2927
|
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
2928
|
+
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
2722
2929
|
constraints: isView ? [] : constraints,
|
|
2723
2930
|
dynamicFilterMaxDepth: 6,
|
|
2724
2931
|
dynamicFilterMaxNodes: 64
|
|
@@ -2739,7 +2946,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2739
2946
|
});
|
|
2740
2947
|
continue;
|
|
2741
2948
|
}
|
|
2742
|
-
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false })) : columnEntries.get(targetName);
|
|
2949
|
+
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
|
|
2743
2950
|
const onEntries = link.kind === "toMany" ? targetEntries : entries;
|
|
2744
2951
|
const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
|
|
2745
2952
|
const on = logicalColumn(onEntries, link.on);
|
|
@@ -2836,6 +3043,9 @@ function buildCompileContext(rawCanonical) {
|
|
|
2836
3043
|
queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
|
|
2837
3044
|
}
|
|
2838
3045
|
queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
|
|
3046
|
+
if (table.columns.some((column) => column.type !== "jsonb")) {
|
|
3047
|
+
queryRoots.set(roots.aggregateGroups, { kind: "aggregateGroups", table: table.table });
|
|
3048
|
+
}
|
|
2839
3049
|
const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
2840
3050
|
if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
|
|
2841
3051
|
for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
|
|
@@ -2971,6 +3181,16 @@ function computeOpsHash(opHashes) {
|
|
|
2971
3181
|
}
|
|
2972
3182
|
var DEFAULT_LIMIT = 100;
|
|
2973
3183
|
var MAX_LIMIT = 1e3;
|
|
3184
|
+
var MAX_GROUPS = 1e3;
|
|
3185
|
+
var DATE_BUCKET_BY_ENUM = {
|
|
3186
|
+
YEAR: "year",
|
|
3187
|
+
QUARTER: "quarter",
|
|
3188
|
+
MONTH: "month",
|
|
3189
|
+
WEEK: "week",
|
|
3190
|
+
DAY: "day",
|
|
3191
|
+
HOUR: "hour"
|
|
3192
|
+
};
|
|
3193
|
+
var BUCKETABLE_TYPES = /* @__PURE__ */ new Set(["timestamptz", "date"]);
|
|
2974
3194
|
var BOOLEXP_OPS = {
|
|
2975
3195
|
_eq: "eq",
|
|
2976
3196
|
_neq: "neq",
|
|
@@ -3002,6 +3222,14 @@ function fail(message, code, candidates = []) {
|
|
|
3002
3222
|
function argsByName(field) {
|
|
3003
3223
|
return new Map((field.arguments ?? []).map((arg) => [arg.name.value, arg]));
|
|
3004
3224
|
}
|
|
3225
|
+
function* aggregateBlockFields(selectionSet) {
|
|
3226
|
+
for (const node of selectionSet.selections) {
|
|
3227
|
+
if (node.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
3228
|
+
if (node.name.value === "__typename") continue;
|
|
3229
|
+
if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
3230
|
+
yield node;
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3005
3233
|
function assertKnownArgs(field, allowed) {
|
|
3006
3234
|
for (const arg of field.arguments ?? []) {
|
|
3007
3235
|
if (!allowed.includes(arg.name.value)) {
|
|
@@ -3302,11 +3530,8 @@ var OperationLowerer = class {
|
|
|
3302
3530
|
lowerAggregateSelection(selectionSet, table) {
|
|
3303
3531
|
const result = {};
|
|
3304
3532
|
let sawAggregate = false;
|
|
3305
|
-
for (const node of selectionSet
|
|
3306
|
-
if (node.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
3533
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
3307
3534
|
const name = node.name.value;
|
|
3308
|
-
if (name === "__typename") continue;
|
|
3309
|
-
if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
3310
3535
|
if (name === "nodes") {
|
|
3311
3536
|
if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
|
|
3312
3537
|
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
@@ -3316,48 +3541,116 @@ var OperationLowerer = class {
|
|
|
3316
3541
|
if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
3317
3542
|
sawAggregate = true;
|
|
3318
3543
|
if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3544
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
3545
|
+
}
|
|
3546
|
+
if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
3547
|
+
return result;
|
|
3548
|
+
}
|
|
3549
|
+
/**
|
|
3550
|
+
* The `{ key {…} aggregate {…} }` block of a grouped aggregate root. Same
|
|
3551
|
+
* `aggregate` sub-block as the ungrouped shape (shared via
|
|
3552
|
+
* `lowerAggregateFields`), but `key` replaces `nodes` — see the IR comment on
|
|
3553
|
+
* `aggregateGroupsRootSchema` for why grouped roots have no `nodes`.
|
|
3554
|
+
*/
|
|
3555
|
+
lowerGroupedAggregateSelection(selectionSet, table, groupBy) {
|
|
3556
|
+
const groupColumns = new Set(groupBy.map((entry) => entry.column));
|
|
3557
|
+
const result = { key: [] };
|
|
3558
|
+
let sawAggregate = false;
|
|
3559
|
+
let sawKey = false;
|
|
3560
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
3561
|
+
const name = node.name.value;
|
|
3562
|
+
if (name === "key") {
|
|
3563
|
+
if (sawKey) fail("one key block per grouped aggregate", "invalid-aggregate");
|
|
3564
|
+
sawKey = true;
|
|
3565
|
+
if (!node.selectionSet) fail("'key' needs a selection set", "invalid-aggregate");
|
|
3566
|
+
result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
|
|
3567
|
+
continue;
|
|
3568
|
+
}
|
|
3569
|
+
if (name === "nodes") {
|
|
3570
|
+
fail("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
|
|
3571
|
+
}
|
|
3572
|
+
if (name !== "aggregate") fail(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
|
|
3573
|
+
if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
3574
|
+
sawAggregate = true;
|
|
3575
|
+
if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
|
|
3576
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
3577
|
+
}
|
|
3578
|
+
const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
|
|
3579
|
+
if (result.key.length === 0 && !rendersAggregate) {
|
|
3580
|
+
fail(
|
|
3581
|
+
"grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
|
|
3582
|
+
"invalid-aggregate",
|
|
3583
|
+
["key", "aggregate"]
|
|
3584
|
+
);
|
|
3585
|
+
}
|
|
3586
|
+
return result;
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* `key { month officer }` → the projected group columns. Every field must be
|
|
3590
|
+
* one of the query's own group keys: any other column is indeterminate
|
|
3591
|
+
* within a group, so selecting it is a document error rather than an
|
|
3592
|
+
* arbitrary pick.
|
|
3593
|
+
*/
|
|
3594
|
+
lowerGroupKeySelection(selectionSet, table, groupColumns) {
|
|
3595
|
+
const key = [];
|
|
3596
|
+
for (const node of selectionSet.selections) {
|
|
3597
|
+
if (node.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
3598
|
+
const name = node.name.value;
|
|
3599
|
+
if (name === "__typename") continue;
|
|
3600
|
+
if (!this.column(table, name)) {
|
|
3601
|
+
fail(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
|
|
3602
|
+
}
|
|
3603
|
+
if (!groupColumns.has(name)) {
|
|
3604
|
+
fail(
|
|
3605
|
+
`key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
|
|
3606
|
+
"invalid-group-key",
|
|
3607
|
+
[...groupColumns]
|
|
3608
|
+
);
|
|
3609
|
+
}
|
|
3610
|
+
key.push({ alias: node.alias?.value ?? name, column: name });
|
|
3611
|
+
}
|
|
3612
|
+
return key;
|
|
3613
|
+
}
|
|
3614
|
+
/** The `count` / `sum {…}` / `avg {…}` fields inside an `aggregate { … }` block. */
|
|
3615
|
+
lowerAggregateFields(selectionSet, table) {
|
|
3616
|
+
const result = {};
|
|
3617
|
+
for (const aggField of aggregateBlockFields(selectionSet)) {
|
|
3618
|
+
const aggName = aggField.name.value;
|
|
3619
|
+
if (aggName === "count") {
|
|
3620
|
+
assertKnownArgs(aggField, ["columns", "distinct"]);
|
|
3621
|
+
const args = argsByName(aggField);
|
|
3622
|
+
const count = {};
|
|
3623
|
+
const columns = args.get("columns");
|
|
3624
|
+
if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
|
|
3625
|
+
const distinct = args.get("distinct");
|
|
3626
|
+
if (distinct) {
|
|
3627
|
+
if (distinct.value.kind !== Kind.BOOLEAN) fail("count(distinct:) must be a boolean literal", "invalid-aggregate");
|
|
3628
|
+
count.distinct = distinct.value.value;
|
|
3337
3629
|
}
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3630
|
+
result.count = count;
|
|
3631
|
+
continue;
|
|
3632
|
+
}
|
|
3633
|
+
if (aggName === "sum" || aggName === "avg") {
|
|
3634
|
+
if (!aggField.selectionSet) fail(`'${aggName}' needs a column selection`, "invalid-aggregate");
|
|
3635
|
+
const columns = [];
|
|
3636
|
+
for (const columnField of aggField.selectionSet.selections) {
|
|
3637
|
+
if (columnField.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
3638
|
+
if (columnField.name.value === "__typename") continue;
|
|
3639
|
+
const column = this.column(table, columnField.name.value);
|
|
3640
|
+
if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
|
|
3641
|
+
fail(
|
|
3642
|
+
`'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
|
|
3643
|
+
"invalid-aggregate",
|
|
3644
|
+
this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
|
|
3645
|
+
);
|
|
3353
3646
|
}
|
|
3354
|
-
|
|
3355
|
-
continue;
|
|
3647
|
+
columns.push(columnField.name.value);
|
|
3356
3648
|
}
|
|
3357
|
-
|
|
3649
|
+
result[aggName] = columns;
|
|
3650
|
+
continue;
|
|
3358
3651
|
}
|
|
3652
|
+
fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
|
|
3359
3653
|
}
|
|
3360
|
-
if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
3361
3654
|
return result;
|
|
3362
3655
|
}
|
|
3363
3656
|
// -------------------------------------------------------------------------
|
|
@@ -3367,19 +3660,49 @@ var OperationLowerer = class {
|
|
|
3367
3660
|
const roots = [];
|
|
3368
3661
|
for (const node of operation.selectionSet.selections) {
|
|
3369
3662
|
if (node.kind !== Kind.FIELD) fail("fragments are not supported at the operation root", "invalid-root");
|
|
3663
|
+
if (node.selectionSet) visit(node.selectionSet, {
|
|
3664
|
+
Directive(directive) {
|
|
3665
|
+
if (directive.name.value === "assertAffectedRows") {
|
|
3666
|
+
fail("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
});
|
|
3670
|
+
const assertion = this.parseAffectedRowAssertion(node, kind);
|
|
3370
3671
|
const gate = this.parseRootDirectives(node);
|
|
3371
3672
|
if (gate.omit) continue;
|
|
3372
3673
|
const root = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
|
|
3373
|
-
|
|
3674
|
+
const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
|
|
3675
|
+
roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
|
|
3374
3676
|
}
|
|
3375
3677
|
if (roots.length === 0) fail("operation has no roots (all statically skipped?)", "invalid-root");
|
|
3376
3678
|
return roots;
|
|
3377
3679
|
}
|
|
3680
|
+
parseAffectedRowAssertion(node, kind) {
|
|
3681
|
+
const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
|
|
3682
|
+
if (!assertions.length) return void 0;
|
|
3683
|
+
if (kind !== "mutation" || assertions.length !== 1) {
|
|
3684
|
+
fail("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
|
|
3685
|
+
}
|
|
3686
|
+
if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
|
|
3687
|
+
fail("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
|
|
3688
|
+
}
|
|
3689
|
+
const args = assertions[0].arguments;
|
|
3690
|
+
const arg = args?.[0];
|
|
3691
|
+
if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== Kind.INT) {
|
|
3692
|
+
fail("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
|
|
3693
|
+
}
|
|
3694
|
+
const count = Number(arg.value.value);
|
|
3695
|
+
if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
|
|
3696
|
+
fail("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
|
|
3697
|
+
}
|
|
3698
|
+
return count;
|
|
3699
|
+
}
|
|
3378
3700
|
parseRootDirectives(node) {
|
|
3379
3701
|
let includeIf;
|
|
3380
3702
|
let omit = false;
|
|
3381
3703
|
for (const directive of node.directives ?? []) {
|
|
3382
3704
|
const name = directive.name.value;
|
|
3705
|
+
if (name === "assertAffectedRows") continue;
|
|
3383
3706
|
if (name !== "include" && name !== "skip") {
|
|
3384
3707
|
fail(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
|
|
3385
3708
|
}
|
|
@@ -3428,6 +3751,9 @@ var OperationLowerer = class {
|
|
|
3428
3751
|
const pk = this.lowerPkArgs(node, table);
|
|
3429
3752
|
return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
|
|
3430
3753
|
}
|
|
3754
|
+
if (rootInfo.kind === "aggregateGroups") {
|
|
3755
|
+
return this.lowerAggregateGroupsRoot(node, { alias, table });
|
|
3756
|
+
}
|
|
3431
3757
|
assertKnownArgs(node, ["where"]);
|
|
3432
3758
|
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
3433
3759
|
const whereArg = args.get("where");
|
|
@@ -3440,6 +3766,108 @@ var OperationLowerer = class {
|
|
|
3440
3766
|
...aggregate
|
|
3441
3767
|
};
|
|
3442
3768
|
}
|
|
3769
|
+
/** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
|
|
3770
|
+
lowerAggregateGroupsRoot(node, { alias, table }) {
|
|
3771
|
+
assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
|
|
3772
|
+
if (!node.selectionSet) fail(`'${node.name.value}' needs a selection set`, "invalid-selection");
|
|
3773
|
+
const args = argsByName(node);
|
|
3774
|
+
const groupByArg = args.get("groupBy");
|
|
3775
|
+
if (!groupByArg) fail(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
|
|
3776
|
+
const whereArg = args.get("where");
|
|
3777
|
+
const groupBy = this.lowerGroupBy(groupByArg.value, table);
|
|
3778
|
+
const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
|
|
3779
|
+
const offset = this.lowerOffset(args.get("offset")?.value);
|
|
3780
|
+
return {
|
|
3781
|
+
kind: "aggregateGroups",
|
|
3782
|
+
alias,
|
|
3783
|
+
table,
|
|
3784
|
+
where: whereArg ? this.lowerBoolExp(whereArg.value, table) : CONST_TRUE,
|
|
3785
|
+
groupBy,
|
|
3786
|
+
key: aggregate.key,
|
|
3787
|
+
...aggregate.count ? { count: aggregate.count } : {},
|
|
3788
|
+
...aggregate.sum ? { sum: aggregate.sum } : {},
|
|
3789
|
+
...aggregate.avg ? { avg: aggregate.avg } : {},
|
|
3790
|
+
limit: this.lowerGroupLimit(args.get("limit")?.value),
|
|
3791
|
+
...offset ? { offset } : {}
|
|
3792
|
+
};
|
|
3793
|
+
}
|
|
3794
|
+
/**
|
|
3795
|
+
* `groupBy: [{ column: startedAt, bucket: MONTH }, …]` → GroupByKeyIr[].
|
|
3796
|
+
*
|
|
3797
|
+
* Document-authored only: a whole-argument variable is rejected rather than
|
|
3798
|
+
* becoming a §4f slot. Group keys choose which expressions reach GROUP BY,
|
|
3799
|
+
* and dynamic-input.ts refuses aggregation shapes from client input on the
|
|
3800
|
+
* same reasoning — `where` stays dynamic, which is what runtime filtering
|
|
3801
|
+
* actually needs.
|
|
3802
|
+
*/
|
|
3803
|
+
lowerGroupBy(node, table) {
|
|
3804
|
+
if (node.kind === Kind.VARIABLE) {
|
|
3805
|
+
fail(
|
|
3806
|
+
`groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
|
|
3807
|
+
"dynamic-group-by"
|
|
3808
|
+
);
|
|
3809
|
+
}
|
|
3810
|
+
const items = node.kind === Kind.LIST ? node.values : [node];
|
|
3811
|
+
if (items.length === 0) fail(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
|
|
3812
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3813
|
+
return items.map((item) => {
|
|
3814
|
+
if (item.kind !== Kind.OBJECT) {
|
|
3815
|
+
fail(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
|
|
3816
|
+
}
|
|
3817
|
+
const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
|
|
3818
|
+
for (const key of fields.keys()) {
|
|
3819
|
+
if (key !== "column" && key !== "bucket") {
|
|
3820
|
+
fail(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
const columnNode = fields.get("column");
|
|
3824
|
+
if (!columnNode) fail(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
|
|
3825
|
+
if (columnNode.kind !== Kind.ENUM) {
|
|
3826
|
+
fail(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
|
|
3827
|
+
}
|
|
3828
|
+
const column = this.column(table, columnNode.value);
|
|
3829
|
+
if (!column) {
|
|
3830
|
+
fail(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
3831
|
+
}
|
|
3832
|
+
if (column.type === "jsonb") {
|
|
3833
|
+
fail(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
|
|
3834
|
+
}
|
|
3835
|
+
const bucketNode = fields.get("bucket");
|
|
3836
|
+
let bucket;
|
|
3837
|
+
if (bucketNode) {
|
|
3838
|
+
if (bucketNode.kind !== Kind.ENUM) {
|
|
3839
|
+
fail(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
3840
|
+
}
|
|
3841
|
+
bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
|
|
3842
|
+
if (!bucket) {
|
|
3843
|
+
fail(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
3844
|
+
}
|
|
3845
|
+
if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
|
|
3846
|
+
fail(
|
|
3847
|
+
`groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
|
|
3848
|
+
"invalid-group-by"
|
|
3849
|
+
);
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
if (seen.has(columnNode.value)) {
|
|
3853
|
+
fail(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
|
|
3854
|
+
}
|
|
3855
|
+
seen.add(columnNode.value);
|
|
3856
|
+
return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
|
|
3857
|
+
});
|
|
3858
|
+
}
|
|
3859
|
+
/** Like lowerLimit, but bounded by MAX_GROUPS and defaulting to it. */
|
|
3860
|
+
lowerGroupLimit(node) {
|
|
3861
|
+
const base = { default: MAX_GROUPS, max: MAX_GROUPS };
|
|
3862
|
+
if (!node) return base;
|
|
3863
|
+
if (node.kind === Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
3864
|
+
if (node.kind === Kind.INT) {
|
|
3865
|
+
const value = Number(node.value);
|
|
3866
|
+
if (value < 1) fail("limit must be a positive integer", "invalid-limit");
|
|
3867
|
+
return { ...base, value: Math.min(value, MAX_GROUPS) };
|
|
3868
|
+
}
|
|
3869
|
+
fail("limit must be an integer literal or a variable", "invalid-limit");
|
|
3870
|
+
}
|
|
3443
3871
|
/** byPk-style pk arguments: one argument per primary-key column, exactly. */
|
|
3444
3872
|
lowerPkArgs(node, table) {
|
|
3445
3873
|
const meta = this.meta(table);
|
|
@@ -3949,7 +4377,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
3949
4377
|
const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
|
|
3950
4378
|
const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
|
|
3951
4379
|
operations[restricted.name] = {
|
|
3952
|
-
irVersion: IR_VERSION,
|
|
4380
|
+
irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? IR_VERSION : LEGACY_IR_VERSION,
|
|
3953
4381
|
name: restricted.name,
|
|
3954
4382
|
opHash: computeOpHash({ definition: flattened, schemaHash }),
|
|
3955
4383
|
kind: restricted.kind,
|
|
@@ -3976,7 +4404,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
3976
4404
|
}
|
|
3977
4405
|
if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
|
|
3978
4406
|
const artifact = {
|
|
3979
|
-
irVersion:
|
|
4407
|
+
irVersion: requiredArtifactIrVersion(Object.values(operations)),
|
|
3980
4408
|
namespace: ctx.namespace,
|
|
3981
4409
|
schemaHash,
|
|
3982
4410
|
opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
|
|
@@ -4176,12 +4604,12 @@ function castParts(cast) {
|
|
|
4176
4604
|
|
|
4177
4605
|
// src/predicate/ast.ts
|
|
4178
4606
|
var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
4179
|
-
function walkPredicate(predicate,
|
|
4180
|
-
|
|
4607
|
+
function walkPredicate(predicate, visit2) {
|
|
4608
|
+
visit2(predicate);
|
|
4181
4609
|
if (predicate.kind === "and" || predicate.kind === "or") {
|
|
4182
|
-
for (const operand of predicate.operands) walkPredicate(operand,
|
|
4610
|
+
for (const operand of predicate.operands) walkPredicate(operand, visit2);
|
|
4183
4611
|
}
|
|
4184
|
-
if (predicate.kind === "not") walkPredicate(predicate.operand,
|
|
4612
|
+
if (predicate.kind === "not") walkPredicate(predicate.operand, visit2);
|
|
4185
4613
|
}
|
|
4186
4614
|
function predicateOperands(predicate) {
|
|
4187
4615
|
const operands = [];
|
|
@@ -5156,7 +5584,7 @@ var FACTORIES2 = {
|
|
|
5156
5584
|
var SCHEMA_IMPORT_KEY2 = "$schema";
|
|
5157
5585
|
var enumImportKey = (name) => `$enum_${name}`;
|
|
5158
5586
|
var columnTablesV2 = (definition) => definition.tables.filter((table) => !table.source);
|
|
5159
|
-
function buildDrizzleEntitiesV2(definition) {
|
|
5587
|
+
function buildDrizzleEntitiesV2(definition, options = {}) {
|
|
5160
5588
|
const schemaName = ormSchemaName(definition.namespace);
|
|
5161
5589
|
const schema = pgSchema(schemaName);
|
|
5162
5590
|
const imports = { [SCHEMA_IMPORT_KEY2]: schema };
|
|
@@ -5171,11 +5599,19 @@ function buildDrizzleEntitiesV2(definition) {
|
|
|
5171
5599
|
}
|
|
5172
5600
|
const byName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
5173
5601
|
for (const table of sortByEnforcedLinks2(columnTablesV2(definition))) {
|
|
5174
|
-
const drizzleTable = buildTable2(schema, table, built, enums, byName);
|
|
5602
|
+
const drizzleTable = buildTable2(schema, table, built, enums, byName, options.introspection ?? false);
|
|
5175
5603
|
built.set(table.table, drizzleTable);
|
|
5176
5604
|
imports[table.table] = drizzleTable;
|
|
5177
5605
|
tables[table.table] = drizzleTable;
|
|
5178
5606
|
}
|
|
5607
|
+
if (options.introspection) {
|
|
5608
|
+
for (const table of definition.tables.filter((table2) => table2.source)) {
|
|
5609
|
+
const columns = Object.fromEntries(
|
|
5610
|
+
table.columns.map((column) => [column.columnName, buildViewColumnV2({ view: table.table, column, enums })])
|
|
5611
|
+
);
|
|
5612
|
+
imports[table.table] = schema.view(table.table, columns).existing();
|
|
5613
|
+
}
|
|
5614
|
+
}
|
|
5179
5615
|
for (const view of definition.views ?? []) {
|
|
5180
5616
|
const columns = Object.fromEntries(
|
|
5181
5617
|
view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
|
|
@@ -5419,9 +5855,10 @@ function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, b
|
|
|
5419
5855
|
function generateSnapshotV2(definition, previousId3) {
|
|
5420
5856
|
const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
|
|
5421
5857
|
const snapshot = generateDrizzleJson(imports, previousId3, [schemaName]);
|
|
5422
|
-
if ((definition.views?.length ?? 0) > 0) {
|
|
5858
|
+
if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
|
|
5423
5859
|
const extended = snapshot;
|
|
5424
5860
|
extended._sequenceOrm = {
|
|
5861
|
+
...definition.tables.some((table) => table.foreignKeys?.length) ? { foreignKeys: Object.fromEntries(definition.tables.flatMap((table) => (table.foreignKeys ?? []).map((fk) => [`${table.table}.${fk.name}`, structuredClone(fk)]))) } : {},
|
|
5425
5862
|
viewDependencies: Object.fromEntries(
|
|
5426
5863
|
(definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
|
|
5427
5864
|
)
|
|
@@ -5439,7 +5876,7 @@ function buildViewColumnV2({
|
|
|
5439
5876
|
if (!column.nullable) builder = builder.notNull();
|
|
5440
5877
|
return builder;
|
|
5441
5878
|
}
|
|
5442
|
-
function buildTable2(schema, table, built, enums, byName) {
|
|
5879
|
+
function buildTable2(schema, table, built, enums, byName, introspection) {
|
|
5443
5880
|
const primaryKeyColumns2 = table.primaryKey ?? [];
|
|
5444
5881
|
if (primaryKeyColumns2.length === 0) {
|
|
5445
5882
|
throw new OrmDefinitionError(`table '${table.table}' declares no primaryKey`);
|
|
@@ -5455,13 +5892,22 @@ function buildTable2(schema, table, built, enums, byName) {
|
|
|
5455
5892
|
const columns = Object.fromEntries(
|
|
5456
5893
|
table.columns.map((column) => [
|
|
5457
5894
|
column.columnName,
|
|
5458
|
-
buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums })
|
|
5895
|
+
buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums, introspection })
|
|
5459
5896
|
])
|
|
5460
5897
|
);
|
|
5461
5898
|
const drizzleTable = schema.table(table.table, columns, (self) => [
|
|
5462
5899
|
...buildCompositePrimaryKey(table, primaryKeyColumns2, self),
|
|
5463
5900
|
...buildIndexes2(table, self),
|
|
5464
|
-
...buildForeignKeys2(table, self, built, byName)
|
|
5901
|
+
...buildForeignKeys2(table, self, built, byName),
|
|
5902
|
+
...(table.foreignKeys ?? []).map((fk) => {
|
|
5903
|
+
const target = fk.target === table.table ? self : built.get(fk.target);
|
|
5904
|
+
if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
|
|
5905
|
+
return foreignKey({
|
|
5906
|
+
name: fk.name,
|
|
5907
|
+
columns: fk.columns.map((column) => self[column]),
|
|
5908
|
+
foreignColumns: fk.references.map((column) => target[column])
|
|
5909
|
+
});
|
|
5910
|
+
})
|
|
5465
5911
|
]);
|
|
5466
5912
|
return drizzleTable.enableRLS();
|
|
5467
5913
|
}
|
|
@@ -5478,7 +5924,8 @@ function buildColumnV2({
|
|
|
5478
5924
|
column,
|
|
5479
5925
|
singlePrimaryKey,
|
|
5480
5926
|
compositeMembers,
|
|
5481
|
-
enums
|
|
5927
|
+
enums,
|
|
5928
|
+
introspection
|
|
5482
5929
|
}) {
|
|
5483
5930
|
const factory = resolveColumnFactory(table, column, enums);
|
|
5484
5931
|
let builder = factory(column.columnName);
|
|
@@ -5499,7 +5946,7 @@ function buildColumnV2({
|
|
|
5499
5946
|
}
|
|
5500
5947
|
builder = builder.default(sql.raw(column.defaultFn));
|
|
5501
5948
|
} else if (column.default !== void 0 && column.default !== null) {
|
|
5502
|
-
builder = builder.default(column.default);
|
|
5949
|
+
builder = introspection && column.type === "jsonb" && typeof column.default === "string" ? builder.default(sql.raw(`${quoteLiteral(column.default)}::jsonb`)) : builder.default(column.default);
|
|
5503
5950
|
}
|
|
5504
5951
|
return builder;
|
|
5505
5952
|
}
|
|
@@ -5684,7 +6131,8 @@ async function planMigrationV2({
|
|
|
5684
6131
|
assertValidViewDependencies(canonical);
|
|
5685
6132
|
const hints = applyMigrationHintsV2(previous, canonical);
|
|
5686
6133
|
const snapshot = generateSnapshotV2(canonical, previousId(previous));
|
|
5687
|
-
const
|
|
6134
|
+
const generatedBaseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
|
|
6135
|
+
const baseDdl = orderForeignKeyAdditions(canonical, generatedBaseDdl);
|
|
5688
6136
|
const previousViews = snapshotViews(hints.adjusted);
|
|
5689
6137
|
const currentViews = snapshotViews(snapshot);
|
|
5690
6138
|
const previousDependencies = snapshotViewDependencies(hints.adjusted);
|
|
@@ -5721,6 +6169,7 @@ async function planMigrationV2({
|
|
|
5721
6169
|
...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
|
|
5722
6170
|
...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
|
|
5723
6171
|
...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
|
|
6172
|
+
...foreignKeyTimingStatements(canonical, previous, baseDdl).map((sql4) => ({ kind: "ddl", sql: sql4 })),
|
|
5724
6173
|
...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
|
|
5725
6174
|
...createOrder.map((name) => {
|
|
5726
6175
|
const view = currentViewDefinitions.get(name);
|
|
@@ -5730,6 +6179,29 @@ async function planMigrationV2({
|
|
|
5730
6179
|
];
|
|
5731
6180
|
return { namespace: canonical.namespace, schema, statements, snapshot };
|
|
5732
6181
|
}
|
|
6182
|
+
function orderForeignKeyAdditions(canonical, statements) {
|
|
6183
|
+
if (!canonical.tables.some((table) => table.foreignKeys?.length)) return [...statements];
|
|
6184
|
+
const addsForeignKey = (statement) => /^ALTER TABLE[\s\S]* ADD CONSTRAINT[\s\S]* FOREIGN KEY\b/.test(statement);
|
|
6185
|
+
return [...statements.filter((statement) => !addsForeignKey(statement)), ...statements.filter(addsForeignKey)];
|
|
6186
|
+
}
|
|
6187
|
+
function foreignKeyTimingStatements(canonical, previous, baseDdl) {
|
|
6188
|
+
const prior = previous._sequenceOrm?.foreignKeys ?? {};
|
|
6189
|
+
const result = [];
|
|
6190
|
+
for (const table of canonical.tables) {
|
|
6191
|
+
for (const fk of table.foreignKeys ?? []) {
|
|
6192
|
+
const before = prior[`${table.table}.${fk.name}`];
|
|
6193
|
+
const recreated = baseDdl.some((statement) => statement.includes(`ALTER TABLE ${quoteIdentifier(ormSchemaName(canonical.namespace))}.${quoteIdentifier(table.table)} ADD CONSTRAINT ${quoteIdentifier(fk.name)}`));
|
|
6194
|
+
if (fk.deferrable ? !before || before.deferrable !== fk.deferrable || recreated : before?.deferrable && !recreated) {
|
|
6195
|
+
result.push(foreignKeyTimingSql(ormSchemaName(canonical.namespace), table.table, fk.name, fk.deferrable));
|
|
6196
|
+
}
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
return result;
|
|
6200
|
+
}
|
|
6201
|
+
function foreignKeyTimingSql(schema, table, name, timing) {
|
|
6202
|
+
const clause = timing === void 0 ? "NOT DEFERRABLE" : timing === "initially-deferred" ? "DEFERRABLE INITIALLY DEFERRED" : "DEFERRABLE INITIALLY IMMEDIATE";
|
|
6203
|
+
return `ALTER TABLE ${quoteIdentifier(schema)}.${quoteIdentifier(table)} ALTER CONSTRAINT ${quoteIdentifier(name)} ${clause}`;
|
|
6204
|
+
}
|
|
5733
6205
|
function validateViewCatalogStatement({
|
|
5734
6206
|
canonical,
|
|
5735
6207
|
view,
|
|
@@ -6850,8 +7322,12 @@ function generateSdl(canonical, options = {}) {
|
|
|
6850
7322
|
'"""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'
|
|
6851
7323
|
);
|
|
6852
7324
|
blocks.push("directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT");
|
|
7325
|
+
blocks.push(
|
|
7326
|
+
'"""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'
|
|
7327
|
+
);
|
|
6853
7328
|
for (const scalar2 of CUSTOM_SCALARS) blocks.push(`scalar ${scalar2}`);
|
|
6854
7329
|
blocks.push(renderEnum(ORDER_BY_ENUM, [...ORDER_BY_ENUM_VALUES]));
|
|
7330
|
+
blocks.push(renderEnum(DATE_BUCKET_ENUM, DATE_BUCKETS.map((bucket) => bucket.toUpperCase())));
|
|
6855
7331
|
for (const enumName of [...usedEnums].sort()) {
|
|
6856
7332
|
const entry = enumByName.get(enumName);
|
|
6857
7333
|
if (!entry) continue;
|
|
@@ -6881,9 +7357,24 @@ function generateSdl(canonical, options = {}) {
|
|
|
6881
7357
|
blocks.push(renderQueryRoot(tables, namesByTable));
|
|
6882
7358
|
const mutationRoot = renderMutationRoot(tables, namesByTable);
|
|
6883
7359
|
if (mutationRoot !== null) blocks.push(mutationRoot);
|
|
7360
|
+
assertNoDuplicateTypeNames(blocks);
|
|
6884
7361
|
return `${blocks.join("\n\n")}
|
|
6885
7362
|
`;
|
|
6886
7363
|
}
|
|
7364
|
+
var TYPE_DECLARATION_RE = /^(?:type|input|enum|scalar|interface|union)\s+([_A-Za-z][_0-9A-Za-z]*)/;
|
|
7365
|
+
function assertNoDuplicateTypeNames(blocks) {
|
|
7366
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7367
|
+
for (const block of blocks) {
|
|
7368
|
+
const declared = TYPE_DECLARATION_RE.exec(block)?.[1];
|
|
7369
|
+
if (declared === void 0) continue;
|
|
7370
|
+
if (seen.has(declared)) {
|
|
7371
|
+
throw new SdlGenerationError(
|
|
7372
|
+
`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.`
|
|
7373
|
+
);
|
|
7374
|
+
}
|
|
7375
|
+
seen.add(declared);
|
|
7376
|
+
}
|
|
7377
|
+
}
|
|
6887
7378
|
function assertEnumValue(value, context) {
|
|
6888
7379
|
if (!GRAPHQL_NAME_RE2.test(value) || RESERVED_ENUM_VALUES.has(value)) {
|
|
6889
7380
|
throw new SdlGenerationError(
|
|
@@ -6972,7 +7463,8 @@ function renderTableBlocks(ctx) {
|
|
|
6972
7463
|
renderSelectColumnEnum(ctx),
|
|
6973
7464
|
renderOrderByInput(ctx),
|
|
6974
7465
|
renderBoolExp(ctx),
|
|
6975
|
-
...renderAggregateTypes(ctx)
|
|
7466
|
+
...renderAggregateTypes(ctx),
|
|
7467
|
+
...renderGroupByTypes(ctx)
|
|
6976
7468
|
];
|
|
6977
7469
|
if (ctx.emitAggregateBoolExp) blocks.push(...renderAggregateBoolExp(ctx));
|
|
6978
7470
|
const pk = primaryKeyColumns(table);
|
|
@@ -7109,6 +7601,32 @@ ${sumLines.join("\n")}
|
|
|
7109
7601
|
}
|
|
7110
7602
|
return blocks;
|
|
7111
7603
|
}
|
|
7604
|
+
function groupableColumns(table) {
|
|
7605
|
+
return table.columns.filter((column) => column.type !== "jsonb");
|
|
7606
|
+
}
|
|
7607
|
+
function renderGroupByTypes(ctx) {
|
|
7608
|
+
const { table, names } = ctx;
|
|
7609
|
+
const groupable = groupableColumns(table);
|
|
7610
|
+
if (groupable.length === 0) return [];
|
|
7611
|
+
const keyLines = groupable.map((column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`);
|
|
7612
|
+
return [
|
|
7613
|
+
renderEnum(names.groupByColumnEnum, groupable.map((column) => fieldName(column.name))),
|
|
7614
|
+
`input ${names.groupByInput} {
|
|
7615
|
+
column: ${names.groupByColumnEnum}!
|
|
7616
|
+
bucket: ${DATE_BUCKET_ENUM}
|
|
7617
|
+
}`,
|
|
7618
|
+
`type ${names.groupKey} {
|
|
7619
|
+
${keyLines.join("\n")}
|
|
7620
|
+
}`,
|
|
7621
|
+
`type ${names.aggregateGroup} {
|
|
7622
|
+
key: ${names.groupKey}!
|
|
7623
|
+
aggregate: ${names.aggregateFields}
|
|
7624
|
+
}`
|
|
7625
|
+
];
|
|
7626
|
+
}
|
|
7627
|
+
function groupRootArgs(names) {
|
|
7628
|
+
return `where: ${names.boolExp}, groupBy: [${names.groupByInput}!]!, limit: Int, offset: Int`;
|
|
7629
|
+
}
|
|
7112
7630
|
function renderAggregateBoolExp(ctx) {
|
|
7113
7631
|
const { names } = ctx;
|
|
7114
7632
|
return [
|
|
@@ -7134,6 +7652,9 @@ function constraintValues(table) {
|
|
|
7134
7652
|
}
|
|
7135
7653
|
return [...values].sort();
|
|
7136
7654
|
}
|
|
7655
|
+
function clientSettableColumns(table) {
|
|
7656
|
+
return table.columns.filter((column) => column.onUpdateFn === void 0);
|
|
7657
|
+
}
|
|
7137
7658
|
function renderWriteBlocks(ctx, access) {
|
|
7138
7659
|
const { table, names, pinned } = ctx;
|
|
7139
7660
|
const blocks = [];
|
|
@@ -7155,7 +7676,7 @@ ${insertLines.join("\n")}
|
|
|
7155
7676
|
}`);
|
|
7156
7677
|
}
|
|
7157
7678
|
if (access.update) {
|
|
7158
|
-
const setLines = table.
|
|
7679
|
+
const setLines = clientSettableColumns(table).map(
|
|
7159
7680
|
(column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
|
|
7160
7681
|
);
|
|
7161
7682
|
blocks.push(`input ${names.setInput} {
|
|
@@ -7182,7 +7703,7 @@ ${incLines.join("\n")}
|
|
|
7182
7703
|
if (access.insert && allowsOnConflict(table) && constraints.length > 0) {
|
|
7183
7704
|
for (const value of constraints) assertEnumValue(value, `constraint enum for table "${table.table}"`);
|
|
7184
7705
|
blocks.push(renderEnum(names.constraintEnum, constraints));
|
|
7185
|
-
const updateColumns = table.
|
|
7706
|
+
const updateColumns = clientSettableColumns(table).map((column) => fieldName(column.name));
|
|
7186
7707
|
blocks.push(renderEnum(names.updateColumnEnum, updateColumns));
|
|
7187
7708
|
blocks.push(
|
|
7188
7709
|
`input ${names.onConflict} {
|
|
@@ -7214,6 +7735,9 @@ function renderQueryRoot(tables, namesByTable) {
|
|
|
7214
7735
|
lines.push(` ${names.byPkRoot}(${args}): ${names.typename}`);
|
|
7215
7736
|
}
|
|
7216
7737
|
lines.push(` ${names.aggregateRoot}(${rootArgs(names)}): ${names.aggregate}!`);
|
|
7738
|
+
if (groupableColumns(table).length > 0) {
|
|
7739
|
+
lines.push(` ${names.aggregateGroupsRoot}(${groupRootArgs(names)}): [${names.aggregateGroup}!]!`);
|
|
7740
|
+
}
|
|
7217
7741
|
}
|
|
7218
7742
|
lines.push("}");
|
|
7219
7743
|
return lines.join("\n");
|
|
@@ -7282,6 +7806,40 @@ function renderCowMutationFields(table, names) {
|
|
|
7282
7806
|
}
|
|
7283
7807
|
return lines;
|
|
7284
7808
|
}
|
|
7809
|
+
var ORM_MIGRATION_EXPORT_FILE = "orm.migration.json";
|
|
7810
|
+
var ORM_MIGRATION_EXPORT_MAX_BYTES = ORM_REGISTRATION_MAX_BYTES;
|
|
7811
|
+
var hash = z.string().regex(/^sha256:[0-9a-f]{64}$/);
|
|
7812
|
+
var ormMigrationPayloadSchema = z.object({
|
|
7813
|
+
canonical_definition: canonicalDefinitionSchema,
|
|
7814
|
+
documents: z.array(z.object({ path: z.string().min(1).max(2048), contents: z.string().max(1e6) }).strict()).max(5e3),
|
|
7815
|
+
client_manifest: z.object({ ops_hash: hash, operations: z.record(z.string(), hash) }).strict(),
|
|
7816
|
+
sdl: z.string().refine((value) => Buffer.byteLength(value, "utf8") <= MAX_GENERATED_SDL_BYTES, {
|
|
7817
|
+
message: `sdl exceeds ${MAX_GENERATED_SDL_BYTES} bytes`
|
|
7818
|
+
}),
|
|
7819
|
+
schema_snapshot: z.json(),
|
|
7820
|
+
// The existing ORM committed-migration validator and replay engine remain
|
|
7821
|
+
// authoritative. This envelope hashes the provenance, never authorizes SQL.
|
|
7822
|
+
migration: z.json().optional()
|
|
7823
|
+
}).strict();
|
|
7824
|
+
z.object({
|
|
7825
|
+
schema_version: z.literal(1),
|
|
7826
|
+
schema_hash: hash,
|
|
7827
|
+
payload: ormMigrationPayloadSchema,
|
|
7828
|
+
checksum: z.string().regex(/^[0-9a-f]{64}$/)
|
|
7829
|
+
}).strict();
|
|
7830
|
+
function checksum(value) {
|
|
7831
|
+
return createHash("sha256").update(stableJson(value)).digest("hex");
|
|
7832
|
+
}
|
|
7833
|
+
function createOrmMigrationExport({ schemaHash, payload }) {
|
|
7834
|
+
const contents = { schema_version: 1, schema_hash: hash.parse(schemaHash), payload: ormMigrationPayloadSchema.parse(payload) };
|
|
7835
|
+
return { ...contents, checksum: checksum(contents) };
|
|
7836
|
+
}
|
|
7837
|
+
function renderOrmMigrationExport(exported) {
|
|
7838
|
+
const text4 = `${JSON.stringify(exported, null, 2)}
|
|
7839
|
+
`;
|
|
7840
|
+
if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
|
|
7841
|
+
return text4;
|
|
7842
|
+
}
|
|
7285
7843
|
|
|
7286
7844
|
// src/cli.ts
|
|
7287
7845
|
var BASE_URL_ENV = "SEQUENCE_ORM_BASE_URL";
|
|
@@ -7290,11 +7848,15 @@ var ORM_MODULE_PATH_ENV = "SEQUENCE_ORM_MODULE_PATH";
|
|
|
7290
7848
|
var USAGE = `usage:
|
|
7291
7849
|
seq-studio orm init <dir> scaffold a namespace package (v2: TypeScript schema + GraphQL documents)
|
|
7292
7850
|
seq-studio orm generate [dir] write schema.graphql + operations.manifest.json + typePolicies.gen.ts (+ consumer graphql-codegen when codegen.ts is present)
|
|
7851
|
+
seq-studio orm generate [dir] --migration-export [--check]
|
|
7852
|
+
export reviewed ORM inputs for a coordinated pipeline migration; --check verifies without writing
|
|
7293
7853
|
seq-studio orm validate [dir] parse + validate definitions and verify committed migrations are in sync
|
|
7294
7854
|
seq-studio orm plan [dir] -e <env> offline dry run: compile definitions and diff against the registry (no database, no writes)
|
|
7295
7855
|
seq-studio orm apply [dir] -e <env> author the migration, register, apply, and refresh the generated outputs (the everyday command)
|
|
7296
7856
|
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)
|
|
7297
7857
|
seq-studio orm apply --no-create -e <env> apply only if the namespace is already registered (CI guard; never provisions a new namespace)
|
|
7858
|
+
seq-studio orm execute <operation> [dir] --variables-file <file> -e <env>
|
|
7859
|
+
execute one named persisted operation as the signed-in caller
|
|
7298
7860
|
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)
|
|
7299
7861
|
seq-studio orm migrate-from-yaml <dir> convert a v1 YAML namespace to the v2 TypeScript format (migrations/ preserved)
|
|
7300
7862
|
|
|
@@ -7302,6 +7864,8 @@ var USAGE = `usage:
|
|
|
7302
7864
|
v2 = sequence.config.ts + schema/*.ts + graphql/**; v1 (legacy) = one <table>.yaml per table.
|
|
7303
7865
|
Environments: see \`seq-studio envs list\` (discovered after you authenticate).
|
|
7304
7866
|
Authenticate with: seq-studio login
|
|
7867
|
+
Execute endpoint rollout: SEQUENCE_ORM_ATLAS_EXECUTE_V2=1 opts into /api/data/v2;
|
|
7868
|
+
otherwise execute uses /_data/v2. Server rollout and authorization still apply.
|
|
7305
7869
|
`;
|
|
7306
7870
|
var tokenProvider = null;
|
|
7307
7871
|
function setTokenProvider(provider) {
|
|
@@ -7318,6 +7882,10 @@ async function runCli(argv) {
|
|
|
7318
7882
|
console.log(USAGE);
|
|
7319
7883
|
return command ? 0 : 1;
|
|
7320
7884
|
}
|
|
7885
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
7886
|
+
console.log(USAGE);
|
|
7887
|
+
return 0;
|
|
7888
|
+
}
|
|
7321
7889
|
try {
|
|
7322
7890
|
switch (command) {
|
|
7323
7891
|
case "init":
|
|
@@ -7355,6 +7923,8 @@ async function runCli(argv) {
|
|
|
7355
7923
|
const dir = positional(rest) ?? ".";
|
|
7356
7924
|
return await detectProjectFormat(dir) === "v2" ? await applyV2Command(rest, dir) : await applyCommand(rest);
|
|
7357
7925
|
}
|
|
7926
|
+
case "execute":
|
|
7927
|
+
return await executeV2Command(rest);
|
|
7358
7928
|
default:
|
|
7359
7929
|
console.error(`unknown command: ${command}`);
|
|
7360
7930
|
console.log(USAGE);
|
|
@@ -7365,13 +7935,14 @@ async function runCli(argv) {
|
|
|
7365
7935
|
return 1;
|
|
7366
7936
|
}
|
|
7367
7937
|
}
|
|
7938
|
+
var NON_DEFINITION_YAML_FILES = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "pnpm-workspace.yaml"]);
|
|
7368
7939
|
async function detectProjectFormat(dir) {
|
|
7369
7940
|
const entries = await readdir(dir, { withFileTypes: true }).catch(() => {
|
|
7370
7941
|
throw new OrmDefinitionError(`namespace directory not found: ${dir}`);
|
|
7371
7942
|
});
|
|
7372
7943
|
const hasConfig = entries.some((entry) => entry.isFile() && entry.name === NAMESPACE_CONFIG_FILE);
|
|
7373
7944
|
const hasYaml = entries.some(
|
|
7374
|
-
(entry) => entry.isFile() && DEFINITION_FILE_EXTENSIONS.some((extension) => entry.name.endsWith(extension))
|
|
7945
|
+
(entry) => entry.isFile() && !NON_DEFINITION_YAML_FILES.has(entry.name) && DEFINITION_FILE_EXTENSIONS.some((extension) => entry.name.endsWith(extension))
|
|
7375
7946
|
);
|
|
7376
7947
|
if (hasConfig && hasYaml) {
|
|
7377
7948
|
throw new OrmDefinitionError(
|
|
@@ -7509,6 +8080,12 @@ function printDiagnostics(diagnostics) {
|
|
|
7509
8080
|
}
|
|
7510
8081
|
async function generateCommand(rest) {
|
|
7511
8082
|
const dir = positional(rest) ?? ".";
|
|
8083
|
+
const migrationExport = rest.includes("--migration-export");
|
|
8084
|
+
const check = rest.includes("--check");
|
|
8085
|
+
if (check && !migrationExport) {
|
|
8086
|
+
console.error("generate --check requires --migration-export");
|
|
8087
|
+
return 1;
|
|
8088
|
+
}
|
|
7512
8089
|
if (await detectProjectFormat(dir) === "v1") {
|
|
7513
8090
|
console.error(`generate needs a v2 project (${NAMESPACE_CONFIG_FILE}) \u2014 convert with: seq-studio orm migrate-from-yaml ${dir}`);
|
|
7514
8091
|
return 1;
|
|
@@ -7516,6 +8093,36 @@ async function generateCommand(rest) {
|
|
|
7516
8093
|
const compiled = await compileV2Project(dir);
|
|
7517
8094
|
printDiagnostics(compiled.ok ? compiled.project.diagnostics : compiled.diagnostics);
|
|
7518
8095
|
if (!compiled.ok) return 1;
|
|
8096
|
+
if (migrationExport) {
|
|
8097
|
+
if (await validateV2Command(dir) !== 0) return 1;
|
|
8098
|
+
const { loaded: loaded2, sdl, documents, manifest: manifest2 } = compiled.project;
|
|
8099
|
+
const workspace = await readMigrationWorkspace(dir, loaded2.canonical.namespace);
|
|
8100
|
+
const migration = await loadCommittedTipMigrationV2({ dir, loaded: loaded2, workspace });
|
|
8101
|
+
const exported = createOrmMigrationExport({ schemaHash: manifest2.schemaHash, payload: {
|
|
8102
|
+
canonical_definition: loaded2.canonical,
|
|
8103
|
+
documents,
|
|
8104
|
+
sdl,
|
|
8105
|
+
client_manifest: { ops_hash: manifest2.opsHash, operations: Object.fromEntries(Object.entries(manifest2.operations).map(([name, operation]) => [name, operation.opHash])) },
|
|
8106
|
+
schema_snapshot: workspace.tipSnapshot,
|
|
8107
|
+
...migration ? { migration } : {}
|
|
8108
|
+
} });
|
|
8109
|
+
const text4 = renderOrmMigrationExport(exported);
|
|
8110
|
+
const path = join(dir, ORM_MIGRATION_EXPORT_FILE);
|
|
8111
|
+
if (check) {
|
|
8112
|
+
const committed = await readFile(path, "utf8").catch((error) => {
|
|
8113
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
|
|
8114
|
+
throw error;
|
|
8115
|
+
});
|
|
8116
|
+
if (committed !== text4) {
|
|
8117
|
+
console.error(`${ORM_MIGRATION_EXPORT_FILE} is missing or stale; run seq-studio orm generate ${dir} --migration-export and commit it`);
|
|
8118
|
+
return 1;
|
|
8119
|
+
}
|
|
8120
|
+
console.log(`${ORM_MIGRATION_EXPORT_FILE} matches the current definitions, operations, and committed migration`);
|
|
8121
|
+
return 0;
|
|
8122
|
+
}
|
|
8123
|
+
await writeFile(path, text4);
|
|
8124
|
+
console.log(`wrote ${ORM_MIGRATION_EXPORT_FILE} (${exported.checksum.slice(0, 12)})`);
|
|
8125
|
+
}
|
|
7519
8126
|
await writeGenerateOutputs(dir, compiled.project);
|
|
7520
8127
|
const { loaded, manifest } = compiled.project;
|
|
7521
8128
|
console.log(
|
|
@@ -7566,6 +8173,80 @@ async function runConsumerCodegen(dir) {
|
|
|
7566
8173
|
console.log(`ran consumer graphql-codegen (${CODEGEN_CONFIG_FILE})`);
|
|
7567
8174
|
return 0;
|
|
7568
8175
|
}
|
|
8176
|
+
function isVariablesObject(value) {
|
|
8177
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8178
|
+
}
|
|
8179
|
+
function parseExecuteArgs(rest) {
|
|
8180
|
+
const args = [...rest];
|
|
8181
|
+
const variablesIndex = args.indexOf("--variables-file");
|
|
8182
|
+
if (variablesIndex === -1) {
|
|
8183
|
+
throw new OrmDefinitionError("execute requires --variables-file <file>");
|
|
8184
|
+
}
|
|
8185
|
+
const variablesFile = args[variablesIndex + 1];
|
|
8186
|
+
if (!variablesFile) {
|
|
8187
|
+
throw new OrmDefinitionError("execute requires --variables-file <file>");
|
|
8188
|
+
}
|
|
8189
|
+
args.splice(variablesIndex, 2);
|
|
8190
|
+
const envIndex = args.indexOf("--env");
|
|
8191
|
+
if (envIndex !== -1) args.splice(envIndex, 2);
|
|
8192
|
+
if (args.some((arg) => arg.startsWith("-"))) {
|
|
8193
|
+
throw new OrmDefinitionError("execute accepts only <operation>, [dir], --variables-file, and --env");
|
|
8194
|
+
}
|
|
8195
|
+
const operation = args[0];
|
|
8196
|
+
if (!operation || args.length > 2) {
|
|
8197
|
+
throw new OrmDefinitionError("usage: seq-studio orm execute <operation> [dir] --variables-file <file> -e <env>");
|
|
8198
|
+
}
|
|
8199
|
+
return {
|
|
8200
|
+
operation,
|
|
8201
|
+
dir: args[1] ?? ".",
|
|
8202
|
+
variablesFile
|
|
8203
|
+
};
|
|
8204
|
+
}
|
|
8205
|
+
async function readExecuteVariables(variablesFile) {
|
|
8206
|
+
let parsed;
|
|
8207
|
+
try {
|
|
8208
|
+
parsed = JSON.parse(await readFile(resolve(variablesFile), "utf8"));
|
|
8209
|
+
} catch (error) {
|
|
8210
|
+
throw new OrmDefinitionError(
|
|
8211
|
+
`could not read JSON variables from ${variablesFile}: ${error instanceof Error ? error.message : String(error)}`
|
|
8212
|
+
);
|
|
8213
|
+
}
|
|
8214
|
+
if (!isVariablesObject(parsed)) {
|
|
8215
|
+
throw new OrmDefinitionError("--variables-file must contain a JSON object");
|
|
8216
|
+
}
|
|
8217
|
+
return parsed;
|
|
8218
|
+
}
|
|
8219
|
+
async function executeV2Command(rest) {
|
|
8220
|
+
const { dir, operation, variablesFile } = parseExecuteArgs(rest);
|
|
8221
|
+
if (await detectProjectFormat(dir) !== "v2") {
|
|
8222
|
+
throw new OrmDefinitionError("execute supports TypeScript ORM v2 namespaces only");
|
|
8223
|
+
}
|
|
8224
|
+
const loaded = await loadV2(dir);
|
|
8225
|
+
const manifestPath = join(dir, OPERATIONS_MANIFEST_FILE);
|
|
8226
|
+
let manifest;
|
|
8227
|
+
try {
|
|
8228
|
+
manifest = operationsManifestSchema.parse(JSON.parse(await readFile(manifestPath, "utf8")));
|
|
8229
|
+
} catch (error) {
|
|
8230
|
+
throw new OrmDefinitionError(
|
|
8231
|
+
`could not read ${OPERATIONS_MANIFEST_FILE}: ${error instanceof Error ? error.message : String(error)} \u2014 run \`seq-studio orm generate ${dir}\` first`
|
|
8232
|
+
);
|
|
8233
|
+
}
|
|
8234
|
+
const selected = manifest.operations[operation];
|
|
8235
|
+
if (!selected) {
|
|
8236
|
+
throw new OrmDefinitionError(
|
|
8237
|
+
`operation "${operation}" is not in ${OPERATIONS_MANIFEST_FILE}; run \`seq-studio orm generate ${dir}\` after editing GraphQL documents`
|
|
8238
|
+
);
|
|
8239
|
+
}
|
|
8240
|
+
const variables = await readExecuteVariables(variablesFile);
|
|
8241
|
+
const executePrefix = process.env["SEQUENCE_ORM_ATLAS_EXECUTE_V2"] === "1" ? "/api/data/v2" : "/_data/v2";
|
|
8242
|
+
const response = await apiRequestStrict(
|
|
8243
|
+
"POST",
|
|
8244
|
+
`${executePrefix}/${loaded.canonical.namespace}`,
|
|
8245
|
+
{ op: selected.hash, variables }
|
|
8246
|
+
);
|
|
8247
|
+
console.log(JSON.stringify(response, null, 2));
|
|
8248
|
+
return 0;
|
|
8249
|
+
}
|
|
7569
8250
|
async function validateV2Command(dir) {
|
|
7570
8251
|
const compiled = await compileV2Project(dir);
|
|
7571
8252
|
printDiagnostics(compiled.ok ? compiled.project.diagnostics : compiled.diagnostics);
|
|
@@ -8075,6 +8756,7 @@ function columnProperty(table, column, singlePrimaryKey) {
|
|
|
8075
8756
|
}
|
|
8076
8757
|
chain += `.default(${JSON.stringify(column.default)})`;
|
|
8077
8758
|
}
|
|
8759
|
+
if (column.onUpdateFn !== void 0) chain += `.onUpdate(${quote(column.onUpdateFn)})`;
|
|
8078
8760
|
if (column.description !== void 0) chain += `.description(${quote(column.description)})`;
|
|
8079
8761
|
if (column.renamedFrom !== void 0) chain += `.renamedFrom(${quote(column.renamedFrom)})`;
|
|
8080
8762
|
if (column.cast !== void 0) chain += `.cast(${quote(column.cast)})`;
|