@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/README.md
CHANGED
|
@@ -1,47 +1,117 @@
|
|
|
1
1
|
# @sequenceholdings/orm
|
|
2
2
|
|
|
3
|
-
Sequence ORM
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Design source of truth: the "Sequence ORM: OpCo Data Layer" Notion page.
|
|
9
|
-
|
|
10
|
-
This package is the pure core — no Atlas imports, no database access:
|
|
11
|
-
|
|
12
|
-
- `authoring/` — TypeScript `defineTable` / `defineView` DSL. View queries use
|
|
13
|
-
Drizzle through `@sequenceholdings/orm/drizzle`; only normalized SQL and
|
|
14
|
-
dependencies enter canonical JSON.
|
|
15
|
-
- `canonical/` — validated v2 table/view wire contract shared by CLI and Atlas.
|
|
16
|
-
- `compiler-gql/`, `sdl/`, and `ir/` — read/write table operations plus
|
|
17
|
-
read-only keyed or keyless view queries compiled to persisted operation IR.
|
|
18
|
-
|
|
19
|
-
- `schema/` — zod schemas for the YAML definition surface (tables, columns, links, indexes, policies, actions)
|
|
20
|
-
- `migrations/` — the committed migration workspace (`orm diff`, offline `--check` verification, destructive-statement classification)
|
|
21
|
-
- `predicate/` — the policy expression grammar (`claim('…')`, `capability('…')`, comparisons, `and`/`or`/`not`)
|
|
22
|
-
- `compiler/` — predicate AST and identifier/literal helpers → SQL fragments for policies and claims
|
|
23
|
-
- `engine/` — namespace definitions → ordered migration plans (`hint` / `ddl` / `security` / `function`) plus drizzle-kit snapshots for the registry. Each declared action compiles to a `SECURITY INVOKER` Postgres function (`orm_<ns>.action_<name>`) created at apply, so the invoke path is a single `SELECT`; writes never run as client-rewritten statements. Apply also `PREPARE`s each executes statement so Postgres validates its columns/types (plpgsql function bodies defer that to first invoke), failing the apply on a bad reference. `executes` may be one statement or an ordered list (run atomically); `returns:` makes the function `RETURNS TABLE(...)` and hands back the RETURNING rows.
|
|
24
|
-
- `loader.ts` — a namespace directory of `*.yaml` files → validated `NamespaceDefinition`
|
|
25
|
-
- `hash.ts` — canonical content hash (key-order-insensitive) used for content-addressed registry versions
|
|
26
|
-
- `client/` (`@sequenceholdings/orm/client`) — typed Data API client; fetch-only, no node builtins. `createOrmClient` acts as the caller (session bearer, or a managed function's delegated user token — the default); `createServiceOrmClient` is the loudly-named escalation that acts as the platform service account attached to the function (its manifest's top-level `service_account`). See the `sequence-orm` skill → "two consumer modes".
|
|
27
|
-
- `codegen/` — derives the client's wire types from the definitions; `apply` refreshes `types.gen.ts` on every successful run (there is no separate `generate` command — one verb owns the loop)
|
|
28
|
-
|
|
29
|
-
Deploy with `seq-studio orm …` (see `@sequenceholdings/studio-cli`):
|
|
3
|
+
Sequence ORM v2 is a Preview SDK for TypeScript-defined governed data
|
|
4
|
+
namespaces. You define tables, policies, and named GraphQL operations in source
|
|
5
|
+
control; the platform compiles them into persisted operations and enforces the
|
|
6
|
+
caller's access on every execution.
|
|
30
7
|
|
|
8
|
+
Sequence ORM is a workflow-agnostic platform primitive. The compiler and
|
|
9
|
+
runtime own generic schema, policy, migration, and execution contracts;
|
|
10
|
+
deployed namespaces own domain tables and operations. Primitive code must not
|
|
11
|
+
branch on a namespace or product identity. See
|
|
12
|
+
[Platform primitive boundaries](../../../docs/platform/platform-primitives.md).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
Install the CLI and SDK together:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add -g @sequenceholdings/studio-cli@0.1.35 @sequenceholdings/orm@2.1.0
|
|
31
20
|
```
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
seq-studio orm
|
|
21
|
+
|
|
22
|
+
Create a namespace project:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
seq-studio orm init lending
|
|
26
|
+
cd lending
|
|
27
|
+
pnpm install
|
|
37
28
|
```
|
|
38
29
|
|
|
39
|
-
|
|
40
|
-
copy-on-write fork of the environment's live data — proving the migration
|
|
41
|
-
survives real rows (a `NOT NULL` onto populated data, a constraint existing
|
|
42
|
-
rows violate) — then discards the fork without applying for real.
|
|
30
|
+
## Everyday workflow
|
|
43
31
|
|
|
44
32
|
```bash
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
# Edit sequence.config.ts, schema/*.ts, and graphql/**/*.ts.
|
|
34
|
+
seq-studio orm generate .
|
|
35
|
+
seq-studio orm validate .
|
|
36
|
+
seq-studio orm plan . -e <environment-name>
|
|
37
|
+
seq-studio orm apply . -e <environment-name>
|
|
47
38
|
```
|
|
39
|
+
|
|
40
|
+
`generate` writes `schema.graphql`, `operations.manifest.json`, cache policies,
|
|
41
|
+
and typed consumer modules. Generated SDL may contain up to 16 MiB of UTF-8
|
|
42
|
+
text for the complete namespace; individual authored files retain a 1 MiB cap.
|
|
43
|
+
Registration and reviewed migration exports are bounded to 64 MiB including
|
|
44
|
+
canonical metadata, snapshots, documents, and migration statements.
|
|
45
|
+
|
|
46
|
+
Commit generated outputs and migrations with the
|
|
47
|
+
source that produced them.
|
|
48
|
+
|
|
49
|
+
Use `apply --dry-run` to rehearse a migration against a disposable copy of the
|
|
50
|
+
target environment's data. `apply --no-create` refuses to provision a missing
|
|
51
|
+
namespace, which is useful for deployment checks.
|
|
52
|
+
|
|
53
|
+
## Execute persisted operations
|
|
54
|
+
|
|
55
|
+
### Guard dependent writes
|
|
56
|
+
|
|
57
|
+
Mutation roots run in one transaction. To require a conditional write to
|
|
58
|
+
succeed before committing dependent roots, add a fixed row-count assertion:
|
|
59
|
+
|
|
60
|
+
```graphql
|
|
61
|
+
mutation SaveDeal($id: uuid!, $revision: Int!, $title: String!) {
|
|
62
|
+
updateDeals(
|
|
63
|
+
where: { id: { _eq: $id }, revision: { _eq: $revision } }
|
|
64
|
+
_set: { title: $title }
|
|
65
|
+
_inc: { revision: 1 }
|
|
66
|
+
) @assertAffectedRows(exactly: 1) {
|
|
67
|
+
returning { id revision }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This example assumes a `deals` table with `id`, `title`, and integer `revision`
|
|
73
|
+
columns and suitable write policies. The assertion uses the actual SQL write
|
|
74
|
+
count even when `affectedRows` is not selected. A mismatch rolls back **all**
|
|
75
|
+
roots and the operation journal with HTTP 409 `MUTATION_PRECONDITION_FAILED`.
|
|
76
|
+
Missing, policy-hidden, and stale matches receive the same generic error.
|
|
77
|
+
Ordinary mutations without this directive keep zero-match success semantics.
|
|
78
|
+
|
|
79
|
+
`exactly` must be a literal integer from 0 through 2147483647. It cannot be a
|
|
80
|
+
caller variable. Only mutation root fields may carry the directive, and a
|
|
81
|
+
guarded root cannot also use `@include` or `@skip`. Policy and constraint
|
|
82
|
+
failures retain their existing errors. This does not add automatic retries:
|
|
83
|
+
when the domain permits retry, reread authorized state and recompute the write.
|
|
84
|
+
Every writer affecting the invariant must participate in the revision protocol.
|
|
85
|
+
|
|
86
|
+
Guarded operations use **IR v2**; older runtimes reject them. Upgrade the host
|
|
87
|
+
compiler and executor together before registering guarded documents, regenerate
|
|
88
|
+
SDL with the matching SDK, and only then activate consumers. Existing unguarded
|
|
89
|
+
operations keep their IR v1 and operation hashes. This source addition requires
|
|
90
|
+
a matching SDK release and host rollout; it is not a claim that the published
|
|
91
|
+
Preview version already supports it.
|
|
92
|
+
|
|
93
|
+
### Execute by name
|
|
94
|
+
|
|
95
|
+
Operations execute by their generated manifest name, never by sending GraphQL
|
|
96
|
+
text or SQL:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
seq-studio orm execute GetOpenDeals . \
|
|
100
|
+
--variables-file variables.json \
|
|
101
|
+
-e <environment-name>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`variables.json` must contain a JSON object. The platform validates variables,
|
|
105
|
+
operation requirements, and row policies as the signed-in caller.
|
|
106
|
+
|
|
107
|
+
Execute uses the `/_data/v2` endpoint by default. Set
|
|
108
|
+
`SEQUENCE_ORM_ATLAS_EXECUTE_V2=1` to opt into Atlas's `/api/data/v2` endpoint.
|
|
109
|
+
This client rollout switch does not bypass server feature flags or authorization.
|
|
110
|
+
|
|
111
|
+
## Public exports
|
|
112
|
+
|
|
113
|
+
- `@sequenceholdings/orm/authoring` — namespace, table, policy, and view DSL
|
|
114
|
+
- `@sequenceholdings/orm/client` — persisted-operation client utilities
|
|
115
|
+
- `@sequenceholdings/orm/drizzle` — supported Drizzle helpers for views
|
|
116
|
+
- `@sequenceholdings/orm/codegen` — GraphQL Code Generator preset used by a
|
|
117
|
+
scaffolded namespace
|
package/dist/authoring.cjs
CHANGED
|
@@ -33,6 +33,7 @@ var ColumnBuilder = class {
|
|
|
33
33
|
hasDefault: false,
|
|
34
34
|
defaultValue: void 0,
|
|
35
35
|
defaultFn: void 0,
|
|
36
|
+
onUpdateFn: void 0,
|
|
36
37
|
description: void 0,
|
|
37
38
|
renamedFrom: void 0,
|
|
38
39
|
cast: void 0,
|
|
@@ -68,6 +69,15 @@ var ColumnBuilder = class {
|
|
|
68
69
|
this.state.defaultFn = void 0;
|
|
69
70
|
return this;
|
|
70
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Stamp this column with Postgres `now()` on every UPDATE. Only valid on
|
|
74
|
+
* timestamptz/date; the compiler drops it from SetInput so callers cannot
|
|
75
|
+
* supply a value.
|
|
76
|
+
*/
|
|
77
|
+
onUpdate(value) {
|
|
78
|
+
this.state.onUpdateFn = value;
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
71
81
|
description(text2) {
|
|
72
82
|
this.state.description = text2;
|
|
73
83
|
return this;
|
|
@@ -111,6 +121,7 @@ var ColumnBuilder = class {
|
|
|
111
121
|
};
|
|
112
122
|
if (this.state.hasDefault) column.default = this.state.defaultValue ?? null;
|
|
113
123
|
if (this.state.defaultFn !== void 0) column.defaultFn = this.state.defaultFn;
|
|
124
|
+
if (this.state.onUpdateFn !== void 0) column.onUpdateFn = this.state.onUpdateFn;
|
|
114
125
|
if (this.state.description !== void 0) column.description = this.state.description;
|
|
115
126
|
if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
|
|
116
127
|
if (this.state.cast !== void 0) column.cast = this.state.cast;
|
|
@@ -138,6 +149,7 @@ var t = {
|
|
|
138
149
|
// src/constants.ts
|
|
139
150
|
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
140
151
|
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
152
|
+
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
141
153
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
142
154
|
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
143
155
|
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
@@ -240,10 +252,10 @@ function isSeqLink(value) {
|
|
|
240
252
|
function isSeqTable(value) {
|
|
241
253
|
return typeof value === "object" && value !== null && value.kind === TABLE_BRAND;
|
|
242
254
|
}
|
|
243
|
-
function assertIdentifier({ value, what, file }) {
|
|
244
|
-
if (!
|
|
255
|
+
function assertIdentifier({ value, what, file, pattern = IDENTIFIER_PATTERN }) {
|
|
256
|
+
if (!pattern.test(value) || value.length > MAX_IDENTIFIER_LENGTH) {
|
|
245
257
|
throw new OrmDefinitionError(
|
|
246
|
-
`${what} "${value}" must match ${
|
|
258
|
+
`${what} "${value}" must match ${pattern} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
|
|
247
259
|
file
|
|
248
260
|
);
|
|
249
261
|
}
|
|
@@ -264,7 +276,7 @@ function defineTable(name, columns, config = {}) {
|
|
|
264
276
|
);
|
|
265
277
|
}
|
|
266
278
|
const columnName = builder.columnNameFor(propertyName);
|
|
267
|
-
assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file });
|
|
279
|
+
assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file, pattern: COLUMN_IDENTIFIER_PATTERN });
|
|
268
280
|
const existing = seenColumnNames.get(columnName);
|
|
269
281
|
if (existing !== void 0) {
|
|
270
282
|
throw new OrmDefinitionError(
|
|
@@ -304,7 +316,8 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
|
|
|
304
316
|
assertIdentifier({
|
|
305
317
|
value: columnName,
|
|
306
318
|
what: `synced table "${name}": column "${propertyName}" column name`,
|
|
307
|
-
file
|
|
319
|
+
file,
|
|
320
|
+
pattern: COLUMN_IDENTIFIER_PATTERN
|
|
308
321
|
});
|
|
309
322
|
const existing = seenColumnNames.get(columnName);
|
|
310
323
|
if (existing !== void 0) {
|
|
@@ -393,7 +406,7 @@ function defineView(name, columns, config) {
|
|
|
393
406
|
);
|
|
394
407
|
}
|
|
395
408
|
const columnName = builder.columnNameFor(propertyName);
|
|
396
|
-
if (!
|
|
409
|
+
if (!COLUMN_IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
|
|
397
410
|
throw new OrmDefinitionError(`view "${name}": column "${propertyName}" has invalid column name "${columnName}"`, file);
|
|
398
411
|
}
|
|
399
412
|
const existing = seenColumnNames.get(columnName);
|
|
@@ -842,6 +855,7 @@ function scalarColumnType(table, property, helper) {
|
|
|
842
855
|
var CANONICAL_FORMAT = 2;
|
|
843
856
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
844
857
|
var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
858
|
+
var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
845
859
|
var COLUMN_TYPES = [
|
|
846
860
|
"uuid",
|
|
847
861
|
"text",
|
|
@@ -862,18 +876,33 @@ var canonicalEnumSchema = zod.z.object({
|
|
|
862
876
|
var canonicalColumnSchema = zod.z.object({
|
|
863
877
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
864
878
|
name: zod.z.string().min(1).max(63),
|
|
865
|
-
columnName:
|
|
879
|
+
columnName: columnIdentifier,
|
|
866
880
|
type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
|
|
867
881
|
nullable: zod.z.boolean().default(true),
|
|
868
882
|
unique: zod.z.boolean().default(false),
|
|
869
883
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
870
884
|
default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
|
|
871
885
|
defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
|
|
886
|
+
/**
|
|
887
|
+
* Stamp this column from the database clock on UPDATE (`now()`). The
|
|
888
|
+
* column stays insertable (defaults fill gaps) but is omitted from
|
|
889
|
+
* SetInput / settable so callers cannot backdate it.
|
|
890
|
+
*/
|
|
891
|
+
onUpdateFn: zod.z.enum(["now()"]).optional(),
|
|
872
892
|
description: zod.z.string().max(500).optional(),
|
|
873
893
|
/** Migration hints — same semantics as v1. */
|
|
874
|
-
renamedFrom:
|
|
894
|
+
renamedFrom: columnIdentifier.optional(),
|
|
875
895
|
cast: zod.z.string().max(200).optional(),
|
|
876
896
|
deprecated: zod.z.boolean().default(false)
|
|
897
|
+
}).superRefine((column, ctx) => {
|
|
898
|
+
if (column.onUpdateFn === void 0) return;
|
|
899
|
+
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
900
|
+
ctx.addIssue({
|
|
901
|
+
code: "custom",
|
|
902
|
+
path: ["onUpdateFn"],
|
|
903
|
+
message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
|
|
904
|
+
});
|
|
905
|
+
}
|
|
877
906
|
});
|
|
878
907
|
var canonicalLinkSchema = zod.z.object({
|
|
879
908
|
name: zod.z.string().min(1).max(63),
|
|
@@ -881,15 +910,15 @@ var canonicalLinkSchema = zod.z.object({
|
|
|
881
910
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
882
911
|
target: identifier.optional(),
|
|
883
912
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
884
|
-
on:
|
|
913
|
+
on: columnIdentifier,
|
|
885
914
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
886
|
-
references:
|
|
915
|
+
references: columnIdentifier.optional(),
|
|
887
916
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
888
917
|
enforce: zod.z.boolean().default(false),
|
|
889
918
|
description: zod.z.string().max(500).optional()
|
|
890
919
|
});
|
|
891
920
|
var canonicalIndexSchema = zod.z.object({
|
|
892
|
-
columns: zod.z.array(
|
|
921
|
+
columns: zod.z.array(columnIdentifier).min(1),
|
|
893
922
|
unique: zod.z.boolean().default(false),
|
|
894
923
|
name: identifier.optional()
|
|
895
924
|
});
|
|
@@ -980,6 +1009,13 @@ var canonicalCowMutationSchema = zod.z.object({
|
|
|
980
1009
|
}
|
|
981
1010
|
}
|
|
982
1011
|
});
|
|
1012
|
+
var canonicalForeignKeySchema = zod.z.object({
|
|
1013
|
+
name: identifier,
|
|
1014
|
+
columns: zod.z.array(columnIdentifier).min(1).max(8),
|
|
1015
|
+
target: identifier,
|
|
1016
|
+
references: zod.z.array(columnIdentifier).min(1).max(8),
|
|
1017
|
+
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
|
|
1018
|
+
}).strict();
|
|
983
1019
|
var canonicalTableSchema = zod.z.object({
|
|
984
1020
|
table: identifier,
|
|
985
1021
|
description: zod.z.string().max(2e3).optional(),
|
|
@@ -988,7 +1024,7 @@ var canonicalTableSchema = zod.z.object({
|
|
|
988
1024
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
989
1025
|
source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
990
1026
|
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
991
|
-
primaryKey: zod.z.array(
|
|
1027
|
+
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
992
1028
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
993
1029
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
994
1030
|
policies: canonicalPoliciesSchema.default({}),
|
|
@@ -999,14 +1035,16 @@ var canonicalTableSchema = zod.z.object({
|
|
|
999
1035
|
*/
|
|
1000
1036
|
actions: zod.z.array(canonicalActionSchema).default([]),
|
|
1001
1037
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
1002
|
-
cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
|
|
1038
|
+
cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
|
|
1039
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
1040
|
+
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
|
|
1003
1041
|
});
|
|
1004
1042
|
var canonicalViewSchema = zod.z.object({
|
|
1005
1043
|
view: identifier,
|
|
1006
1044
|
description: zod.z.string().max(2e3).optional(),
|
|
1007
1045
|
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
1008
1046
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
1009
|
-
primaryKey: zod.z.array(
|
|
1047
|
+
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
1010
1048
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
1011
1049
|
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
1012
1050
|
query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
|
|
@@ -1019,6 +1057,9 @@ var canonicalViewSchema = zod.z.object({
|
|
|
1019
1057
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1020
1058
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
|
|
1021
1059
|
}
|
|
1060
|
+
if (column.onUpdateFn !== void 0) {
|
|
1061
|
+
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare onUpdateFn` });
|
|
1062
|
+
}
|
|
1022
1063
|
if (column.unique) {
|
|
1023
1064
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
|
|
1024
1065
|
}
|
|
@@ -1052,6 +1093,45 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
1052
1093
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
1053
1094
|
}
|
|
1054
1095
|
seen.add(table.table);
|
|
1096
|
+
if (table.source !== void 0) {
|
|
1097
|
+
for (const [columnIndex, column] of table.columns.entries()) {
|
|
1098
|
+
if (column.onUpdateFn !== void 0) {
|
|
1099
|
+
ctx.addIssue({
|
|
1100
|
+
code: "custom",
|
|
1101
|
+
path: ["tables", index, "columns", columnIndex],
|
|
1102
|
+
message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1109
|
+
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
1110
|
+
const names = /* @__PURE__ */ new Set([
|
|
1111
|
+
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
1112
|
+
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
1113
|
+
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
1114
|
+
]);
|
|
1115
|
+
for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
|
|
1116
|
+
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
|
|
1117
|
+
if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
|
|
1118
|
+
names.add(fk.name);
|
|
1119
|
+
const target = fkTables.get(fk.target);
|
|
1120
|
+
if (table.source || !target || target.source) {
|
|
1121
|
+
fail("foreign keys must connect owned tables in the same namespace");
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
|
|
1125
|
+
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
|
|
1126
|
+
for (const [position, name] of fk.columns.entries()) {
|
|
1127
|
+
const sourceColumn = table.columns.find((column) => column.columnName === name);
|
|
1128
|
+
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
1129
|
+
if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
|
|
1130
|
+
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
|
|
1131
|
+
}
|
|
1132
|
+
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
1133
|
+
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail("foreign key target columns must form a unique key");
|
|
1134
|
+
}
|
|
1055
1135
|
}
|
|
1056
1136
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1057
1137
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
@@ -1174,12 +1254,13 @@ function normalizeCanonical(definition) {
|
|
|
1174
1254
|
...definition,
|
|
1175
1255
|
enums: sortByName(definition.enums),
|
|
1176
1256
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
|
|
1177
|
-
const { cowMutations, ...rest } = t2;
|
|
1257
|
+
const { cowMutations, foreignKeys, ...rest } = t2;
|
|
1178
1258
|
return {
|
|
1179
1259
|
...rest,
|
|
1180
1260
|
links: sortByName(t2.links),
|
|
1181
1261
|
indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
1182
1262
|
actions: sortByName(t2.actions),
|
|
1263
|
+
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
1183
1264
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
1184
1265
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
1185
1266
|
};
|
|
@@ -1375,6 +1456,22 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1375
1456
|
} else if (columnLevelPk.length > 0) {
|
|
1376
1457
|
candidate.primaryKey = columnLevelPk;
|
|
1377
1458
|
}
|
|
1459
|
+
if (table.config.foreignKeys?.length) {
|
|
1460
|
+
if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
|
|
1461
|
+
candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
|
|
1462
|
+
const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
|
|
1463
|
+
throw new OrmDefinitionError(message, table.file);
|
|
1464
|
+
} });
|
|
1465
|
+
if (!isSeqTable(target) || !relationsByName.has(target.tableName)) throw new OrmDefinitionError("foreign key target must be a table in this namespace", table.file);
|
|
1466
|
+
return {
|
|
1467
|
+
name: fk.name,
|
|
1468
|
+
columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
|
|
1469
|
+
target: target.tableName,
|
|
1470
|
+
references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
|
|
1471
|
+
...fk.deferrable ? { deferrable: fk.deferrable } : {}
|
|
1472
|
+
};
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1378
1475
|
for (const index of table.config.indexes ?? []) {
|
|
1379
1476
|
const entry = {
|
|
1380
1477
|
columns: index.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
|
|
@@ -1427,6 +1524,12 @@ function buildCanonicalView({
|
|
|
1427
1524
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1428
1525
|
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
|
|
1429
1526
|
}
|
|
1527
|
+
if (column.onUpdateFn !== void 0) {
|
|
1528
|
+
throw new OrmDefinitionError(
|
|
1529
|
+
`view "${view.viewName}": column "${propertyName}" cannot declare onUpdateFn`,
|
|
1530
|
+
view.file
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1430
1533
|
if (column.unique) {
|
|
1431
1534
|
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
|
|
1432
1535
|
}
|
|
@@ -1685,7 +1788,16 @@ async function loadNamespaceFromDir(dir, options = {}) {
|
|
|
1685
1788
|
const jiti$1 = jiti.createJiti((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('authoring.cjs', document.baseURI).href)), {
|
|
1686
1789
|
interopDefault: true,
|
|
1687
1790
|
fsCache: false,
|
|
1688
|
-
...options.ormModulePath !== void 0 ? {
|
|
1791
|
+
...options.ormModulePath !== void 0 ? {
|
|
1792
|
+
alias: {
|
|
1793
|
+
// The public /drizzle barrel re-exports this pinned dependency.
|
|
1794
|
+
// Explicitly map it before the root alias: otherwise jiti appends
|
|
1795
|
+
// "/drizzle" to the overridden module's filename.
|
|
1796
|
+
"@sequenceholdings/orm/drizzle": module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('authoring.cjs', document.baseURI).href))).resolve("drizzle-orm"),
|
|
1797
|
+
"@sequenceholdings/orm": options.ormModulePath,
|
|
1798
|
+
"@sequenceholdings/orm/authoring": options.ormModulePath
|
|
1799
|
+
}
|
|
1800
|
+
} : {}
|
|
1689
1801
|
});
|
|
1690
1802
|
const context = beginAuthoringContext();
|
|
1691
1803
|
try {
|