@sequenceholdings/orm 2.8.4 → 2.9.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/dist/authoring.cjs +142 -56
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +7 -3
- package/dist/authoring.d.ts +7 -3
- package/dist/authoring.js +142 -56
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +63 -13
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +77 -72
- package/dist/canonical.d.ts +77 -72
- package/dist/canonical.js +63 -13
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +170 -72
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +23 -1
- package/dist/cli.d.ts +23 -1
- package/dist/cli.js +167 -73
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +77 -20
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +2 -2
- package/dist/compiler.d.ts +2 -2
- package/dist/compiler.js +77 -20
- package/dist/compiler.js.map +1 -1
- package/dist/{constants-Dou1RkTo.d.cts → constants-D2o0vK6U.d.cts} +8 -1
- package/dist/{constants-Dou1RkTo.d.ts → constants-D2o0vK6U.d.ts} +8 -1
- package/dist/dynamic-input.d.cts +1 -1
- package/dist/dynamic-input.d.ts +1 -1
- package/dist/index.cjs +193 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -59
- package/dist/index.d.ts +62 -59
- package/dist/index.js +192 -83
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +70 -15
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +76 -76
- package/dist/ir.d.ts +76 -76
- package/dist/ir.js +70 -15
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +70 -15
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +1 -1
- package/dist/predicate-v2.d.ts +1 -1
- package/dist/predicate-v2.js +70 -15
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +70 -15
- 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 +70 -15
- package/dist/sdl.js.map +1 -1
- package/package.json +3 -3
- package/templates/orm-namespace/package.json +2 -1
- package/dist/{definition-DjfC7Lll.d.cts → definition-B99BsOAO.d.cts} +4 -4
- package/dist/{definition-DjfC7Lll.d.ts → definition-B99BsOAO.d.ts} +4 -4
package/dist/cli.cjs
CHANGED
|
@@ -105,6 +105,8 @@ function materializedRefreshFunctionName(table) {
|
|
|
105
105
|
function materializeLockKey(schema, table) {
|
|
106
106
|
return `orm:materialize:${schema}.${table}`;
|
|
107
107
|
}
|
|
108
|
+
var OPERATION_DOCUMENTS_FILE = "operations.documents.json";
|
|
109
|
+
var OPERATION_DOCUMENTS_VERSION = 1;
|
|
108
110
|
|
|
109
111
|
// src/hash.ts
|
|
110
112
|
function stableJson(value) {
|
|
@@ -415,6 +417,48 @@ var policyExpressionSchema = zod.z.union([
|
|
|
415
417
|
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
416
418
|
})
|
|
417
419
|
]);
|
|
420
|
+
|
|
421
|
+
// src/canonical/resolved-rows.ts
|
|
422
|
+
function logicalKey(table) {
|
|
423
|
+
return (table.primaryKey ?? []).map((columnName) => table.columns.find((c) => c.columnName === columnName)?.name ?? columnName);
|
|
424
|
+
}
|
|
425
|
+
function sameKey(key, expected) {
|
|
426
|
+
return key.length === expected.length && key.every((name2) => expected.includes(name2));
|
|
427
|
+
}
|
|
428
|
+
function validateRowIdentity(input) {
|
|
429
|
+
validateExistsIn(input);
|
|
430
|
+
if (input.keys.length > 1) validateCompositeKey(input);
|
|
431
|
+
}
|
|
432
|
+
function validateExistsIn({ resolved, sourceByName, fail: fail4 }) {
|
|
433
|
+
const name2 = resolved.existsIn?.[0];
|
|
434
|
+
if (name2 === void 0) return;
|
|
435
|
+
if (sourceByName.get(name2)?.kind !== "etl") fail4(`existsIn '${name2}' must be an etl source`);
|
|
436
|
+
if (resolved.policies.insert !== void 0 || resolved.policies.write !== void 0) {
|
|
437
|
+
fail4("existsIn allows only an update policy: rows come from the etl source");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function validateCompositeKey({ resolved, keys, tableByName, fail: fail4 }) {
|
|
441
|
+
const keyList = `(${keys.join(", ")})`;
|
|
442
|
+
if (resolved.existsIn === void 0) {
|
|
443
|
+
fail4(`requires exactly one primary-key column: composite key ${keyList} requires existsIn`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const writes = ["insert", "update", "write"].filter((kind) => resolved.policies[kind] !== void 0);
|
|
447
|
+
if (writes.length > 0) fail4(`composite key ${keyList} requires a read-only table: remove the ${writes.join("/")} policy`);
|
|
448
|
+
if (resolved.materialized) fail4(`composite key ${keyList} cannot be materialized`);
|
|
449
|
+
for (const source of resolved.sources) {
|
|
450
|
+
if (source.kind === "prefetch") {
|
|
451
|
+
fail4(`composite key ${keyList} cannot have prefetch source '${source.name}'`);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const table = tableByName.get(source.table);
|
|
455
|
+
if (table && !sameKey(logicalKey(table), keys)) {
|
|
456
|
+
fail4(`composite key ${keyList} requires ${source.kind} source '${source.name}' to be keyed by it; table '${source.table}' is keyed by (${logicalKey(table).join(", ")})`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/canonical/resolved.ts
|
|
418
462
|
function parseDurationMs(input) {
|
|
419
463
|
if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
|
|
420
464
|
const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
|
|
@@ -491,6 +535,13 @@ var canonicalResolvedSchema = zod.z.object({
|
|
|
491
535
|
resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
|
|
492
536
|
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
493
537
|
deletedBy: zod.z.array(identifier).max(1).default([]),
|
|
538
|
+
/**
|
|
539
|
+
* `rows.existsIn`: the one etl source whose rows are the table's rows (read
|
|
540
|
+
* directly as the row source). Only an update policy may write. It is also
|
|
541
|
+
* what admits a composite primary key: a read-only table keyed exactly like
|
|
542
|
+
* that source.
|
|
543
|
+
*/
|
|
544
|
+
existsIn: zod.z.array(identifier).length(1).optional(),
|
|
494
545
|
/**
|
|
495
546
|
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
496
547
|
* data plane keeps in step with `compute`, the internal view holding the
|
|
@@ -509,8 +560,8 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
509
560
|
if (!resolved) return [];
|
|
510
561
|
const errors = [];
|
|
511
562
|
const fail4 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
512
|
-
const
|
|
513
|
-
if (
|
|
563
|
+
const keyColumns = (view.primaryKey ?? []).map((columnName) => view.columns.find((c) => c.columnName === columnName)).filter((column) => column !== void 0);
|
|
564
|
+
if (keyColumns.length === 0 || keyColumns.length !== view.primaryKey?.length) {
|
|
514
565
|
fail4("requires exactly one primary-key column");
|
|
515
566
|
return errors;
|
|
516
567
|
}
|
|
@@ -518,7 +569,7 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
518
569
|
const scope = {
|
|
519
570
|
view,
|
|
520
571
|
resolved,
|
|
521
|
-
|
|
572
|
+
keys: new Map(keyColumns.map((column) => [column.name, column])),
|
|
522
573
|
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
523
574
|
sourceByName: /* @__PURE__ */ new Map(),
|
|
524
575
|
fail: fail4
|
|
@@ -532,6 +583,7 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
532
583
|
if (!table) fail4(`source '${source.name}' references unknown table '${source.table}'`);
|
|
533
584
|
else validateSource({ scope, source, table });
|
|
534
585
|
}
|
|
586
|
+
validateRowIdentity({ resolved, keys: [...scope.keys.keys()], sourceByName: scope.sourceByName, tableByName, fail: fail4 });
|
|
535
587
|
validateRules(scope);
|
|
536
588
|
validateViewCells(scope);
|
|
537
589
|
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
@@ -561,7 +613,7 @@ function validateSource({ scope, source, table }) {
|
|
|
561
613
|
validatePrefetchFields({ scope, source });
|
|
562
614
|
}
|
|
563
615
|
function validatePrefetchFields({ scope, source }) {
|
|
564
|
-
const { fail: fail4,
|
|
616
|
+
const { fail: fail4, keys } = scope;
|
|
565
617
|
if (source.kind !== "prefetch") {
|
|
566
618
|
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
567
619
|
fail4(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
@@ -570,7 +622,7 @@ function validatePrefetchFields({ scope, source }) {
|
|
|
570
622
|
}
|
|
571
623
|
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
572
624
|
fail4(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
573
|
-
} else if (source.key
|
|
625
|
+
} else if (!keys.has(source.key) && !source.columns.includes(source.key)) {
|
|
574
626
|
fail4(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
575
627
|
}
|
|
576
628
|
for (const column of source.writeback?.columns ?? []) {
|
|
@@ -578,19 +630,19 @@ function validatePrefetchFields({ scope, source }) {
|
|
|
578
630
|
}
|
|
579
631
|
}
|
|
580
632
|
function validateSourceColumns({ scope, source, byName }) {
|
|
581
|
-
const { fail: fail4,
|
|
582
|
-
for (const column of [
|
|
583
|
-
const own = column
|
|
633
|
+
const { fail: fail4, keys, dataColumns } = scope;
|
|
634
|
+
for (const column of [...keys.keys(), ...source.columns]) {
|
|
635
|
+
const own = keys.get(column) ?? dataColumns.get(column);
|
|
584
636
|
const theirs = byName.get(column);
|
|
585
637
|
if (!own) fail4(`source '${source.name}' supplies unknown column '${column}'`);
|
|
586
638
|
else if (!theirs) fail4(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
587
639
|
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail4(`column '${column}' has a different type on source '${source.name}'`);
|
|
588
640
|
}
|
|
589
641
|
}
|
|
590
|
-
function validateRules({ resolved,
|
|
642
|
+
function validateRules({ resolved, keys, dataColumns, sourceByName, fail: fail4 }) {
|
|
591
643
|
const ruled = /* @__PURE__ */ new Set();
|
|
592
644
|
for (const rule of resolved.resolve) {
|
|
593
|
-
if (!dataColumns.has(rule.column) || rule.column
|
|
645
|
+
if (!dataColumns.has(rule.column) || keys.has(rule.column)) {
|
|
594
646
|
fail4(`resolve rule targets unknown column '${rule.column}'`);
|
|
595
647
|
continue;
|
|
596
648
|
}
|
|
@@ -607,7 +659,7 @@ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fa
|
|
|
607
659
|
}
|
|
608
660
|
}
|
|
609
661
|
for (const column of dataColumns.keys()) {
|
|
610
|
-
if (column
|
|
662
|
+
if (!keys.has(column) && !ruled.has(column)) fail4(`column '${column}' has no resolve rule`);
|
|
611
663
|
}
|
|
612
664
|
}
|
|
613
665
|
var VIEW_CELL_TYPES = {
|
|
@@ -616,10 +668,10 @@ var VIEW_CELL_TYPES = {
|
|
|
616
668
|
by: "text",
|
|
617
669
|
conflicts: "jsonb"
|
|
618
670
|
};
|
|
619
|
-
function validateViewCells({ view, dataColumns,
|
|
671
|
+
function validateViewCells({ view, dataColumns, keys, fail: fail4 }) {
|
|
620
672
|
const expected = /* @__PURE__ */ new Map();
|
|
621
673
|
for (const column of dataColumns.values()) {
|
|
622
|
-
if (column.name
|
|
674
|
+
if (keys.has(column.name)) continue;
|
|
623
675
|
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
624
676
|
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
625
677
|
}
|
|
@@ -1845,8 +1897,13 @@ var resolvedSourceIrSchema = zod.z.object({
|
|
|
1845
1897
|
}).optional()
|
|
1846
1898
|
});
|
|
1847
1899
|
var resolvedMetaIrSchema = zod.z.object({
|
|
1848
|
-
/**
|
|
1849
|
-
|
|
1900
|
+
/**
|
|
1901
|
+
* Logical pk column of the resolved view. Absent for a composite key, which
|
|
1902
|
+
* only a read-only `existsIn` table may declare: it has no writes, prefetch
|
|
1903
|
+
* or materialization (the paths that address one key column), and its reads
|
|
1904
|
+
* use the view's `primaryKey` like any keyed view.
|
|
1905
|
+
*/
|
|
1906
|
+
pk: zod.z.string().optional(),
|
|
1850
1907
|
/** Source name of the manual source. */
|
|
1851
1908
|
manual: zod.z.string(),
|
|
1852
1909
|
sources: zod.z.array(resolvedSourceIrSchema),
|
|
@@ -3271,7 +3328,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3271
3328
|
appendable: readOnly ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
3272
3329
|
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
3273
3330
|
...cells ? { cells } : {},
|
|
3274
|
-
...resolved ? { resolved: resolvedMeta({ resolved,
|
|
3331
|
+
...resolved ? { resolved: resolvedMeta({ resolved, primaryKey: primaryKey2 }) } : {},
|
|
3275
3332
|
constraints: isView ? [] : constraints,
|
|
3276
3333
|
dynamicFilterMaxDepth: 6,
|
|
3277
3334
|
dynamicFilterMaxNodes: 64
|
|
@@ -3553,10 +3610,10 @@ function resolvedWritable(entries) {
|
|
|
3553
3610
|
const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
|
|
3554
3611
|
return entries.filter((entry) => !entry.deprecated && !system.has(entry.name)).map((entry) => entry.name);
|
|
3555
3612
|
}
|
|
3556
|
-
function resolvedMeta({ resolved,
|
|
3613
|
+
function resolvedMeta({ resolved, primaryKey: primaryKey2 }) {
|
|
3557
3614
|
const manual = resolved.sources.find((source) => source.kind === "manual");
|
|
3558
3615
|
return {
|
|
3559
|
-
pk,
|
|
3616
|
+
...primaryKey2.length === 1 ? { pk: primaryKey2[0] } : {},
|
|
3560
3617
|
manual: manual?.name ?? "",
|
|
3561
3618
|
deletedBy: [...resolved.deletedBy],
|
|
3562
3619
|
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
@@ -3682,7 +3739,9 @@ function applyPrefetchDirectives({ node, root, meta }) {
|
|
|
3682
3739
|
if (directive.maxStaleness !== void 0 && maxStalenessMs === void 0) {
|
|
3683
3740
|
fail(`@prefetch(maxStaleness: "${directive.maxStaleness}") must look like '500ms', '30s' or '5m'`, "invalid-source-directive");
|
|
3684
3741
|
}
|
|
3685
|
-
const
|
|
3742
|
+
const pk = meta.resolved?.pk;
|
|
3743
|
+
if (pk === void 0) return fail(`@prefetch on '${root.alias}' needs a single-column primary key`, "invalid-source-directive");
|
|
3744
|
+
const key = root.kind === "byPk" ? { one: root.pk[0].value } : selectKey({ where: root.where, pk, source, alias: root.alias });
|
|
3686
3745
|
return { source: source.name, ...maxStalenessMs !== void 0 ? { maxStalenessMs } : {}, key };
|
|
3687
3746
|
});
|
|
3688
3747
|
return { ...root, prefetch: plans };
|
|
@@ -6578,6 +6637,7 @@ function buildCanonicalResolved(resolved) {
|
|
|
6578
6637
|
})),
|
|
6579
6638
|
resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
|
|
6580
6639
|
deletedBy: [...resolved.deletedBy],
|
|
6640
|
+
...resolved.existsIn !== void 0 ? { existsIn: [...resolved.existsIn] } : {},
|
|
6581
6641
|
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
6582
6642
|
policies
|
|
6583
6643
|
};
|
|
@@ -10288,6 +10348,42 @@ async function readSnapshot(dir, ordinal) {
|
|
|
10288
10348
|
const path$1 = path.join(dir, MIGRATIONS_DIRNAME, META_DIRNAME, `${pad(ordinal)}.snapshot.json`);
|
|
10289
10349
|
return parseJson(path$1, await readSnapshotText(dir, ordinal));
|
|
10290
10350
|
}
|
|
10351
|
+
async function verifyCommittedLedger(dir, entries) {
|
|
10352
|
+
const firstRequiredSnapshot = entries.length - 2;
|
|
10353
|
+
let previousId3;
|
|
10354
|
+
let previousSnapshotPresent = false;
|
|
10355
|
+
let previousHash = null;
|
|
10356
|
+
for (const [index3, entry] of entries.entries()) {
|
|
10357
|
+
const sql4 = await readMigrationSql(dir, entry);
|
|
10358
|
+
if (checksumOf(sql4) !== entry.checksum) {
|
|
10359
|
+
return `${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`;
|
|
10360
|
+
}
|
|
10361
|
+
const snapshotText = index3 >= firstRequiredSnapshot ? await readSnapshotText(dir, entry.ordinal) : await readPrunableSnapshotText(dir, entry.ordinal);
|
|
10362
|
+
if (snapshotText !== void 0) {
|
|
10363
|
+
if (checksumOf(snapshotText) !== entry.snapshot_checksum) {
|
|
10364
|
+
return `the snapshot for ${entry.name} does not match its journal checksum \u2014 committed snapshots must not be edited (the next diff builds on it)`;
|
|
10365
|
+
}
|
|
10366
|
+
const snapshot = JSON.parse(snapshotText);
|
|
10367
|
+
if (previousSnapshotPresent && snapshot.prevId !== previousId3) {
|
|
10368
|
+
return `snapshot chain broken at ${entry.name} \u2014 regenerate the workspace with \`seq-studio orm diff\``;
|
|
10369
|
+
}
|
|
10370
|
+
previousId3 = snapshot.id;
|
|
10371
|
+
}
|
|
10372
|
+
previousSnapshotPresent = snapshotText !== void 0;
|
|
10373
|
+
if (entry.previous_content_hash !== previousHash) {
|
|
10374
|
+
return `content-hash chain broken at ${entry.name} \u2014 migrations must be appended in order by \`seq-studio orm diff\``;
|
|
10375
|
+
}
|
|
10376
|
+
previousHash = entry.content_hash;
|
|
10377
|
+
}
|
|
10378
|
+
return void 0;
|
|
10379
|
+
}
|
|
10380
|
+
async function readPrunableSnapshotText(dir, ordinal) {
|
|
10381
|
+
const path$1 = path.join(dir, MIGRATIONS_DIRNAME, META_DIRNAME, `${pad(ordinal)}.snapshot.json`);
|
|
10382
|
+
return promises.readFile(path$1, "utf8").catch((error) => {
|
|
10383
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
|
|
10384
|
+
throw error;
|
|
10385
|
+
});
|
|
10386
|
+
}
|
|
10291
10387
|
async function readMigrationSql(dir, entry) {
|
|
10292
10388
|
const path$1 = path.join(dir, MIGRATIONS_DIRNAME, `${entry.name}.sql`);
|
|
10293
10389
|
return promises.readFile(path$1, "utf8").catch(() => {
|
|
@@ -10448,30 +10544,10 @@ async function checkWorkspace({
|
|
|
10448
10544
|
log(`no committed migrations for '${loaded.definition.namespace}' \u2014 run \`seq-studio orm diff\` and commit the result`);
|
|
10449
10545
|
return 1;
|
|
10450
10546
|
}
|
|
10451
|
-
|
|
10452
|
-
|
|
10453
|
-
|
|
10454
|
-
|
|
10455
|
-
if (checksumOf(sql4) !== entry.checksum) {
|
|
10456
|
-
log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
|
|
10457
|
-
return 1;
|
|
10458
|
-
}
|
|
10459
|
-
const snapshotText = await readSnapshotText(dir, entry.ordinal);
|
|
10460
|
-
if (checksumOf(snapshotText) !== entry.snapshot_checksum) {
|
|
10461
|
-
log(`the snapshot for ${entry.name} does not match its journal checksum \u2014 committed snapshots must not be edited (the next diff builds on it)`);
|
|
10462
|
-
return 1;
|
|
10463
|
-
}
|
|
10464
|
-
const snapshot = JSON.parse(snapshotText);
|
|
10465
|
-
if (entry.ordinal > 1 && snapshot.prevId !== previousId3) {
|
|
10466
|
-
log(`snapshot chain broken at ${entry.name} \u2014 regenerate the workspace with \`seq-studio orm diff\``);
|
|
10467
|
-
return 1;
|
|
10468
|
-
}
|
|
10469
|
-
if (entry.previous_content_hash !== previousHash) {
|
|
10470
|
-
log(`content-hash chain broken at ${entry.name} \u2014 migrations must be appended in order by \`seq-studio orm diff\``);
|
|
10471
|
-
return 1;
|
|
10472
|
-
}
|
|
10473
|
-
previousId3 = snapshot.id;
|
|
10474
|
-
previousHash = entry.content_hash;
|
|
10547
|
+
const broken = await verifyCommittedLedger(dir, workspace.journal.entries);
|
|
10548
|
+
if (broken) {
|
|
10549
|
+
log(broken);
|
|
10550
|
+
return 1;
|
|
10475
10551
|
}
|
|
10476
10552
|
if (tip.content_hash !== loaded.contentHash) {
|
|
10477
10553
|
log(`definitions changed since ${tip.name} \u2014 run \`seq-studio orm diff\` and commit the new migration`);
|
|
@@ -11350,6 +11426,22 @@ function parseArgs(argv) {
|
|
|
11350
11426
|
// src/cli.ts
|
|
11351
11427
|
var BASE_URL_ENV = "SEQUENCE_ORM_BASE_URL";
|
|
11352
11428
|
var TOKEN_ENV = "SEQUENCE_ORM_TOKEN";
|
|
11429
|
+
var SOURCE_ENV = "SEQUENCE_ORM_SOURCE";
|
|
11430
|
+
var UNVERIFIED_SOURCE_REASON_ENV = "SEQUENCE_ORM_UNVERIFIED_SOURCE_REASON";
|
|
11431
|
+
function applySourceFromEnv(env = process.env) {
|
|
11432
|
+
const reason = env[UNVERIFIED_SOURCE_REASON_ENV]?.trim();
|
|
11433
|
+
const override = reason ? { unverified_source_reason: reason } : {};
|
|
11434
|
+
const raw = env[SOURCE_ENV];
|
|
11435
|
+
if (!raw) return override;
|
|
11436
|
+
let parsed;
|
|
11437
|
+
try {
|
|
11438
|
+
parsed = JSON.parse(raw);
|
|
11439
|
+
} catch {
|
|
11440
|
+
throw new Error(`${SOURCE_ENV} is not valid JSON`);
|
|
11441
|
+
}
|
|
11442
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${SOURCE_ENV} must be a JSON object`);
|
|
11443
|
+
return { source: Object.fromEntries(Object.entries(parsed)), ...override };
|
|
11444
|
+
}
|
|
11353
11445
|
var ORM_MODULE_PATH_ENV = "SEQUENCE_ORM_MODULE_PATH";
|
|
11354
11446
|
var USAGE = `usage:
|
|
11355
11447
|
seq-studio orm init <dir> scaffold a namespace package (v2: TypeScript schema + persisted operations)
|
|
@@ -11667,15 +11759,21 @@ async function generateCommand(rest) {
|
|
|
11667
11759
|
await writeGenerateOutputs(dir, compiled.project);
|
|
11668
11760
|
const { loaded, manifest } = compiled.project;
|
|
11669
11761
|
console.log(
|
|
11670
|
-
`wrote ${SCHEMA_GRAPHQL_FILE}, ${OPERATIONS_MANIFEST_FILE}, ${TYPE_POLICIES_FILE}, sql-operations.gen.ts, namespace.contract.json (${loaded.canonical.tables.length} table(s), ${loaded.canonical.views?.length ?? 0} view(s), ${Object.keys(manifest.operations).length} operation(s))`
|
|
11762
|
+
`wrote ${SCHEMA_GRAPHQL_FILE}, ${OPERATIONS_MANIFEST_FILE}, ${OPERATION_DOCUMENTS_FILE}, ${TYPE_POLICIES_FILE}, sql-operations.gen.ts, namespace.contract.json (${loaded.canonical.tables.length} table(s), ${loaded.canonical.views?.length ?? 0} view(s), ${Object.keys(manifest.operations).length} operation(s))`
|
|
11671
11763
|
);
|
|
11672
11764
|
return runConsumerCodegen(dir);
|
|
11673
11765
|
}
|
|
11674
11766
|
async function writeGenerateOutputs(dir, project) {
|
|
11675
|
-
const { loaded, sdl, manifest } = project;
|
|
11767
|
+
const { loaded, sdl, manifest, documents } = project;
|
|
11676
11768
|
await promises.writeFile(path.join(dir, SCHEMA_GRAPHQL_FILE), sdl);
|
|
11677
11769
|
await promises.writeFile(path.join(dir, "namespace.contract.json"), `${JSON.stringify(loaded.canonical, null, 2)}
|
|
11678
11770
|
`);
|
|
11771
|
+
const sortedDocuments = [...documents].sort((a, b) => a.path.localeCompare(b.path)).map(({ path, contents }) => ({ path, contents }));
|
|
11772
|
+
await promises.writeFile(
|
|
11773
|
+
path.join(dir, OPERATION_DOCUMENTS_FILE),
|
|
11774
|
+
`${JSON.stringify({ documentsVersion: OPERATION_DOCUMENTS_VERSION, documents: sortedDocuments }, null, 2)}
|
|
11775
|
+
`
|
|
11776
|
+
);
|
|
11679
11777
|
const operations = Object.entries(manifest.operations).map(([name2, op]) => ({
|
|
11680
11778
|
name: name2,
|
|
11681
11779
|
kind: op.kind,
|
|
@@ -11881,8 +11979,20 @@ function reportRegistryState(registry) {
|
|
|
11881
11979
|
if (match && match.id === registry.activeVersionId) console.log("-- registry: this content is the ACTIVE version");
|
|
11882
11980
|
else if (match) console.log(`-- registry: registered (status ${match.status}), not active`);
|
|
11883
11981
|
else console.log(`-- registry: new content (active is ${active ? active.content_hash.slice(0, 12) : "none"})`);
|
|
11982
|
+
if (registry.activeActivation) console.log(`-- registry: ${describeActivation(registry.activeActivation)}`);
|
|
11884
11983
|
return 0;
|
|
11885
11984
|
}
|
|
11985
|
+
function describeActivation(activation) {
|
|
11986
|
+
const since = `live since ${activation.activated_at.slice(0, 16).replace("T", " ")}Z`;
|
|
11987
|
+
const by = `by ${activation.actor_sub.replace(/^google-apps\|/, "")}`;
|
|
11988
|
+
if (activation.source_commit && activation.source_branch) {
|
|
11989
|
+
const at = activation.source_path ? ` at ${activation.source_path}` : "";
|
|
11990
|
+
return `${since} from ${activation.source_commit.slice(0, 12)} on ${activation.source_branch}${at} (${activation.provenance_class}) ${by}`;
|
|
11991
|
+
}
|
|
11992
|
+
const failure = activation.reported_source?.verification_failure;
|
|
11993
|
+
const why = typeof failure === "string" ? `: ${failure}` : "";
|
|
11994
|
+
return `${since} from an untraced source (${activation.provenance_class}${why}) ${by}`;
|
|
11995
|
+
}
|
|
11886
11996
|
var META_DIRNAME2 = "meta";
|
|
11887
11997
|
var JOURNAL_FILENAME2 = "journal.json";
|
|
11888
11998
|
var padOrdinal = (ordinal) => String(ordinal).padStart(4, "0");
|
|
@@ -11970,30 +12080,10 @@ async function checkWorkspaceV2({
|
|
|
11970
12080
|
log(`no committed migrations for '${namespace}' \u2014 run \`seq-studio orm diff\` and commit the result`);
|
|
11971
12081
|
return 1;
|
|
11972
12082
|
}
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
if (checksumOf(sql4) !== entry.checksum) {
|
|
11978
|
-
log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
|
|
11979
|
-
return 1;
|
|
11980
|
-
}
|
|
11981
|
-
const snapshotText = await readSnapshotText(dir, entry.ordinal);
|
|
11982
|
-
if (checksumOf(snapshotText) !== entry.snapshot_checksum) {
|
|
11983
|
-
log(`the snapshot for ${entry.name} does not match its journal checksum \u2014 committed snapshots must not be edited (the next diff builds on it)`);
|
|
11984
|
-
return 1;
|
|
11985
|
-
}
|
|
11986
|
-
const snapshot = JSON.parse(snapshotText);
|
|
11987
|
-
if (entry.ordinal > 1 && snapshot.prevId !== previousId3) {
|
|
11988
|
-
log(`snapshot chain broken at ${entry.name} \u2014 regenerate the workspace with \`seq-studio orm diff\``);
|
|
11989
|
-
return 1;
|
|
11990
|
-
}
|
|
11991
|
-
if (entry.previous_content_hash !== previousHash) {
|
|
11992
|
-
log(`content-hash chain broken at ${entry.name} \u2014 migrations must be appended in order by \`seq-studio orm diff\``);
|
|
11993
|
-
return 1;
|
|
11994
|
-
}
|
|
11995
|
-
previousId3 = snapshot.id;
|
|
11996
|
-
previousHash = entry.content_hash;
|
|
12083
|
+
const broken = await verifyCommittedLedger(dir, workspace.journal.entries);
|
|
12084
|
+
if (broken) {
|
|
12085
|
+
log(broken);
|
|
12086
|
+
return 1;
|
|
11997
12087
|
}
|
|
11998
12088
|
if (tip.content_hash !== loaded.hash) {
|
|
11999
12089
|
log(`definitions changed since ${tip.name} \u2014 run \`seq-studio orm diff\` and commit the new migration`);
|
|
@@ -12107,7 +12197,8 @@ async function applyV2Command(rest, dir) {
|
|
|
12107
12197
|
const applied = await apiRequestStrict("POST", `/api/orm/namespaces/${namespace}/apply`, {
|
|
12108
12198
|
version_id: registered.version_id,
|
|
12109
12199
|
...allowDestructive ? { allow_destructive: true } : {},
|
|
12110
|
-
...schemaAlreadyActive && registered.operation_set_id ? { operation_set_id: registered.operation_set_id } : {}
|
|
12200
|
+
...schemaAlreadyActive && registered.operation_set_id ? { operation_set_id: registered.operation_set_id } : {},
|
|
12201
|
+
...applySourceFromEnv()
|
|
12111
12202
|
});
|
|
12112
12203
|
const live = { namespace, contentHash: registered.content_hash };
|
|
12113
12204
|
if ("ops_only" in applied) {
|
|
@@ -12564,7 +12655,8 @@ async function applyCommand(rest) {
|
|
|
12564
12655
|
if (dryRun) return dryRunRehearsal({ namespace, versionId: registered.version_id, contentHash: registered.content_hash });
|
|
12565
12656
|
const applied = await apiRequestStrict("POST", `/api/orm/namespaces/${namespace}/apply`, {
|
|
12566
12657
|
version_id: registered.version_id,
|
|
12567
|
-
...allowDestructive ? { allow_destructive: true } : {}
|
|
12658
|
+
...allowDestructive ? { allow_destructive: true } : {},
|
|
12659
|
+
...applySourceFromEnv()
|
|
12568
12660
|
});
|
|
12569
12661
|
if ("status" in applied) {
|
|
12570
12662
|
const verdict = await awaitQueuedApply({ dir, namespace, contentHash: registered.content_hash, runId: applied.run_id, applyWait });
|
|
@@ -12744,11 +12836,13 @@ async function apiRequestStrict(method, path, body) {
|
|
|
12744
12836
|
}
|
|
12745
12837
|
async function fetchRegistryState(namespace, contentHash) {
|
|
12746
12838
|
if (!process.env[BASE_URL_ENV]?.trim() || !await resolveToken()) return { kind: "offline" };
|
|
12747
|
-
const body = await apiRequest("GET", `/api/orm/namespaces/${namespace}?hash=${encodeURIComponent(contentHash)}`, void 0, { allow404: true });
|
|
12839
|
+
const body = await apiRequest("GET", `/api/orm/namespaces/${namespace}?hash=${encodeURIComponent(contentHash)}&activations_limit=1`, void 0, { allow404: true });
|
|
12748
12840
|
if (!body) return { kind: "missing" };
|
|
12841
|
+
const latest = body.activations?.[0];
|
|
12749
12842
|
return {
|
|
12750
12843
|
kind: "found",
|
|
12751
12844
|
activeVersionId: body.namespace.active_version_id,
|
|
12845
|
+
activeActivation: latest && latest.id === body.namespace.active_activation_id ? latest : null,
|
|
12752
12846
|
versions: body.versions,
|
|
12753
12847
|
hashVersion: body.hash_version ?? null
|
|
12754
12848
|
};
|
|
@@ -12765,7 +12859,11 @@ var firstLine2 = (sql4) => sql4.split("\n")[0] ?? sql4;
|
|
|
12765
12859
|
|
|
12766
12860
|
exports.BASE_URL_ENV = BASE_URL_ENV;
|
|
12767
12861
|
exports.ORM_MODULE_PATH_ENV = ORM_MODULE_PATH_ENV;
|
|
12862
|
+
exports.SOURCE_ENV = SOURCE_ENV;
|
|
12768
12863
|
exports.TOKEN_ENV = TOKEN_ENV;
|
|
12864
|
+
exports.UNVERIFIED_SOURCE_REASON_ENV = UNVERIFIED_SOURCE_REASON_ENV;
|
|
12865
|
+
exports.applySourceFromEnv = applySourceFromEnv;
|
|
12866
|
+
exports.describeActivation = describeActivation;
|
|
12769
12867
|
exports.errorDetail = errorDetail;
|
|
12770
12868
|
exports.parseApplyWait = parseApplyWait;
|
|
12771
12869
|
exports.runCli = runCli;
|