@sequenceholdings/orm 0.1.0 → 0.1.2

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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/dist/authoring.cjs +794 -0
  3. package/dist/authoring.cjs.map +1 -0
  4. package/dist/authoring.d.cts +274 -0
  5. package/dist/authoring.d.ts +274 -0
  6. package/dist/authoring.js +763 -0
  7. package/dist/authoring.js.map +1 -0
  8. package/dist/canonical.cjs +146 -0
  9. package/dist/canonical.cjs.map +1 -0
  10. package/dist/canonical.d.cts +369 -0
  11. package/dist/canonical.d.ts +369 -0
  12. package/dist/canonical.js +131 -0
  13. package/dist/canonical.js.map +1 -0
  14. package/dist/cli.cjs +6208 -1693
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.d.cts +8 -1
  17. package/dist/cli.d.ts +8 -1
  18. package/dist/cli.js +6262 -1749
  19. package/dist/cli.js.map +1 -1
  20. package/dist/client.cjs +120 -0
  21. package/dist/client.cjs.map +1 -1
  22. package/dist/client.d.cts +74 -2
  23. package/dist/client.d.ts +74 -2
  24. package/dist/client.js +116 -1
  25. package/dist/client.js.map +1 -1
  26. package/dist/compiler.cjs +3001 -0
  27. package/dist/compiler.cjs.map +1 -0
  28. package/dist/compiler.d.cts +581 -0
  29. package/dist/compiler.d.ts +581 -0
  30. package/dist/compiler.js +2947 -0
  31. package/dist/compiler.js.map +1 -0
  32. package/dist/index.cjs +1730 -327
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +126 -56
  35. package/dist/index.d.ts +126 -56
  36. package/dist/index.js +1643 -324
  37. package/dist/index.js.map +1 -1
  38. package/dist/ir.cjs +407 -0
  39. package/dist/ir.cjs.map +1 -0
  40. package/dist/ir.d.cts +1179 -0
  41. package/dist/ir.d.ts +1179 -0
  42. package/dist/ir.js +388 -0
  43. package/dist/ir.js.map +1 -0
  44. package/dist/naming-CTvWSRTf.d.ts +79 -0
  45. package/dist/naming-DRyinU4u.d.cts +79 -0
  46. package/dist/sdl.cjs +833 -0
  47. package/dist/sdl.cjs.map +1 -0
  48. package/dist/sdl.d.cts +96 -0
  49. package/dist/sdl.d.ts +96 -0
  50. package/dist/sdl.js +809 -0
  51. package/dist/sdl.js.map +1 -0
  52. package/package.json +36 -3
  53. package/src/codegen-gql/apollo-hooks.cjs +98 -0
  54. package/src/codegen-gql/server-fns.cjs +101 -0
  55. package/templates/README.md +13 -0
  56. package/templates/orm-namespace/CLAUDE.md +81 -0
  57. package/templates/orm-namespace/codegen.ts +22 -0
  58. package/templates/orm-namespace/gitignore +5 -0
  59. package/templates/orm-namespace/graphql/example/queries.ts +56 -0
  60. package/templates/orm-namespace/package.json +24 -0
  61. package/templates/orm-namespace/schema/tables.ts +71 -0
  62. package/templates/orm-namespace/sequence.config.ts +32 -0
  63. package/templates/orm-namespace/tsconfig.json +15 -0
package/dist/index.js CHANGED
@@ -1,11 +1,863 @@
1
+ import { createHash } from 'crypto';
1
2
  import { z } from 'zod';
3
+ import { createJiti } from 'jiti';
4
+ import { realpath, stat, readdir, readFile, mkdir, writeFile } from 'fs/promises';
5
+ import { createRequire } from 'module';
6
+ import { join, sep, extname } from 'path';
2
7
  import { generateDrizzleJson, upPgSnapshot, generateMigration, pushSchema } from 'drizzle-kit/api';
3
- import { pgSchema, uniqueIndex, index, foreignKey, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
8
+ import { pgSchema, uniqueIndex, index, foreignKey, primaryKey, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
4
9
  import { sql } from 'drizzle-orm';
5
- import { createHash } from 'crypto';
6
- import { readFile, readdir, mkdir, writeFile } from 'fs/promises';
7
- import { join, extname } from 'path';
8
10
  import { load } from 'js-yaml';
11
+ import { capabilityPinsSchema, customRolesBlockSchema, validateCapabilityPins } from '@sequenceholdings/custom-roles-manifest';
12
+ export { capabilityPinsSchema, customRolesBlockSchema, customRolesDeclared, parseCapabilityPin, validateCapabilityPins } from '@sequenceholdings/custom-roles-manifest';
13
+
14
+ // src/authoring/columns.ts
15
+ var COLUMN_BRAND = "seq-column";
16
+ var ENUM_BRAND = "seq-enum";
17
+ function isEnumRef(value) {
18
+ return typeof value === "object" && value !== null && value.kind === ENUM_BRAND;
19
+ }
20
+ var COLUMN_DEFAULT_FNS = ["gen_random_uuid()", "now()"];
21
+ function deriveColumnName(propertyName) {
22
+ return propertyName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").toLowerCase();
23
+ }
24
+ var ColumnBuilder = class {
25
+ kind = COLUMN_BRAND;
26
+ state;
27
+ constructor(type, columnName) {
28
+ this.state = {
29
+ type,
30
+ columnName,
31
+ primaryKey: false,
32
+ notNull: false,
33
+ unique: false,
34
+ hasDefault: false,
35
+ defaultValue: void 0,
36
+ defaultFn: void 0,
37
+ description: void 0,
38
+ renamedFrom: void 0,
39
+ cast: void 0,
40
+ deprecated: false
41
+ };
42
+ }
43
+ /** Marks this column as (part of) the primary key. Implies NOT NULL. */
44
+ primaryKey() {
45
+ this.state.primaryKey = true;
46
+ return this;
47
+ }
48
+ notNull() {
49
+ this.state.notNull = true;
50
+ return this;
51
+ }
52
+ unique() {
53
+ this.state.unique = true;
54
+ return this;
55
+ }
56
+ /**
57
+ * Column default: a literal, or one of the allowlisted SQL functions
58
+ * (`'gen_random_uuid()'` / `'now()'`), which serialize as `defaultFn`.
59
+ */
60
+ default(value) {
61
+ if (value === "gen_random_uuid()" || value === "now()") {
62
+ this.state.defaultFn = value;
63
+ this.state.hasDefault = false;
64
+ this.state.defaultValue = void 0;
65
+ return this;
66
+ }
67
+ this.state.hasDefault = true;
68
+ this.state.defaultValue = value;
69
+ this.state.defaultFn = void 0;
70
+ return this;
71
+ }
72
+ description(text3) {
73
+ this.state.description = text3;
74
+ return this;
75
+ }
76
+ /** Migration rename hint: the previous Postgres column name. */
77
+ renamedFrom(previousColumnName) {
78
+ this.state.renamedFrom = previousColumnName;
79
+ return this;
80
+ }
81
+ /** Migration cast hint: the `USING` SQL applied when this column changes type. */
82
+ cast(usingSql) {
83
+ this.state.cast = usingSql;
84
+ return this;
85
+ }
86
+ /** Emits `@deprecated` in the generated SDL; the column stays queryable. */
87
+ deprecated() {
88
+ this.state.deprecated = true;
89
+ return this;
90
+ }
91
+ /** Whether `.primaryKey()` was called — collected into the table-level PK. */
92
+ get isPrimaryKey() {
93
+ return this.state.primaryKey;
94
+ }
95
+ /** The enum name when this is an enum column, else undefined. */
96
+ get enumName() {
97
+ return typeof this.state.type === "object" ? this.state.type.enum : void 0;
98
+ }
99
+ /** Postgres column name: explicit override, else snake_case of the property name. */
100
+ columnNameFor(propertyName) {
101
+ return this.state.columnName ?? deriveColumnName(propertyName);
102
+ }
103
+ /** Serialize to a canonical v2 column. Validation happens in serializeNamespace. */
104
+ toCanonicalColumn(propertyName) {
105
+ const column = {
106
+ name: propertyName,
107
+ columnName: this.columnNameFor(propertyName),
108
+ type: typeof this.state.type === "object" ? { enum: this.state.type.enum } : this.state.type,
109
+ nullable: !(this.state.notNull || this.state.primaryKey),
110
+ unique: this.state.unique,
111
+ deprecated: this.state.deprecated
112
+ };
113
+ if (this.state.hasDefault) column.default = this.state.defaultValue ?? null;
114
+ if (this.state.defaultFn !== void 0) column.defaultFn = this.state.defaultFn;
115
+ if (this.state.description !== void 0) column.description = this.state.description;
116
+ if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
117
+ if (this.state.cast !== void 0) column.cast = this.state.cast;
118
+ return column;
119
+ }
120
+ };
121
+ function isColumnBuilder(value) {
122
+ return typeof value === "object" && value !== null && value.kind === COLUMN_BRAND;
123
+ }
124
+ var scalar = (type) => (columnName) => new ColumnBuilder(type, columnName);
125
+ var t = {
126
+ uuid: scalar("uuid"),
127
+ text: scalar("text"),
128
+ integer: scalar("integer"),
129
+ bigint: scalar("bigint"),
130
+ numeric: scalar("numeric"),
131
+ double: scalar("double"),
132
+ boolean: scalar("boolean"),
133
+ timestamptz: scalar("timestamptz"),
134
+ date: scalar("date"),
135
+ jsonb: scalar("jsonb"),
136
+ enum: (ref, columnName) => new ColumnBuilder({ enum: ref.name }, columnName)
137
+ };
138
+
139
+ // src/constants.ts
140
+ var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
141
+ var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
142
+ var MAX_IDENTIFIER_LENGTH = 63;
143
+ var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
144
+ var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
145
+ var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
146
+ var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
147
+ var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
148
+ var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
149
+ var ORM_SCHEMA_PREFIX = "orm_";
150
+ var CONSUMER_ROLE_SUFFIX = "_consumer";
151
+ var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
152
+ var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
153
+ var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
154
+ var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
155
+ var HASH_DOMAIN = "sequence-orm-definitions\0";
156
+ var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
157
+ var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
158
+ var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
159
+
160
+ // src/errors.ts
161
+ var OrmDefinitionError = class extends Error {
162
+ constructor(message, file) {
163
+ super(file ? `${file}: ${message}` : message);
164
+ this.file = file;
165
+ }
166
+ name = "OrmDefinitionError";
167
+ };
168
+ var PredicateParseError = class extends Error {
169
+ constructor(message, position) {
170
+ super(`${message} (at position ${position})`);
171
+ this.position = position;
172
+ }
173
+ name = "PredicateParseError";
174
+ };
175
+
176
+ // src/authoring/registry.ts
177
+ var CONTEXT_STACK_KEY = Symbol.for("sequence.orm.authoring.context-stack.v2");
178
+ function contextStack() {
179
+ const holder = globalThis;
180
+ const existing = holder[CONTEXT_STACK_KEY];
181
+ if (Array.isArray(existing)) {
182
+ return existing;
183
+ }
184
+ const created = [];
185
+ holder[CONTEXT_STACK_KEY] = created;
186
+ return created;
187
+ }
188
+ function beginAuthoringContext() {
189
+ const context = { currentFile: void 0, tables: [], enums: [], configs: [] };
190
+ contextStack().push(context);
191
+ return context;
192
+ }
193
+ function endAuthoringContext(context) {
194
+ const stack = contextStack();
195
+ const top = stack[stack.length - 1];
196
+ if (top !== context) {
197
+ throw new Error("endAuthoringContext: the given context is not the active one \u2014 begin/end calls are mismatched");
198
+ }
199
+ stack.pop();
200
+ }
201
+ function activeAuthoringContext() {
202
+ const stack = contextStack();
203
+ return stack[stack.length - 1];
204
+ }
205
+ function register(bucket, value) {
206
+ const context = activeAuthoringContext();
207
+ if (!context) return;
208
+ context[bucket].push({ value, file: context.currentFile });
209
+ }
210
+ function registerTable(value) {
211
+ register("tables", value);
212
+ }
213
+ function registerEnum(value) {
214
+ register("enums", value);
215
+ }
216
+ function registerNamespaceConfig(value) {
217
+ register("configs", value);
218
+ }
219
+
220
+ // src/authoring/table.ts
221
+ var TABLE_BRAND = "seq-table";
222
+ var LINK_BRAND = "seq-link";
223
+ function toOne(target, options) {
224
+ return {
225
+ kind: LINK_BRAND,
226
+ linkKind: "toOne",
227
+ target,
228
+ on: options.on,
229
+ references: options.references,
230
+ enforce: options.enforce ?? false,
231
+ description: options.description
232
+ };
233
+ }
234
+ function toMany(target, options) {
235
+ return {
236
+ kind: LINK_BRAND,
237
+ linkKind: "toMany",
238
+ target,
239
+ on: options.on,
240
+ references: options.references,
241
+ enforce: false,
242
+ description: options.description
243
+ };
244
+ }
245
+ function toPlatformUser(options) {
246
+ return {
247
+ kind: LINK_BRAND,
248
+ linkKind: "toPlatformUser",
249
+ target: void 0,
250
+ on: options.on,
251
+ references: options.references,
252
+ enforce: false,
253
+ description: options.description
254
+ };
255
+ }
256
+ function isSeqLink(value) {
257
+ return typeof value === "object" && value !== null && value.kind === LINK_BRAND;
258
+ }
259
+ function isSeqTable(value) {
260
+ return typeof value === "object" && value !== null && value.kind === TABLE_BRAND;
261
+ }
262
+ function assertIdentifier({ value, what, file }) {
263
+ if (!IDENTIFIER_PATTERN.test(value) || value.length > MAX_IDENTIFIER_LENGTH) {
264
+ throw new OrmDefinitionError(
265
+ `${what} "${value}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
266
+ file
267
+ );
268
+ }
269
+ }
270
+ function defineTable(name, columns, config = {}) {
271
+ const file = activeAuthoringContext()?.currentFile;
272
+ assertIdentifier({ value: name, what: "table name", file });
273
+ const entries = Object.entries(columns);
274
+ if (entries.length === 0) {
275
+ throw new OrmDefinitionError(`table "${name}" declares no columns`, file);
276
+ }
277
+ const seenColumnNames = /* @__PURE__ */ new Map();
278
+ for (const [propertyName, builder] of entries) {
279
+ if (!isColumnBuilder(builder)) {
280
+ throw new OrmDefinitionError(
281
+ `table "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
282
+ file
283
+ );
284
+ }
285
+ const columnName = builder.columnNameFor(propertyName);
286
+ assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file });
287
+ const existing = seenColumnNames.get(columnName);
288
+ if (existing !== void 0) {
289
+ throw new OrmDefinitionError(
290
+ `table "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
291
+ file
292
+ );
293
+ }
294
+ seenColumnNames.set(columnName, propertyName);
295
+ }
296
+ const table = { kind: TABLE_BRAND, tableName: name, columns, config, source: void 0, file };
297
+ registerTable(table);
298
+ return table;
299
+ }
300
+ function defineSyncedTable(name, config) {
301
+ const file = activeAuthoringContext()?.currentFile;
302
+ assertIdentifier({ value: name, what: "table name", file });
303
+ if (!SYNCED_SOURCE_PATTERN.test(config.source)) {
304
+ throw new OrmDefinitionError(
305
+ `synced table "${name}": source "${config.source}" must match ${SYNCED_SOURCE_PATTERN} (gold layer only)`,
306
+ file
307
+ );
308
+ }
309
+ const table = {
310
+ kind: TABLE_BRAND,
311
+ tableName: name,
312
+ columns: {},
313
+ config: config.description !== void 0 ? { description: config.description } : {},
314
+ source: config.source,
315
+ file
316
+ };
317
+ registerTable(table);
318
+ return table;
319
+ }
320
+ function defineEnum(name, values) {
321
+ const file = activeAuthoringContext()?.currentFile;
322
+ assertIdentifier({ value: name, what: "enum name", file });
323
+ if (values.length === 0 || values.length > 100) {
324
+ throw new OrmDefinitionError(`enum "${name}" must declare between 1 and 100 values`, file);
325
+ }
326
+ const seen = /* @__PURE__ */ new Set();
327
+ for (const value of values) {
328
+ if (value.length === 0 || value.length > MAX_IDENTIFIER_LENGTH) {
329
+ throw new OrmDefinitionError(
330
+ `enum "${name}": value ${JSON.stringify(value)} must be 1-${MAX_IDENTIFIER_LENGTH} characters`,
331
+ file
332
+ );
333
+ }
334
+ if (seen.has(value)) {
335
+ throw new OrmDefinitionError(`enum "${name}" declares duplicate value "${value}"`, file);
336
+ }
337
+ seen.add(value);
338
+ }
339
+ const ref = { kind: ENUM_BRAND, name, values: [...values] };
340
+ registerEnum(ref);
341
+ return ref;
342
+ }
343
+ var CANONICAL_FORMAT = 2;
344
+ var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
345
+ var identifier = z.string().min(1).max(63).regex(IDENTIFIER_RE);
346
+ var COLUMN_TYPES = [
347
+ "uuid",
348
+ "text",
349
+ "integer",
350
+ "bigint",
351
+ "numeric",
352
+ "double",
353
+ "boolean",
354
+ "timestamptz",
355
+ "date",
356
+ "jsonb"
357
+ ];
358
+ var columnTypeSchema = z.enum(COLUMN_TYPES);
359
+ var canonicalEnumSchema = z.object({
360
+ name: identifier,
361
+ values: z.array(z.string().min(1).max(63)).min(1).max(100)
362
+ });
363
+ var canonicalColumnSchema = z.object({
364
+ /** Property name in TS (camelCase); columnName is the Postgres name. */
365
+ name: z.string().min(1).max(63),
366
+ columnName: identifier,
367
+ type: z.union([columnTypeSchema, z.object({ enum: identifier })]),
368
+ nullable: z.boolean().default(true),
369
+ unique: z.boolean().default(false),
370
+ /** Literal default, or one of the allowlisted SQL functions. */
371
+ default: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
372
+ defaultFn: z.enum(["gen_random_uuid()", "now()"]).optional(),
373
+ description: z.string().max(500).optional(),
374
+ /** Migration hints — same semantics as v1. */
375
+ renamedFrom: identifier.optional(),
376
+ cast: z.string().max(200).optional(),
377
+ deprecated: z.boolean().default(false)
378
+ });
379
+ var canonicalLinkSchema = z.object({
380
+ name: z.string().min(1).max(63),
381
+ kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
382
+ /** Target table name (namespace-local). Absent for toPlatformUser. */
383
+ target: identifier.optional(),
384
+ /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
385
+ on: identifier,
386
+ /** Referenced column on the target (default: target's PK / platform user `sub`). */
387
+ references: identifier.optional(),
388
+ /** enforce: true emits a real FK constraint (same-namespace toOne only). */
389
+ enforce: z.boolean().default(false),
390
+ description: z.string().max(500).optional()
391
+ });
392
+ var canonicalIndexSchema = z.object({
393
+ columns: z.array(identifier).min(1),
394
+ unique: z.boolean().default(false),
395
+ name: identifier.optional()
396
+ });
397
+ var policyVariantSchema = z.object({
398
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
399
+ when: z.string().min(1).max(1e3),
400
+ /** Exactly one of: allow-all bypass, or a row predicate. */
401
+ allow: z.literal(true).optional(),
402
+ policy: z.string().min(1).max(2e3).optional()
403
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
404
+ message: "A variant declares exactly one of allow: true or policy"
405
+ });
406
+ var policyExpressionSchema = z.union([
407
+ z.string().min(1).max(2e3),
408
+ z.object({
409
+ default: z.string().min(1).max(2e3),
410
+ variants: z.array(policyVariantSchema).max(10).default([])
411
+ })
412
+ ]);
413
+ var canonicalPoliciesSchema = z.object({
414
+ read: policyExpressionSchema.optional(),
415
+ write: policyExpressionSchema.optional()
416
+ });
417
+ var canonicalActionSchema = z.object({
418
+ name: identifier,
419
+ requires: z.string().max(1e3).optional(),
420
+ params: z.record(z.string(), z.object({
421
+ type: columnTypeSchema,
422
+ nullable: z.boolean().default(false)
423
+ })).default({}),
424
+ executes: z.string().min(1).max(1e4),
425
+ returns: z.enum(["rows", "rowsAffected", "scalar"]).default("rowsAffected"),
426
+ audit: z.object({ action: z.string(), resource: z.string() }).optional()
427
+ });
428
+ var canonicalTableSchema = z.object({
429
+ table: identifier,
430
+ description: z.string().max(2e3).optional(),
431
+ /** Table-level rename hint (new in v2). */
432
+ renamedFrom: identifier.optional(),
433
+ /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
434
+ source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
435
+ columns: z.array(canonicalColumnSchema).default([]),
436
+ primaryKey: z.array(identifier).min(1).max(8).optional(),
437
+ links: z.array(canonicalLinkSchema).default([]),
438
+ indexes: z.array(canonicalIndexSchema).default([]),
439
+ policies: canonicalPoliciesSchema.default({}),
440
+ actions: z.array(canonicalActionSchema).default([])
441
+ });
442
+ var canonicalDefinitionSchema = z.object({
443
+ definition_format: z.literal(CANONICAL_FORMAT),
444
+ namespace: identifier.max(41),
445
+ description: z.string().max(2e3).optional(),
446
+ enums: z.array(canonicalEnumSchema).default([]),
447
+ tables: z.array(canonicalTableSchema).min(0).max(500),
448
+ /** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
449
+ capabilities: z.object({
450
+ roles: z.unknown().optional(),
451
+ uses: z.array(z.string()).default([])
452
+ }).default({ uses: [] })
453
+ });
454
+ function normalizeCanonical(definition) {
455
+ const sortByName = (items) => [...items].sort((a, b) => a.name.localeCompare(b.name));
456
+ return {
457
+ ...definition,
458
+ enums: sortByName(definition.enums),
459
+ tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => ({
460
+ ...t2,
461
+ links: sortByName(t2.links),
462
+ indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
463
+ actions: sortByName(t2.actions)
464
+ }))
465
+ };
466
+ }
467
+ function stableJson(value) {
468
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
469
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
470
+ const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([a], [b]) => a < b ? -1 : 1).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJson(entryValue)}`);
471
+ return `{${entries.join(",")}}`;
472
+ }
473
+ function emptyToNull(value) {
474
+ if (value == null) return null;
475
+ if (Array.isArray(value)) return value.length === 0 ? null : value;
476
+ if (typeof value === "object" && Object.keys(value).length === 0) return null;
477
+ return value;
478
+ }
479
+ function canonicalTable(table) {
480
+ return {
481
+ table: table.table,
482
+ description: table.description ?? null,
483
+ columns: table.columns ?? null,
484
+ source: table.source ?? null,
485
+ links: emptyToNull(table.links),
486
+ indexes: emptyToNull(table.indexes),
487
+ policies: {
488
+ read: table.policies?.read?.source ?? null,
489
+ write: table.policies?.write?.source ?? null
490
+ },
491
+ actions: emptyToNull(
492
+ Object.fromEntries(
493
+ Object.entries(table.actions ?? {}).map(([name, action]) => [
494
+ name,
495
+ {
496
+ description: action.description ?? null,
497
+ requires: action.requires?.source ?? null,
498
+ params: emptyToNull(action.params),
499
+ validates: emptyToNull(action.validates?.map((entry) => entry.source)),
500
+ executes: action.executes,
501
+ returns: emptyToNull(action.returns),
502
+ audit: action.audit ?? null
503
+ }
504
+ ])
505
+ )
506
+ )
507
+ };
508
+ }
509
+ function canonicalCapabilities(capabilities) {
510
+ const { definitions, capabilities: declared } = capabilities.roles;
511
+ const isEmpty = Object.keys(definitions).length === 0 && Object.keys(declared).length === 0 && capabilities.uses.length === 0;
512
+ if (isEmpty) return null;
513
+ return {
514
+ roles: {
515
+ definitions: Object.fromEntries(
516
+ Object.entries(definitions).map(([key, def]) => [
517
+ key,
518
+ {
519
+ ...def.description !== void 0 ? { description: def.description } : {},
520
+ default: def.default,
521
+ inherits: [...def.inherits].sort(),
522
+ grants: [...def.grants].sort()
523
+ }
524
+ ])
525
+ ),
526
+ capabilities: Object.fromEntries(
527
+ Object.entries(declared).map(([key, cap]) => [
528
+ key,
529
+ cap.description !== void 0 ? { description: cap.description } : {}
530
+ ])
531
+ )
532
+ },
533
+ uses: [...capabilities.uses].sort()
534
+ };
535
+ }
536
+ function canonicalizeDefinition(definition) {
537
+ return {
538
+ namespace: definition.namespace,
539
+ description: definition.description,
540
+ capabilities: canonicalCapabilities(definition.capabilities),
541
+ tables: definition.tables.map(canonicalTable)
542
+ };
543
+ }
544
+ function computeDefinitionHash(definition) {
545
+ return createHash("sha256").update(HASH_DOMAIN).update(stableJson(canonicalizeDefinition(definition))).digest("hex");
546
+ }
547
+
548
+ // src/authoring/namespace.ts
549
+ var NAMESPACE_CONFIG_BRAND = "seq-namespace-config";
550
+ function isSeqNamespaceConfig(value) {
551
+ return typeof value === "object" && value !== null && value.kind === NAMESPACE_CONFIG_BRAND;
552
+ }
553
+ function defineNamespace(input) {
554
+ const file = activeAuthoringContext()?.currentFile;
555
+ if (!NAMESPACE_PATTERN.test(input.namespace)) {
556
+ throw new OrmDefinitionError(`namespace "${input.namespace}" must match ${NAMESPACE_PATTERN}`, file);
557
+ }
558
+ const config = {
559
+ kind: NAMESPACE_CONFIG_BRAND,
560
+ namespace: input.namespace,
561
+ description: input.description,
562
+ capabilities: input.capabilities ?? {},
563
+ file
564
+ };
565
+ registerNamespaceConfig(config);
566
+ return config;
567
+ }
568
+ function serializeNamespace(input) {
569
+ const { config, tables } = input;
570
+ const enums = input.enums ?? [];
571
+ const tablesByName = /* @__PURE__ */ new Map();
572
+ for (const table of tables) {
573
+ const existing = tablesByName.get(table.tableName);
574
+ if (existing) {
575
+ throw new OrmDefinitionError(
576
+ `duplicate table "${table.tableName}" (defined in ${describeFile(existing)} and ${describeFile(table)})`,
577
+ table.file
578
+ );
579
+ }
580
+ tablesByName.set(table.tableName, table);
581
+ }
582
+ const enumsByName = /* @__PURE__ */ new Map();
583
+ for (const ref of enums) {
584
+ if (enumsByName.has(ref.name)) {
585
+ throw new OrmDefinitionError(`duplicate enum "${ref.name}"`, config.file);
586
+ }
587
+ enumsByName.set(ref.name, ref);
588
+ }
589
+ const canonicalTables = tables.map((table) => buildCanonicalTable({ table, tablesByName, enumsByName }));
590
+ const canonicalEnums = [...enumsByName.values()].map((ref) => ({
591
+ name: ref.name,
592
+ values: [...ref.values]
593
+ }));
594
+ const candidate = {
595
+ definition_format: CANONICAL_FORMAT,
596
+ namespace: config.namespace,
597
+ enums: canonicalEnums,
598
+ tables: canonicalTables,
599
+ capabilities: {
600
+ ...config.capabilities.roles !== void 0 ? { roles: config.capabilities.roles } : {},
601
+ uses: [...config.capabilities.uses ?? []]
602
+ }
603
+ };
604
+ if (config.description !== void 0) candidate.description = config.description;
605
+ let parsed;
606
+ try {
607
+ parsed = canonicalDefinitionSchema.parse(candidate);
608
+ } catch (error) {
609
+ if (error instanceof z.ZodError) {
610
+ throw new OrmDefinitionError(`namespace "${config.namespace}": ${formatZodIssues(error)}`, config.file);
611
+ }
612
+ throw error;
613
+ }
614
+ return normalizeCanonical(parsed);
615
+ }
616
+ function computeCanonicalHash(definition) {
617
+ return createHash("sha256").update(HASH_DOMAIN).update(stableJson(definition)).digest("hex");
618
+ }
619
+ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
620
+ const candidate = {
621
+ table: table.tableName,
622
+ columns: [],
623
+ links: [],
624
+ indexes: [],
625
+ policies: {},
626
+ actions: []
627
+ };
628
+ if (table.config.description !== void 0) candidate.description = table.config.description;
629
+ if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
630
+ if (table.source !== void 0) candidate.source = table.source;
631
+ const columnLevelPk = [];
632
+ for (const [propertyName, builder] of Object.entries(table.columns)) {
633
+ const column = builder.toCanonicalColumn(propertyName);
634
+ const enumName = builder.enumName;
635
+ if (enumName !== void 0 && !enumsByName.has(enumName)) {
636
+ throw new OrmDefinitionError(
637
+ `table "${table.tableName}": column "${propertyName}" references enum "${enumName}" which is not passed to serializeNamespace \u2014 declare it with defineEnum and include it`,
638
+ table.file
639
+ );
640
+ }
641
+ if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
642
+ candidate.columns?.push(column);
643
+ }
644
+ if (table.config.primaryKey !== void 0) {
645
+ if (columnLevelPk.length > 0) {
646
+ throw new OrmDefinitionError(
647
+ `table "${table.tableName}": declare the primary key either with .primaryKey() on columns or with config.primaryKey \u2014 not both`,
648
+ table.file
649
+ );
650
+ }
651
+ candidate.primaryKey = table.config.primaryKey.map(
652
+ (ref) => resolveColumnRef({ table, ref, role: "primaryKey" })
653
+ );
654
+ } else if (columnLevelPk.length > 0) {
655
+ candidate.primaryKey = columnLevelPk;
656
+ }
657
+ for (const index3 of table.config.indexes ?? []) {
658
+ const entry = {
659
+ columns: index3.columns.map((ref) => resolveColumnRef({ table, ref, role: "index" }))
660
+ };
661
+ if (index3.unique !== void 0) entry.unique = index3.unique;
662
+ if (index3.name !== void 0) entry.name = index3.name;
663
+ candidate.indexes?.push(entry);
664
+ }
665
+ for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
666
+ candidate.links?.push(buildCanonicalLink({ table, linkName, link, tablesByName }));
667
+ }
668
+ const policies = {};
669
+ if (table.config.policies?.read !== void 0) policies.read = toCanonicalPolicy(table.config.policies.read);
670
+ if (table.config.policies?.write !== void 0) policies.write = toCanonicalPolicy(table.config.policies.write);
671
+ candidate.policies = policies;
672
+ for (const action of table.config.actions ?? []) {
673
+ candidate.actions?.push({
674
+ name: action.name,
675
+ ...action.requires !== void 0 ? { requires: action.requires } : {},
676
+ params: Object.fromEntries(
677
+ Object.entries(action.params ?? {}).map(([name, param]) => [
678
+ name,
679
+ { type: param.type, nullable: param.nullable ?? false }
680
+ ])
681
+ ),
682
+ executes: action.executes,
683
+ returns: action.returns ?? "rowsAffected",
684
+ ...action.audit !== void 0 ? { audit: action.audit } : {}
685
+ });
686
+ }
687
+ try {
688
+ return canonicalTableSchema.parse(candidate);
689
+ } catch (error) {
690
+ if (error instanceof z.ZodError) {
691
+ throw new OrmDefinitionError(`table "${table.tableName}": ${formatZodIssues(error)}`, table.file);
692
+ }
693
+ throw error;
694
+ }
695
+ }
696
+ function buildCanonicalLink({
697
+ table,
698
+ linkName,
699
+ link,
700
+ tablesByName
701
+ }) {
702
+ const fail = (message) => {
703
+ throw new OrmDefinitionError(`table "${table.tableName}": link "${linkName}" ${message}`, table.file);
704
+ };
705
+ if (link.enforce && link.linkKind !== "toOne") {
706
+ return fail("sets enforce: true, which is only supported on toOne links");
707
+ }
708
+ if (link.linkKind === "toPlatformUser") {
709
+ if (link.target !== void 0) return fail("is toPlatformUser and must not declare a target table");
710
+ return {
711
+ name: linkName,
712
+ kind: "toPlatformUser",
713
+ on: resolveColumnRef({ table, ref: link.on, role: `link "${linkName}" on` }),
714
+ ...link.references !== void 0 ? { references: deriveColumnName(link.references) } : {},
715
+ enforce: false,
716
+ ...link.description !== void 0 ? { description: link.description } : {}
717
+ };
718
+ }
719
+ const target = resolveTableRef({ ref: link.target, fail });
720
+ if (!tablesByName.has(target.tableName)) {
721
+ return fail(`targets table "${target.tableName}" which is not part of this namespace`);
722
+ }
723
+ const owningSide = link.linkKind === "toOne" ? table : target;
724
+ return {
725
+ name: linkName,
726
+ kind: link.linkKind,
727
+ target: target.tableName,
728
+ on: resolveColumnRef({ table: owningSide, ref: link.on, role: `link "${linkName}" on` }),
729
+ ...link.references !== void 0 ? { references: resolveColumnRef({ table: target, ref: link.references, role: `link "${linkName}" references` }) } : {},
730
+ enforce: link.enforce,
731
+ ...link.description !== void 0 ? { description: link.description } : {}
732
+ };
733
+ }
734
+ function resolveTableRef({ ref, fail }) {
735
+ if (ref === void 0) return fail("declares no target table");
736
+ if (typeof ref !== "function") return ref;
737
+ let resolved;
738
+ try {
739
+ resolved = ref();
740
+ } catch (error) {
741
+ return fail(
742
+ `target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
743
+ );
744
+ }
745
+ if (!isSeqTable(resolved)) return fail("target thunk did not return a table defined with defineTable");
746
+ return resolved;
747
+ }
748
+ function resolveColumnRef({ table, ref, role }) {
749
+ const direct = table.columns[ref];
750
+ if (direct) return direct.columnNameFor(ref);
751
+ for (const [propertyName, builder] of Object.entries(table.columns)) {
752
+ if (builder.columnNameFor(propertyName) === ref) return ref;
753
+ }
754
+ const known = Object.keys(table.columns).join(", ");
755
+ throw new OrmDefinitionError(
756
+ `table "${table.tableName}": ${role} references unknown column "${ref}" (known columns: ${known || "none"})`,
757
+ table.file
758
+ );
759
+ }
760
+ function toCanonicalPolicy(policy) {
761
+ if (typeof policy === "string") return policy;
762
+ return {
763
+ default: policy.default,
764
+ variants: (policy.variants ?? []).map((variant) => ({
765
+ when: variant.when,
766
+ ...variant.allow === true ? { allow: true } : {},
767
+ ...variant.policy !== void 0 ? { policy: variant.policy } : {}
768
+ }))
769
+ };
770
+ }
771
+ function describeFile(table) {
772
+ return table.file ?? "<unknown file>";
773
+ }
774
+ function formatZodIssues(error) {
775
+ return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
776
+ }
777
+ var NAMESPACE_CONFIG_FILE = "sequence.config.ts";
778
+ var SCHEMA_DIR = "schema";
779
+ async function loadNamespaceFromDir(dir, options = {}) {
780
+ const root = await realpath(dir).catch(() => {
781
+ throw new OrmDefinitionError(`namespace directory not found: ${dir}`);
782
+ });
783
+ const configPath = join(root, NAMESPACE_CONFIG_FILE);
784
+ const configStat = await stat(configPath).catch(() => void 0);
785
+ if (!configStat?.isFile()) {
786
+ throw new OrmDefinitionError(`${NAMESPACE_CONFIG_FILE} not found in ${dir}`);
787
+ }
788
+ const schemaFiles = await listSchemaFiles(root);
789
+ const files = [NAMESPACE_CONFIG_FILE, ...schemaFiles];
790
+ purgeModuleCache(root);
791
+ const jiti = createJiti(import.meta.url, {
792
+ interopDefault: true,
793
+ fsCache: false,
794
+ ...options.ormModulePath !== void 0 ? { alias: { "@sequenceholdings/orm": options.ormModulePath } } : {}
795
+ });
796
+ const context = beginAuthoringContext();
797
+ try {
798
+ for (const file of files) {
799
+ context.currentFile = file;
800
+ try {
801
+ await jiti.import(join(root, file));
802
+ } catch (error) {
803
+ if (error instanceof Error && error.name === "OrmDefinitionError") throw error;
804
+ throw new OrmDefinitionError(
805
+ `failed to load: ${error instanceof Error ? error.message : String(error)}`,
806
+ file
807
+ );
808
+ }
809
+ }
810
+ } finally {
811
+ context.currentFile = void 0;
812
+ endAuthoringContext(context);
813
+ }
814
+ const configs = [];
815
+ for (const entry of context.configs) {
816
+ if (isSeqNamespaceConfig(entry.value)) configs.push({ value: entry.value, file: entry.file });
817
+ }
818
+ const configEntry = configs[0];
819
+ if (!configEntry) {
820
+ throw new OrmDefinitionError(
821
+ "no namespace config registered \u2014 sequence.config.ts must call defineNamespace(...) at import time",
822
+ NAMESPACE_CONFIG_FILE
823
+ );
824
+ }
825
+ if (configs.length > 1) {
826
+ const where = configs.map((entry) => entry.file ?? "<unknown file>").join(", ");
827
+ throw new OrmDefinitionError(
828
+ `defineNamespace called ${configs.length} times (${where}) \u2014 declare exactly one namespace config`
829
+ );
830
+ }
831
+ const tables = [];
832
+ for (const entry of context.tables) {
833
+ if (isSeqTable(entry.value)) tables.push(entry.value);
834
+ }
835
+ if (tables.length === 0) {
836
+ throw new OrmDefinitionError(
837
+ `no tables registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) at import time`
838
+ );
839
+ }
840
+ const enums = [];
841
+ for (const entry of context.enums) {
842
+ if (isEnumRef(entry.value)) enums.push(entry.value);
843
+ }
844
+ const canonical = serializeNamespace({ config: configEntry.value, tables, enums });
845
+ return { canonical, hash: computeCanonicalHash(canonical), files };
846
+ }
847
+ async function listSchemaFiles(root) {
848
+ const schemaDir = join(root, SCHEMA_DIR);
849
+ const entries = await readdir(schemaDir, { withFileTypes: true }).catch(() => []);
850
+ return entries.filter(
851
+ (entry) => entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") && !entry.name.startsWith(".")
852
+ ).map((entry) => join(SCHEMA_DIR, entry.name)).sort();
853
+ }
854
+ function purgeModuleCache(root) {
855
+ const require2 = createRequire(import.meta.url);
856
+ const prefix = root.endsWith(sep) ? root : `${root}${sep}`;
857
+ for (const key of Object.keys(require2.cache)) {
858
+ if (key.startsWith(prefix)) delete require2.cache[key];
859
+ }
860
+ }
9
861
 
10
862
  // src/codegen/types.ts
11
863
  var WIRE_TYPES = {
@@ -22,7 +874,7 @@ var WIRE_TYPES = {
22
874
  };
23
875
  function generateNamespaceTypes(definition) {
24
876
  const lines = [
25
- `// Generated by \`seq-studio orm generate\` from the '${definition.namespace}' definitions \u2014 do not edit.`,
877
+ `// Generated by \`seq-studio orm apply\` from the '${definition.namespace}' definitions \u2014 do not edit.`,
26
878
  "",
27
879
  `export type ${typesInterfaceName(definition.namespace)} = {`
28
880
  ];
@@ -84,43 +936,6 @@ function columnWireType(column) {
84
936
  return notNull || base === "unknown" ? base : `${base} | null`;
85
937
  }
86
938
 
87
- // src/constants.ts
88
- var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
89
- var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
90
- var MAX_IDENTIFIER_LENGTH = 63;
91
- var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
92
- var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
93
- var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
94
- var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
95
- var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
96
- var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
97
- var ORM_SCHEMA_PREFIX = "orm_";
98
- var CONSUMER_ROLE_SUFFIX = "_consumer";
99
- var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
100
- var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
101
- var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
102
- var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
103
- var HASH_DOMAIN = "sequence-orm-definitions\0";
104
- var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
105
- var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
106
- var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
107
-
108
- // src/errors.ts
109
- var OrmDefinitionError = class extends Error {
110
- constructor(message, file) {
111
- super(file ? `${file}: ${message}` : message);
112
- this.file = file;
113
- }
114
- name = "OrmDefinitionError";
115
- };
116
- var PredicateParseError = class extends Error {
117
- constructor(message, position) {
118
- super(`${message} (at position ${position})`);
119
- this.position = position;
120
- }
121
- name = "PredicateParseError";
122
- };
123
-
124
939
  // src/predicate/ast.ts
125
940
  var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
126
941
  function walkPredicate(predicate, visit) {
@@ -159,7 +974,7 @@ var tableReferenceSchema = z.string().refine(
159
974
  var isQualifiedReference = (reference) => reference.includes(".");
160
975
 
161
976
  // src/schema/columns.ts
162
- var COLUMN_TYPES = [
977
+ var COLUMN_TYPES2 = [
163
978
  "uuid",
164
979
  "text",
165
980
  "integer",
@@ -171,7 +986,7 @@ var COLUMN_TYPES = [
171
986
  "date",
172
987
  "jsonb"
173
988
  ];
174
- var columnTypeSchema = z.enum(COLUMN_TYPES);
989
+ var columnTypeSchema2 = z.enum(COLUMN_TYPES2);
175
990
  var FUNCTION_DEFAULTS = {
176
991
  "gen_random_uuid()": ["uuid"],
177
992
  "now()": ["timestamptz", "date"]
@@ -200,7 +1015,7 @@ function defaultIssue(value, type) {
200
1015
  return null;
201
1016
  }
202
1017
  var columnSchema = z.strictObject({
203
- type: columnTypeSchema,
1018
+ type: columnTypeSchema2,
204
1019
  primary_key: z.boolean().optional(),
205
1020
  nullable: z.boolean().optional(),
206
1021
  unique: z.boolean().optional(),
@@ -467,7 +1282,7 @@ var NAMED_PARAM_PATTERN = /(?<!:):([a-z][a-z0-9_]*)/g;
467
1282
  var QUOTED_STRING_PATTERN = /'(?:[^']|'')*'/g;
468
1283
  var SQL_COMMENT_PATTERN = /--|\/\*/;
469
1284
  var actionParamSchema = z.strictObject({
470
- type: columnTypeSchema,
1285
+ type: columnTypeSchema2,
471
1286
  optional: z.boolean().optional(),
472
1287
  description: z.string().optional()
473
1288
  });
@@ -500,7 +1315,7 @@ var actionSchema = z.strictObject({
500
1315
  // function returns those rows (typed in the client); absent → the action
501
1316
  // reports rows_affected only. The column names/types must match the
502
1317
  // RETURNING list, which Postgres enforces at CREATE FUNCTION.
503
- returns: z.record(identifierSchema, columnTypeSchema).optional(),
1318
+ returns: z.record(identifierSchema, columnTypeSchema2).optional(),
504
1319
  audit: auditSchema.optional()
505
1320
  }).superRefine((action, ctx) => {
506
1321
  const params = Object.keys(action.params ?? {});
@@ -517,30 +1332,53 @@ var actionSchema = z.strictObject({
517
1332
  if (action.returns && Object.keys(action.returns).length === 0) {
518
1333
  ctx.addIssue({ code: "custom", message: "returns must declare at least one column", path: ["returns"] });
519
1334
  }
520
- for (const [index2, entry] of (action.validates ?? []).entries()) {
1335
+ for (const [index3, entry] of (action.validates ?? []).entries()) {
521
1336
  if (!params.includes(entry.param)) {
522
1337
  ctx.addIssue({
523
1338
  code: "custom",
524
1339
  message: `validates references undeclared param '${entry.param}'`,
525
- path: ["validates", index2]
1340
+ path: ["validates", index3]
526
1341
  });
527
1342
  }
528
1343
  }
529
1344
  });
530
- function extractNamedParams(sql2) {
531
- const withoutStrings = sql2.replace(QUOTED_STRING_PATTERN, "''");
1345
+ function extractNamedParams(sql3) {
1346
+ const withoutStrings = sql3.replace(QUOTED_STRING_PATTERN, "''");
532
1347
  const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
533
1348
  return [...new Set(names)];
534
1349
  }
535
1350
  function actionStatementSources(executes) {
536
1351
  return (Array.isArray(executes) ? executes : [executes]).map((statement) => statement.trim());
537
1352
  }
1353
+ var WRITE_TARGET_PATTERNS = [
1354
+ /^insert\s+into\s+(?:"?[a-z_][a-z0-9_]*"?\.)?"?([a-z_][a-z0-9_]*)"?/i,
1355
+ /^update\s+(?:only\s+)?(?:"?[a-z_][a-z0-9_]*"?\.)?"?([a-z_][a-z0-9_]*)"?/i,
1356
+ /^delete\s+from\s+(?:only\s+)?(?:"?[a-z_][a-z0-9_]*"?\.)?"?([a-z_][a-z0-9_]*)"?/i
1357
+ ];
1358
+ function actionWriteTargets(action) {
1359
+ const targets = [];
1360
+ const seen = /* @__PURE__ */ new Set();
1361
+ for (const statement of actionStatementSources(action.executes)) {
1362
+ const masked = statement.replace(QUOTED_STRING_PATTERN, "''");
1363
+ for (const pattern of WRITE_TARGET_PATTERNS) {
1364
+ const match = pattern.exec(masked);
1365
+ if (!match) continue;
1366
+ const table = match[1].toLowerCase();
1367
+ if (!seen.has(table)) {
1368
+ seen.add(table);
1369
+ targets.push(table);
1370
+ }
1371
+ break;
1372
+ }
1373
+ }
1374
+ return targets;
1375
+ }
538
1376
  function executesIssues(executes, declaredParams, hasReturns) {
539
1377
  const issues = [];
540
1378
  const statements = actionStatementSources(executes);
541
- statements.forEach((statement, index2) => {
1379
+ statements.forEach((statement, index3) => {
542
1380
  const withoutStrings = statement.replace(QUOTED_STRING_PATTERN, "''");
543
- const at = statements.length > 1 ? ` (statement ${index2 + 1})` : "";
1381
+ const at = statements.length > 1 ? ` (statement ${index3 + 1})` : "";
544
1382
  if (!EXECUTES_VERB_PATTERN.test(statement)) issues.push(`executes must be INSERT, UPDATE, or DELETE${at}`);
545
1383
  if (withoutStrings.replace(/;\s*$/, "").includes(";")) issues.push(`each executes entry must be a single statement${at}`);
546
1384
  if (SQL_COMMENT_PATTERN.test(withoutStrings)) issues.push(`executes must not contain SQL comments${at}`);
@@ -550,7 +1388,7 @@ function executesIssues(executes, declaredParams, hasReturns) {
550
1388
  if (BARE_CLAIM_CALL_PATTERN.test(withoutStrings.replace(MASKED_CLAIM_CALL_PATTERN, ""))) {
551
1389
  issues.push(`executes has a malformed claim()/capability() call \u2014 use e.g. claim('sub')${at}`);
552
1390
  }
553
- if (RETURNING_PATTERN.test(withoutStrings) && !(hasReturns && index2 === statements.length - 1)) {
1391
+ if (RETURNING_PATTERN.test(withoutStrings) && !(hasReturns && index3 === statements.length - 1)) {
554
1392
  issues.push(
555
1393
  hasReturns ? `only the last executes statement may use RETURNING${at}` : `executes must not use RETURNING unless the action declares returns${at}`
556
1394
  );
@@ -660,8 +1498,8 @@ var actionParamArg = (name) => `p_${name}`;
660
1498
  function compileActionStatements(action, renderParam = actionParamArg) {
661
1499
  return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
662
1500
  }
663
- function compileStatement(sql2, renderParam) {
664
- const compiled = sql2.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
1501
+ function compileStatement(sql3, renderParam) {
1502
+ const compiled = sql3.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
665
1503
  if (claimKey !== void 0) {
666
1504
  if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
667
1505
  return claimExpression(claimKey);
@@ -714,9 +1552,9 @@ function actionPrepareChecks(definition) {
714
1552
  const order = actionParamOrder(action);
715
1553
  const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
716
1554
  const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
717
- compiled.forEach((sql2, index2) => {
718
- const checked = action.returns && index2 === compiled.length - 1 ? returnProjection(action.returns, sql2) : sql2;
719
- statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index2}`)}${signature} AS ${checked}`);
1555
+ compiled.forEach((sql3, index3) => {
1556
+ const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql3) : sql3;
1557
+ statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index3}`)}${signature} AS ${checked}`);
720
1558
  });
721
1559
  }
722
1560
  return statements;
@@ -831,7 +1669,7 @@ var indexSchema = z.union([
831
1669
  unique: z.boolean().optional(),
832
1670
  name: identifierSchema.optional()
833
1671
  })
834
- ]).transform((index2) => Array.isArray(index2) ? { columns: index2 } : index2);
1672
+ ]).transform((index3) => Array.isArray(index3) ? { columns: index3 } : index3);
835
1673
  var policiesSchema = z.strictObject({
836
1674
  read: parsedPredicateSchema.optional(),
837
1675
  write: parsedPredicateSchema.optional()
@@ -935,15 +1773,15 @@ function validateColumnTable(table, ctx) {
935
1773
  });
936
1774
  }
937
1775
  }
938
- for (const [position, index2] of (table.indexes ?? []).entries()) {
939
- if (new Set(index2.columns).size !== index2.columns.length) {
1776
+ for (const [position, index3] of (table.indexes ?? []).entries()) {
1777
+ if (new Set(index3.columns).size !== index3.columns.length) {
940
1778
  ctx.addIssue({
941
1779
  code: "custom",
942
1780
  message: "index names a column more than once",
943
1781
  path: ["indexes", position]
944
1782
  });
945
1783
  }
946
- for (const column of index2.columns) {
1784
+ for (const column of index3.columns) {
947
1785
  if (!names.includes(column)) {
948
1786
  ctx.addIssue({
949
1787
  code: "custom",
@@ -985,8 +1823,8 @@ function validateDerivedNameLengths(table, ctx) {
985
1823
  path: ["table"]
986
1824
  });
987
1825
  }
988
- for (const [position, index2] of (table.indexes ?? []).entries()) {
989
- const name = indexName(table.table, index2);
1826
+ for (const [position, index3] of (table.indexes ?? []).entries()) {
1827
+ const name = indexName(table.table, index3);
990
1828
  if (name.length > MAX_IDENTIFIER_LENGTH) {
991
1829
  ctx.addIssue({
992
1830
  code: "custom",
@@ -1226,9 +2064,9 @@ function sortByEnforcedLinks(tables) {
1226
2064
  var enforcedTargets = (table) => Object.values(table.links ?? {}).filter((link) => link.enforce && !isQualifiedReference(link.to) && link.to !== table.table).map((link) => link.to);
1227
2065
 
1228
2066
  // src/engine/snapshot.ts
1229
- function generateSnapshot(definition, previousId2) {
2067
+ function generateSnapshot(definition, previousId3) {
1230
2068
  const { imports, schemaName } = buildDrizzleEntities(definition);
1231
- return generateDrizzleJson(imports, previousId2, [schemaName]);
2069
+ return generateDrizzleJson(imports, previousId3, [schemaName]);
1232
2070
  }
1233
2071
  var emptySnapshot = (namespace) => generateDrizzleJson({}, void 0, [ormSchemaName(namespace)]);
1234
2072
  var CURRENT_SNAPSHOT_VERSION = generateDrizzleJson({}).version;
@@ -1380,264 +2218,596 @@ async function guardedRun(run) {
1380
2218
  process.exit = originalExit;
1381
2219
  }
1382
2220
  }
1383
-
1384
- // src/engine/index.ts
1385
- var STATEMENT_KINDS = ["hint", "ddl", "security", "function"];
1386
- async function planMigration({
1387
- definition,
1388
- previousSnapshot
1389
- }) {
1390
- const schema = ormSchemaName(definition.namespace);
1391
- const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(definition.namespace);
1392
- const hints = applyMigrationHints(previous, definition);
1393
- const snapshot = generateSnapshot(definition, previousId(previous));
1394
- const ddl = await diffStatements(hints.adjusted, snapshot);
1395
- const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
1396
- const statements = [
1397
- ...policyDrops.map((sql2) => ({ kind: "security", sql: sql2 })),
1398
- ...hints.statements.map((sql2) => ({ kind: "hint", sql: sql2 })),
1399
- ...ddl.map((sql2) => ({ kind: "ddl", sql: sql2.replace(/;\s*$/, "") })),
1400
- ...securityStatements(definition).map((sql2) => ({ kind: "security", sql: sql2 })),
1401
- // PREPARE each action's executes first (plpgsql bodies don't check columns
1402
- // at CREATE), then create the functions. Both reference the tables the DDL
1403
- // above creates, so they close the plan.
1404
- ...actionPrepareChecks(definition).map((sql2) => ({ kind: "function", sql: sql2 })),
1405
- ...actionFunctionStatements(definition).map((sql2) => ({ kind: "function", sql: sql2 }))
1406
- ];
1407
- return { namespace: definition.namespace, schema, statements, snapshot };
1408
- }
1409
- var compileNamespace = (definition) => planMigration({ definition });
1410
- var previousId = (snapshot) => snapshot.id;
1411
- function stableJson(value) {
1412
- if (value === null || typeof value !== "object") return JSON.stringify(value);
1413
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
1414
- const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([a], [b]) => a < b ? -1 : 1).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJson(entryValue)}`);
1415
- return `{${entries.join(",")}}`;
1416
- }
1417
- function emptyToNull(value) {
1418
- if (value == null) return null;
1419
- if (Array.isArray(value)) return value.length === 0 ? null : value;
1420
- if (typeof value === "object" && Object.keys(value).length === 0) return null;
1421
- return value;
1422
- }
1423
- function canonicalTable(table) {
1424
- return {
1425
- table: table.table,
1426
- description: table.description ?? null,
1427
- columns: table.columns ?? null,
1428
- source: table.source ?? null,
1429
- links: emptyToNull(table.links),
1430
- indexes: emptyToNull(table.indexes),
1431
- policies: {
1432
- read: table.policies?.read?.source ?? null,
1433
- write: table.policies?.write?.source ?? null
1434
- },
1435
- actions: emptyToNull(
1436
- Object.fromEntries(
1437
- Object.entries(table.actions ?? {}).map(([name, action]) => [
1438
- name,
1439
- {
1440
- description: action.description ?? null,
1441
- requires: action.requires?.source ?? null,
1442
- params: emptyToNull(action.params),
1443
- validates: emptyToNull(action.validates?.map((entry) => entry.source)),
1444
- executes: action.executes,
1445
- returns: emptyToNull(action.returns),
1446
- audit: action.audit ?? null
1447
- }
1448
- ])
1449
- )
1450
- )
1451
- };
1452
- }
1453
- function canonicalCapabilities(capabilities) {
1454
- const { definitions, capabilities: declared } = capabilities.roles;
1455
- const isEmpty = Object.keys(definitions).length === 0 && Object.keys(declared).length === 0 && capabilities.uses.length === 0;
1456
- if (isEmpty) return null;
1457
- return {
1458
- roles: {
1459
- definitions: Object.fromEntries(
1460
- Object.entries(definitions).map(([key, def]) => [
1461
- key,
1462
- {
1463
- ...def.description !== void 0 ? { description: def.description } : {},
1464
- default: def.default,
1465
- inherits: [...def.inherits].sort(),
1466
- grants: [...def.grants].sort()
1467
- }
1468
- ])
1469
- ),
1470
- capabilities: Object.fromEntries(
1471
- Object.entries(declared).map(([key, cap]) => [
1472
- key,
1473
- cap.description !== void 0 ? { description: cap.description } : {}
1474
- ])
1475
- )
1476
- },
1477
- uses: [...capabilities.uses].sort()
1478
- };
2221
+
2222
+ // src/engine/index.ts
2223
+ var STATEMENT_KINDS = ["hint", "ddl", "security", "function"];
2224
+ async function planMigration({
2225
+ definition,
2226
+ previousSnapshot
2227
+ }) {
2228
+ const schema = ormSchemaName(definition.namespace);
2229
+ const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(definition.namespace);
2230
+ const hints = applyMigrationHints(previous, definition);
2231
+ const snapshot = generateSnapshot(definition, previousId(previous));
2232
+ const ddl = await diffStatements(hints.adjusted, snapshot);
2233
+ const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
2234
+ const statements = [
2235
+ ...policyDrops.map((sql3) => ({ kind: "security", sql: sql3 })),
2236
+ ...hints.statements.map((sql3) => ({ kind: "hint", sql: sql3 })),
2237
+ ...ddl.map((sql3) => ({ kind: "ddl", sql: sql3.replace(/;\s*$/, "") })),
2238
+ ...securityStatements(definition).map((sql3) => ({ kind: "security", sql: sql3 })),
2239
+ // PREPARE each action's executes first (plpgsql bodies don't check columns
2240
+ // at CREATE), then create the functions. Both reference the tables the DDL
2241
+ // above creates, so they close the plan.
2242
+ ...actionPrepareChecks(definition).map((sql3) => ({ kind: "function", sql: sql3 })),
2243
+ ...actionFunctionStatements(definition).map((sql3) => ({ kind: "function", sql: sql3 }))
2244
+ ];
2245
+ return { namespace: definition.namespace, schema, statements, snapshot };
1479
2246
  }
1480
- function canonicalizeDefinition(definition) {
1481
- return {
1482
- namespace: definition.namespace,
1483
- description: definition.description,
1484
- capabilities: canonicalCapabilities(definition.capabilities),
1485
- tables: definition.tables.map(canonicalTable)
1486
- };
2247
+ var compileNamespace = (definition) => planMigration({ definition });
2248
+ var previousId = (snapshot) => snapshot.id;
2249
+ var FACTORIES2 = {
2250
+ uuid: (name) => uuid(name),
2251
+ text: (name) => text(name),
2252
+ integer: (name) => integer(name),
2253
+ // Mode 'bigint' keeps int8 lossless — mode 'number' silently rounds past
2254
+ // 2^53. The read executor serializes BigInt to string for JSON.
2255
+ bigint: (name) => bigint(name, { mode: "bigint" }),
2256
+ numeric: (name) => numeric(name),
2257
+ double: (name) => doublePrecision(name),
2258
+ boolean: (name) => boolean(name),
2259
+ timestamptz: (name) => timestamp(name, { withTimezone: true }),
2260
+ date: (name) => date(name),
2261
+ jsonb: (name) => jsonb(name)
2262
+ };
2263
+ var SCHEMA_IMPORT_KEY2 = "$schema";
2264
+ var enumImportKey = (name) => `$enum_${name}`;
2265
+ var columnTablesV2 = (definition) => definition.tables.filter((table) => !table.source);
2266
+ function buildDrizzleEntitiesV2(definition) {
2267
+ const schemaName = ormSchemaName(definition.namespace);
2268
+ const schema = pgSchema(schemaName);
2269
+ const imports = { [SCHEMA_IMPORT_KEY2]: schema };
2270
+ const tables = {};
2271
+ const built = /* @__PURE__ */ new Map();
2272
+ const enums = /* @__PURE__ */ new Map();
2273
+ for (const enumDefinition of definition.enums) {
2274
+ const values = enumDefinition.values;
2275
+ const builtEnum = schema.enum(enumDefinition.name, values);
2276
+ enums.set(enumDefinition.name, builtEnum);
2277
+ imports[enumImportKey(enumDefinition.name)] = builtEnum;
2278
+ }
2279
+ const byName = new Map(definition.tables.map((table) => [table.table, table]));
2280
+ for (const table of sortByEnforcedLinks2(columnTablesV2(definition))) {
2281
+ const drizzleTable = buildTable2(schema, table, built, enums, byName);
2282
+ built.set(table.table, drizzleTable);
2283
+ imports[table.table] = drizzleTable;
2284
+ tables[table.table] = drizzleTable;
2285
+ }
2286
+ return { imports, tables, schemaName };
1487
2287
  }
1488
- function computeDefinitionHash(definition) {
1489
- return createHash("sha256").update(HASH_DOMAIN).update(stableJson(canonicalizeDefinition(definition))).digest("hex");
2288
+ function generateSnapshotV2(definition, previousId3) {
2289
+ const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
2290
+ return generateDrizzleJson(imports, previousId3, [schemaName]);
1490
2291
  }
1491
- var CUSTOM_ROLE_KEY_RE = /^[a-z0-9-]+$/;
1492
- var customRoleKeySchema = z.string().min(1).max(64).regex(
1493
- CUSTOM_ROLE_KEY_RE,
1494
- "Role and capability keys must match [a-z0-9-]+"
1495
- );
1496
- var MAX_CUSTOM_ROLE_DEFINITIONS = 50;
1497
- var MAX_CUSTOM_ROLE_CAPABILITIES = 100;
1498
- var customRoleCapabilityDeclarationSchema = z.object({
1499
- description: z.string().max(500).optional()
1500
- });
1501
- var customRoleDefinitionSchema = z.object({
1502
- description: z.string().max(500).optional(),
1503
- /**
1504
- * Grants this role to the host's entire runtime audience. Resolved at the
1505
- * app layer at claim time — every caller who passes the host floor
1506
- * (`end_user`) holds the default roles; no tuple is written for it.
1507
- */
1508
- default: z.boolean().default(false),
1509
- /** Role keys whose grants this role also receives (privilege waterfall). */
1510
- inherits: z.array(customRoleKeySchema).default([]),
1511
- /** Capability keys this role grants directly. */
1512
- grants: z.array(customRoleKeySchema).default([])
1513
- });
1514
- var customRolesBlockSchema = z.object({
1515
- /** Roles grantable to principals, keyed by role key. */
1516
- definitions: z.record(customRoleKeySchema, customRoleDefinitionSchema).default({}),
1517
- /** Leaf capability keys the host's code checks, keyed by capability key. */
1518
- capabilities: z.record(customRoleKeySchema, customRoleCapabilityDeclarationSchema).default({})
1519
- }).superRefine((block, ctx) => {
1520
- const roleKeys = Object.keys(block.definitions);
1521
- const capabilityKeys = new Set(Object.keys(block.capabilities));
1522
- if (roleKeys.length > MAX_CUSTOM_ROLE_DEFINITIONS) {
1523
- ctx.addIssue({
1524
- code: "custom",
1525
- message: `At most ${MAX_CUSTOM_ROLE_DEFINITIONS} role definitions are allowed`,
1526
- path: ["definitions"]
1527
- });
2292
+ function buildTable2(schema, table, built, enums, byName) {
2293
+ const primaryKeyColumns = table.primaryKey ?? [];
2294
+ if (primaryKeyColumns.length === 0) {
2295
+ throw new OrmDefinitionError(`table '${table.table}' declares no primaryKey`);
1528
2296
  }
1529
- if (capabilityKeys.size > MAX_CUSTOM_ROLE_CAPABILITIES) {
1530
- ctx.addIssue({
1531
- code: "custom",
1532
- message: `At most ${MAX_CUSTOM_ROLE_CAPABILITIES} capabilities are allowed`,
1533
- path: ["capabilities"]
1534
- });
2297
+ const columnNames = new Set(table.columns.map((column) => column.columnName));
2298
+ for (const name of primaryKeyColumns) {
2299
+ if (!columnNames.has(name)) {
2300
+ throw new OrmDefinitionError(`primaryKey of '${table.table}' references unknown column '${name}'`);
2301
+ }
1535
2302
  }
1536
- for (const key of roleKeys) {
1537
- if (capabilityKeys.has(key)) {
1538
- ctx.addIssue({
1539
- code: "custom",
1540
- message: `Key "${key}" is declared as both a role and a capability`,
1541
- path: ["definitions", key]
1542
- });
2303
+ const singlePrimaryKey = primaryKeyColumns.length === 1 ? primaryKeyColumns[0] : void 0;
2304
+ const compositeMembers = new Set(singlePrimaryKey === void 0 ? primaryKeyColumns : []);
2305
+ const columns = Object.fromEntries(
2306
+ table.columns.map((column) => [
2307
+ column.columnName,
2308
+ buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums })
2309
+ ])
2310
+ );
2311
+ const drizzleTable = schema.table(table.table, columns, (self) => [
2312
+ ...buildCompositePrimaryKey(table, primaryKeyColumns, self),
2313
+ ...buildIndexes2(table, self),
2314
+ ...buildForeignKeys2(table, self, built, byName)
2315
+ ]);
2316
+ return drizzleTable.enableRLS();
2317
+ }
2318
+ function resolveColumnFactory(table, column, enums) {
2319
+ if (typeof column.type === "string") return FACTORIES2[column.type];
2320
+ const enumFactory = enums.get(column.type.enum);
2321
+ if (!enumFactory) {
2322
+ throw new OrmDefinitionError(`column '${table}.${column.columnName}' references undeclared enum '${column.type.enum}'`);
2323
+ }
2324
+ return enumFactory;
2325
+ }
2326
+ function buildColumnV2({
2327
+ table,
2328
+ column,
2329
+ singlePrimaryKey,
2330
+ compositeMembers,
2331
+ enums
2332
+ }) {
2333
+ const factory = resolveColumnFactory(table, column, enums);
2334
+ let builder = factory(column.columnName);
2335
+ const isSinglePrimaryKey = column.columnName === singlePrimaryKey;
2336
+ if (isSinglePrimaryKey) builder = builder.primaryKey();
2337
+ if ((column.nullable === false || compositeMembers.has(column.columnName)) && !isSinglePrimaryKey) {
2338
+ builder = builder.notNull();
2339
+ }
2340
+ if (column.unique && !isSinglePrimaryKey) builder = builder.unique();
2341
+ if (column.defaultFn !== void 0 && column.default !== void 0) {
2342
+ throw new OrmDefinitionError(`column '${table}.${column.columnName}' declares both default and defaultFn`);
2343
+ }
2344
+ if (column.defaultFn !== void 0) {
2345
+ if (typeof column.type !== "string" || !isFunctionDefault(column.defaultFn, column.type)) {
2346
+ throw new OrmDefinitionError(
2347
+ `column '${table}.${column.columnName}' declares defaultFn '${column.defaultFn}' which is invalid for its type`
2348
+ );
1543
2349
  }
2350
+ builder = builder.default(sql.raw(column.defaultFn));
2351
+ } else if (column.default !== void 0 && column.default !== null) {
2352
+ builder = builder.default(column.default);
1544
2353
  }
1545
- const roleKeySet = new Set(roleKeys);
1546
- for (const [roleKey, definition] of Object.entries(block.definitions)) {
1547
- const seenInherits = /* @__PURE__ */ new Set();
1548
- for (const inherited of definition.inherits) {
1549
- if (seenInherits.has(inherited)) {
1550
- ctx.addIssue({
1551
- code: "custom",
1552
- message: `Role "${roleKey}" lists "${inherited}" in inherits more than once`,
1553
- path: ["definitions", roleKey, "inherits"]
1554
- });
1555
- }
1556
- seenInherits.add(inherited);
1557
- if (!roleKeySet.has(inherited)) {
1558
- ctx.addIssue({
1559
- code: "custom",
1560
- message: `Role "${roleKey}" inherits unknown role "${inherited}"`,
1561
- path: ["definitions", roleKey, "inherits"]
1562
- });
2354
+ return builder;
2355
+ }
2356
+ function buildCompositePrimaryKey(table, primaryKeyColumns, self) {
2357
+ if (primaryKeyColumns.length < 2) return [];
2358
+ return [
2359
+ primaryKey({
2360
+ name: `${table.table}_pkey`,
2361
+ columns: primaryKeyColumns.map((column) => self[column])
2362
+ })
2363
+ ];
2364
+ }
2365
+ function buildIndexes2(table, self) {
2366
+ return table.indexes.map((definition) => {
2367
+ for (const column of definition.columns) {
2368
+ if (!(column in self)) {
2369
+ throw new OrmDefinitionError(`index on '${table.table}' references unknown column '${column}'`);
1563
2370
  }
1564
2371
  }
1565
- const seenGrants = /* @__PURE__ */ new Set();
1566
- for (const granted of definition.grants) {
1567
- if (seenGrants.has(granted)) {
1568
- ctx.addIssue({
1569
- code: "custom",
1570
- message: `Role "${roleKey}" lists "${granted}" in grants more than once`,
1571
- path: ["definitions", roleKey, "grants"]
1572
- });
1573
- }
1574
- seenGrants.add(granted);
1575
- if (!capabilityKeys.has(granted)) {
1576
- ctx.addIssue({
1577
- code: "custom",
1578
- message: `Role "${roleKey}" grants unknown capability "${granted}"`,
1579
- path: ["definitions", roleKey, "grants"]
1580
- });
1581
- }
2372
+ const name = indexName(table.table, definition);
2373
+ const factory = definition.unique ? uniqueIndex(name) : index(name);
2374
+ const columns = definition.columns.map((column) => self[column]);
2375
+ return factory.on(...columns);
2376
+ });
2377
+ }
2378
+ function buildForeignKeys2(table, self, built, byName) {
2379
+ return table.links.filter((link) => link.enforce).map((link) => {
2380
+ if (link.kind !== "toOne" || !link.target) {
2381
+ throw new OrmDefinitionError(
2382
+ `enforced link '${link.name}' on '${table.table}' must be a toOne link with a namespace-local target`
2383
+ );
1582
2384
  }
1583
- }
1584
- const state = /* @__PURE__ */ new Map();
1585
- const visit = (key, path) => {
1586
- if (state.get(key) === "done") return null;
1587
- if (state.get(key) === "visiting") return [...path, key];
1588
- state.set(key, "visiting");
1589
- const definition = block.definitions[key];
1590
- for (const inherited of definition?.inherits ?? []) {
1591
- if (!roleKeySet.has(inherited)) continue;
1592
- const cycle = visit(inherited, [...path, key]);
1593
- if (cycle) return cycle;
2385
+ const targetTable = byName.get(link.target);
2386
+ const target = link.target === table.table ? self : built.get(link.target);
2387
+ if (!targetTable || !target) {
2388
+ throw new OrmDefinitionError(`enforced link '${link.name}' on '${table.table}' has no built target '${link.target}'`);
1594
2389
  }
1595
- state.set(key, "done");
1596
- return null;
1597
- };
1598
- for (const roleKey of roleKeys) {
1599
- const cycle = visit(roleKey, []);
1600
- if (cycle) {
1601
- ctx.addIssue({
1602
- code: "custom",
1603
- message: `Role inheritance cycle: ${cycle.join(" -> ")}`,
1604
- path: ["definitions"]
1605
- });
1606
- break;
2390
+ const referencedColumn = link.references ?? singlePrimaryKeyOf(targetTable);
2391
+ if (!referencedColumn) {
2392
+ throw new OrmDefinitionError(
2393
+ `enforced link '${link.name}' on '${table.table}' targets '${link.target}' whose primary key is composite \u2014 declare 'references'`
2394
+ );
2395
+ }
2396
+ const targetColumn = target[referencedColumn];
2397
+ const sourceColumn = self[link.on];
2398
+ if (!targetColumn || !sourceColumn) {
2399
+ throw new OrmDefinitionError(
2400
+ `enforced link '${link.name}' on '${table.table}' references unknown column '${targetColumn ? link.on : referencedColumn}'`
2401
+ );
2402
+ }
2403
+ return foreignKey({
2404
+ name: foreignKeyName(table.table, link.name),
2405
+ columns: [sourceColumn],
2406
+ foreignColumns: [targetColumn]
2407
+ });
2408
+ });
2409
+ }
2410
+ var singlePrimaryKeyOf = (table) => table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
2411
+ function sortByEnforcedLinks2(tables) {
2412
+ const remaining = new Map(tables.map((table) => [table.table, table]));
2413
+ const sorted = [];
2414
+ const placed = /* @__PURE__ */ new Set();
2415
+ while (remaining.size > 0) {
2416
+ const ready = [...remaining.values()].filter(
2417
+ (table) => enforcedTargets2(table).every((target) => placed.has(target) || !remaining.has(target))
2418
+ );
2419
+ if (ready.length === 0) {
2420
+ throw new OrmDefinitionError(`enforced links form a cycle among: ${[...remaining.keys()].join(", ")}`);
2421
+ }
2422
+ for (const table of ready) {
2423
+ sorted.push(table);
2424
+ placed.add(table.table);
2425
+ remaining.delete(table.table);
1607
2426
  }
1608
2427
  }
1609
- });
1610
- function customRolesDeclared(block) {
1611
- if (!block) return false;
1612
- return Object.keys(block.definitions).length > 0 || Object.keys(block.capabilities).length > 0;
1613
- }
1614
- var CUSTOM_CAPABILITY_PIN_RE = /^(?:org\/)?[a-z0-9-]+$/;
1615
- var MAX_CAPABILITY_PINS = 100;
1616
- var capabilityPinsSchema = z.array(
1617
- z.string().min(1).max(68).regex(
1618
- CUSTOM_CAPABILITY_PIN_RE,
1619
- 'Pins must be "org/<key>" or a bare "<key>" matching [a-z0-9-]+'
2428
+ return sorted;
2429
+ }
2430
+ var enforcedTargets2 = (table) => table.links.flatMap(
2431
+ (link) => (
2432
+ // A self-target is not an ordering dependency — the FK builds inline.
2433
+ link.enforce && link.target !== void 0 && link.target !== table.table ? [link.target] : []
1620
2434
  )
1621
- ).max(MAX_CAPABILITY_PINS).default([]);
1622
- function parseCapabilityPin(pin) {
1623
- return pin.startsWith("org/") ? { scope: "org", key: pin.slice("org/".length) } : { scope: "host", key: pin };
2435
+ );
2436
+ function applyMigrationHintsV2(previous, definition) {
2437
+ const adjusted = structuredClone(previous);
2438
+ const statements = [];
2439
+ const schemaName = ormSchemaName(definition.namespace);
2440
+ const qualified = (table) => `${quoteIdentifier(schemaName)}.${quoteIdentifier(table)}`;
2441
+ const enumsByName = new Map(definition.enums.map((entry) => [entry.name, entry]));
2442
+ for (const table of columnTablesV2(definition)) {
2443
+ if (table.renamedFrom) {
2444
+ applyTableRename({ adjusted, schemaName, table, renamedFrom: table.renamedFrom, statements, qualified });
2445
+ }
2446
+ const snapshotTable = findTable2(adjusted, schemaName, table.table);
2447
+ if (!snapshotTable) continue;
2448
+ for (const column of table.columns) {
2449
+ const name = column.columnName;
2450
+ if (column.renamedFrom) {
2451
+ const previousColumn = snapshotTable.columns[column.renamedFrom];
2452
+ if (previousColumn && snapshotTable.columns[name]) {
2453
+ throw new OrmDefinitionError(
2454
+ `'${table.table}.${name}' declares renamedFrom '${column.renamedFrom}', but both columns exist in the previous schema \u2014 a rename cannot overwrite a live column`
2455
+ );
2456
+ }
2457
+ if (previousColumn) {
2458
+ statements.push(
2459
+ `ALTER TABLE ${qualified(table.table)} RENAME COLUMN ${quoteIdentifier(column.renamedFrom)} TO ${quoteIdentifier(name)}`
2460
+ );
2461
+ delete snapshotTable.columns[column.renamedFrom];
2462
+ snapshotTable.columns[name] = { ...previousColumn, name };
2463
+ } else if (!snapshotTable.columns[name]) {
2464
+ throw new OrmDefinitionError(
2465
+ `'${table.table}.${name}' declares renamedFrom '${column.renamedFrom}', but the previous schema has neither column \u2014 check the declared source name`
2466
+ );
2467
+ }
2468
+ }
2469
+ const cast = column.cast ? castParts(column.cast) : null;
2470
+ if (cast) {
2471
+ const current = snapshotTable.columns[name];
2472
+ const { snapshotType, sqlType } = castTargetTypes(column, enumsByName, schemaName, table.table);
2473
+ if (current && current.type !== snapshotType) {
2474
+ statements.push(
2475
+ `ALTER TABLE ${qualified(table.table)} ALTER COLUMN ${quoteIdentifier(name)} TYPE ${sqlType} USING ${quoteIdentifier(name)}::${sqlType}`
2476
+ );
2477
+ current.type = snapshotType;
2478
+ }
2479
+ }
2480
+ }
2481
+ }
2482
+ return { statements, adjusted };
1624
2483
  }
1625
- function validateCapabilityPins({
1626
- uses,
1627
- roles
2484
+ function castTargetTypes(column, enums, schemaName, table) {
2485
+ if (typeof column.type === "string") {
2486
+ const name = sqlTypeName(column.type);
2487
+ return { snapshotType: name, sqlType: name };
2488
+ }
2489
+ const enumName = column.type.enum;
2490
+ if (!enums.has(enumName)) {
2491
+ throw new OrmDefinitionError(`column '${table}.${column.columnName}' casts to undeclared enum '${enumName}'`);
2492
+ }
2493
+ return { snapshotType: enumName, sqlType: `${quoteIdentifier(schemaName)}.${quoteIdentifier(enumName)}` };
2494
+ }
2495
+ function applyTableRename({
2496
+ adjusted,
2497
+ schemaName,
2498
+ table,
2499
+ renamedFrom,
2500
+ statements,
2501
+ qualified
1628
2502
  }) {
1629
- const errors = [];
1630
- const seen = /* @__PURE__ */ new Set();
1631
- const roleKeys = new Set(Object.keys(roles?.definitions ?? {}));
1632
- for (const pin of uses) {
1633
- if (seen.has(pin)) errors.push(`Pin "${pin}" is listed more than once`);
1634
- seen.add(pin);
1635
- const parsed = parseCapabilityPin(pin);
1636
- if (parsed.scope === "host" && roleKeys.has(parsed.key)) {
1637
- errors.push(`Pin "${pin}" references a role \u2014 roles are never pinned`);
1638
- }
2503
+ const tables = adjusted.tables ?? {};
2504
+ const previousEntry = Object.entries(tables).find(
2505
+ ([, entry]) => entry.schema === schemaName && entry.name === renamedFrom
2506
+ );
2507
+ const currentTable = findTable2(adjusted, schemaName, table.table);
2508
+ if (previousEntry && currentTable) {
2509
+ throw new OrmDefinitionError(
2510
+ `'${table.table}' declares renamedFrom '${renamedFrom}', but both tables exist in the previous schema \u2014 a rename cannot overwrite a live table`
2511
+ );
2512
+ }
2513
+ if (!previousEntry) {
2514
+ if (currentTable) return;
2515
+ throw new OrmDefinitionError(
2516
+ `'${table.table}' declares renamedFrom '${renamedFrom}', but the previous schema has neither table \u2014 check the declared source name`
2517
+ );
2518
+ }
2519
+ statements.push(`ALTER TABLE ${qualified(renamedFrom)} RENAME TO ${quoteIdentifier(table.table)}`);
2520
+ const [previousKey, previousTable] = previousEntry;
2521
+ delete tables[previousKey];
2522
+ tables[`${schemaName}.${table.table}`] = { ...previousTable, name: table.table };
2523
+ }
2524
+ function findTable2(snapshot, schema, table) {
2525
+ const tables = snapshot.tables ?? {};
2526
+ return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
2527
+ }
2528
+ async function planMigrationV2({
2529
+ canonical,
2530
+ previousSnapshot
2531
+ }) {
2532
+ const schema = ormSchemaName(canonical.namespace);
2533
+ const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
2534
+ const hints = applyMigrationHintsV2(previous, canonical);
2535
+ const snapshot = generateSnapshotV2(canonical, previousId2(previous));
2536
+ const ddl = await diffStatements(hints.adjusted, snapshot);
2537
+ const statements = [
2538
+ ...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
2539
+ ...ddl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") }))
2540
+ ];
2541
+ return { namespace: canonical.namespace, schema, statements, snapshot };
2542
+ }
2543
+ var previousId2 = (snapshot) => snapshot.id;
2544
+ var IR_VERSION = 1;
2545
+ var CLAIMS_CONTRACT = {
2546
+ sub: "string",
2547
+ org: "string?",
2548
+ roles: "string[]",
2549
+ capabilities: "string[]"
2550
+ };
2551
+ var scalarLiteral = z.union([z.string(), z.number(), z.boolean(), z.null()]);
2552
+ var valueIrSchema = z.discriminatedUnion("kind", [
2553
+ z.object({ kind: z.literal("literal"), value: z.union([scalarLiteral, z.array(scalarLiteral)]) }),
2554
+ z.object({ kind: z.literal("variable"), name: z.string() }),
2555
+ z.object({ kind: z.literal("claim"), key: z.enum(["sub", "org"]) })
2556
+ ]);
2557
+ var CMP_OPS = [
2558
+ "eq",
2559
+ "neq",
2560
+ "gt",
2561
+ "gte",
2562
+ "lt",
2563
+ "lte",
2564
+ "in",
2565
+ "nin",
2566
+ "like",
2567
+ "ilike",
2568
+ "isNull",
2569
+ "contains",
2570
+ "hasKey"
2571
+ ];
2572
+ var predIrSchema = z.lazy(
2573
+ () => z.discriminatedUnion("kind", [
2574
+ z.object({ kind: z.literal("and"), operands: z.array(predIrSchema).min(1) }),
2575
+ z.object({ kind: z.literal("or"), operands: z.array(predIrSchema).min(1) }),
2576
+ z.object({ kind: z.literal("not"), operand: predIrSchema }),
2577
+ z.object({ kind: z.literal("cmp"), op: z.enum(CMP_OPS), column: z.string(), value: valueIrSchema }),
2578
+ z.object({ kind: z.literal("exists"), link: z.string(), where: predIrSchema }),
2579
+ z.object({
2580
+ kind: z.literal("aggPredicate"),
2581
+ link: z.string(),
2582
+ fn: z.literal("count"),
2583
+ op: z.enum(["eq", "neq", "gt", "gte", "lt", "lte"]),
2584
+ value: z.number(),
2585
+ where: predIrSchema.optional()
2586
+ }),
2587
+ z.object({ kind: z.literal("capability"), key: z.string() }),
2588
+ z.object({ kind: z.literal("role"), key: z.string() }),
2589
+ z.object({ kind: z.literal("dynamicFilter"), slot: z.string() }),
2590
+ z.object({ kind: z.literal("const"), value: z.boolean() })
2591
+ ])
2592
+ );
2593
+ var orderIrSchema = z.discriminatedUnion("kind", [
2594
+ z.object({
2595
+ kind: z.literal("column"),
2596
+ column: z.string(),
2597
+ /** Order through a to-one link: `link` is set and `column` is on the target. */
2598
+ link: z.string().optional(),
2599
+ direction: z.enum(["asc", "desc"]),
2600
+ nulls: z.enum(["first", "last"]).optional()
2601
+ }),
2602
+ /** §4f: `$orderBy: [FooOrderBy!]` variable — validated + lowered at runtime. */
2603
+ z.object({ kind: z.literal("dynamic"), slot: z.string() })
2604
+ ]);
2605
+ var limitIrSchema = z.object({
2606
+ /** Fixed value or a variable, clamped to [1, max]; absent → default. */
2607
+ variable: z.string().optional(),
2608
+ value: z.number().int().positive().optional(),
2609
+ default: z.number().int().positive(),
2610
+ max: z.number().int().positive()
2611
+ });
2612
+ var selectionIrSchema = z.lazy(
2613
+ () => z.object({
2614
+ typename: z.string(),
2615
+ columns: z.array(z.object({ alias: z.string(), column: z.string() })),
2616
+ relations: z.array(z.object({
2617
+ alias: z.string(),
2618
+ link: z.string(),
2619
+ selection: selectionIrSchema,
2620
+ where: predIrSchema.optional(),
2621
+ orderBy: z.array(orderIrSchema).optional(),
2622
+ limit: limitIrSchema.optional(),
2623
+ offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional()
2624
+ })),
2625
+ aggregates: z.array(z.object({
2626
+ alias: z.string(),
2627
+ link: z.string().optional(),
2628
+ where: predIrSchema.optional(),
2629
+ count: z.object({ columns: z.array(z.string()).optional(), distinct: z.boolean().optional() }).optional(),
2630
+ sum: z.array(z.string()).optional(),
2631
+ avg: z.array(z.string()).optional(),
2632
+ nodes: selectionIrSchema.optional()
2633
+ }))
2634
+ })
2635
+ );
2636
+ var includeIfSchema = z.object({ variable: z.string(), negated: z.boolean() });
2637
+ var selectRootSchema = z.object({
2638
+ kind: z.literal("select"),
2639
+ alias: z.string(),
2640
+ table: z.string(),
2641
+ selection: selectionIrSchema,
2642
+ /** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
2643
+ where: predIrSchema,
2644
+ orderBy: z.array(orderIrSchema).default([]),
2645
+ distinctOn: z.union([z.array(z.string()), z.object({ slot: z.string() })]).optional(),
2646
+ limit: limitIrSchema,
2647
+ offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
2648
+ includeIf: includeIfSchema.optional()
2649
+ });
2650
+ var byPkRootSchema = z.object({
2651
+ kind: z.literal("byPk"),
2652
+ alias: z.string(),
2653
+ table: z.string(),
2654
+ selection: selectionIrSchema,
2655
+ pk: z.array(z.object({ column: z.string(), value: valueIrSchema })).min(1),
2656
+ /** Policy predicate — ANDed with the PK match. */
2657
+ where: predIrSchema,
2658
+ includeIf: includeIfSchema.optional()
2659
+ });
2660
+ var aggregateRootSchema = z.object({
2661
+ kind: z.literal("aggregate"),
2662
+ alias: z.string(),
2663
+ table: z.string(),
2664
+ where: predIrSchema,
2665
+ count: z.object({ columns: z.array(z.string()).optional(), distinct: z.boolean().optional() }).optional(),
2666
+ sum: z.array(z.string()).optional(),
2667
+ avg: z.array(z.string()).optional(),
2668
+ nodes: selectionIrSchema.optional(),
2669
+ includeIf: includeIfSchema.optional()
2670
+ });
2671
+ var writeValuesSchema = z.union([
2672
+ z.object({ slot: z.string() }),
2673
+ z.object({ entries: z.array(z.object({ column: z.string(), value: valueIrSchema })) })
2674
+ ]);
2675
+ var onConflictIrSchema = z.union([
2676
+ z.object({ slot: z.string() }),
2677
+ z.object({ constraint: z.string(), updateColumns: z.array(z.string()) })
2678
+ ]);
2679
+ var mutationRootSchema = z.object({
2680
+ kind: z.literal("mutation"),
2681
+ op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk"]),
2682
+ alias: z.string(),
2683
+ table: z.string(),
2684
+ /** insert/insertOne: rows come from a slot ($object/$objects) or literals. */
2685
+ objects: z.union([
2686
+ z.object({ slot: z.string() }),
2687
+ z.object({ rows: z.array(z.array(z.object({ column: z.string(), value: valueIrSchema }))) })
2688
+ ]).optional(),
2689
+ /** Server-pinned columns (e.g. author_sub ← claim('sub')) merged into every inserted row. */
2690
+ pinnedColumns: z.array(z.object({ column: z.string(), value: valueIrSchema })).default([]),
2691
+ onConflict: onConflictIrSchema.optional(),
2692
+ set: writeValuesSchema.optional(),
2693
+ append: writeValuesSchema.optional(),
2694
+ inc: writeValuesSchema.optional(),
2695
+ /** update/delete row filter: AND(writePolicy, documentWhere/pk, dynamicFilter?). */
2696
+ where: predIrSchema.optional(),
2697
+ pk: z.array(z.object({ column: z.string(), value: valueIrSchema })).optional(),
2698
+ /** WITH CHECK analogue evaluated against NEW row values (insert/update). */
2699
+ insertCheck: predIrSchema.optional(),
2700
+ returning: selectionIrSchema.nullable(),
2701
+ affectedRows: z.boolean().default(false),
2702
+ includeIf: includeIfSchema.optional()
2703
+ });
2704
+ var actionRootSchema = z.object({
2705
+ kind: z.literal("action"),
2706
+ alias: z.string(),
2707
+ name: z.string(),
2708
+ params: z.array(z.object({ name: z.string(), value: valueIrSchema })),
2709
+ returns: z.enum(["rows", "rowsAffected", "scalar"])
2710
+ });
2711
+ var rootIrSchema = z.discriminatedUnion("kind", [
2712
+ selectRootSchema,
2713
+ byPkRootSchema,
2714
+ aggregateRootSchema,
2715
+ mutationRootSchema,
2716
+ actionRootSchema
2717
+ ]);
2718
+ var INPUT_REF_KINDS = [
2719
+ "boolExp",
2720
+ "orderBy",
2721
+ "selectColumn",
2722
+ "insertInput",
2723
+ "setInput",
2724
+ "appendInput",
2725
+ "onConflict"
2726
+ ];
2727
+ var variableDefaultIrSchema = z.object({
2728
+ value: z.union([scalarLiteral, z.array(scalarLiteral)])
2729
+ });
2730
+ var variableIrSchema = z.object({
2731
+ name: z.string(),
2732
+ type: z.union([
2733
+ z.object({ kind: z.literal("scalar"), scalar: z.enum(COLUMN_TYPES) }),
2734
+ /** `values` is the declared membership whitelist — the executor validates against it. */
2735
+ z.object({ kind: z.literal("enum"), name: z.string(), values: z.array(z.string()).min(1) }),
2736
+ /** §4f dynamic slot: validated against `tables[table]` whitelists at runtime. */
2737
+ z.object({ kind: z.literal("inputRef"), input: z.enum(INPUT_REF_KINDS), table: z.string() })
2738
+ ]),
2739
+ nullable: z.boolean(),
2740
+ list: z.boolean(),
2741
+ /** Absent = no default declared; see {@link variableDefaultIrSchema}. */
2742
+ defaultValue: variableDefaultIrSchema.optional()
2743
+ });
2744
+ var compiledOperationSchema = z.object({
2745
+ irVersion: z.literal(IR_VERSION),
2746
+ name: z.string(),
2747
+ opHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
2748
+ kind: z.enum(["query", "mutation", "action"]),
2749
+ requires: z.object({
2750
+ capability: z.string().optional(),
2751
+ role: z.string().optional()
2752
+ }).nullable(),
2753
+ variables: z.array(variableIrSchema),
2754
+ /** Every table the op touches — data-token scoping + audit. */
2755
+ tableSet: z.array(z.string()).min(1),
2756
+ roots: z.array(rootIrSchema).min(1)
2757
+ });
2758
+ var tableMetaIrSchema = z.object({
2759
+ table: z.string(),
2760
+ schema: z.string(),
2761
+ typename: z.string(),
2762
+ columns: z.array(z.object({
2763
+ name: z.string(),
2764
+ columnName: z.string(),
2765
+ type: z.union([z.enum(COLUMN_TYPES), z.object({ enum: z.string(), values: z.array(z.string()) })]),
2766
+ nullable: z.boolean()
2767
+ })),
2768
+ primaryKey: z.array(z.string()).min(1),
2769
+ links: z.array(z.object({
2770
+ name: z.string(),
2771
+ kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
2772
+ target: z.string(),
2773
+ on: z.string(),
2774
+ references: z.string()
2775
+ })),
2776
+ /** §4f whitelists: what dynamic inputs may reference. */
2777
+ filterable: z.array(z.string()),
2778
+ orderable: z.array(z.string()),
2779
+ insertable: z.array(z.string()),
2780
+ settable: z.array(z.string()),
2781
+ appendable: z.array(z.string()),
2782
+ constraints: z.array(z.object({ name: z.string(), columns: z.array(z.string()) })),
2783
+ /** Depth/node caps for dynamic filter trees on this table. */
2784
+ dynamicFilterMaxDepth: z.number().int().positive().default(6),
2785
+ dynamicFilterMaxNodes: z.number().int().positive().default(64)
2786
+ });
2787
+ var compiledArtifactSchema = z.object({
2788
+ irVersion: z.literal(IR_VERSION),
2789
+ namespace: z.string(),
2790
+ schemaHash: z.string(),
2791
+ opsHash: z.string(),
2792
+ tables: z.record(z.string(), tableMetaIrSchema),
2793
+ operations: z.record(z.string(), compiledOperationSchema)
2794
+ });
2795
+ var IrVersionError = class extends Error {
2796
+ constructor(found) {
2797
+ super(`Unsupported IR version ${String(found)} \u2014 this executor supports up to ${IR_VERSION}. Upgrade @sequenceholdings/orm or re-push the namespace.`);
2798
+ this.name = "IrVersionError";
1639
2799
  }
1640
- return errors;
2800
+ };
2801
+ function normalizeIr(raw) {
2802
+ if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
2803
+ const version = raw.irVersion;
2804
+ if (version !== IR_VERSION) throw new IrVersionError(version);
2805
+ return compiledArtifactSchema.parse(raw);
2806
+ }
2807
+ function indexByHash(artifact) {
2808
+ const map = /* @__PURE__ */ new Map();
2809
+ for (const op of Object.values(artifact.operations)) map.set(op.opHash, op);
2810
+ return map;
1641
2811
  }
1642
2812
 
1643
2813
  // src/schema/namespace.ts
@@ -1895,8 +3065,8 @@ async function readMigrationWorkspace(dir, namespace) {
1895
3065
  journalPath
1896
3066
  );
1897
3067
  }
1898
- parsed.data.entries.forEach((entry, index2) => {
1899
- if (entry.ordinal !== index2 + 1) {
3068
+ parsed.data.entries.forEach((entry, index3) => {
3069
+ if (entry.ordinal !== index3 + 1) {
1900
3070
  throw new OrmDefinitionError(`migration journal entries must be contiguous from 1 (found ${entry.name})`, journalPath);
1901
3071
  }
1902
3072
  });
@@ -1919,7 +3089,7 @@ async function readMigrationSql(dir, entry) {
1919
3089
  throw new OrmDefinitionError("missing migration file", path);
1920
3090
  });
1921
3091
  }
1922
- var checksumOf = (text2) => createHash("sha256").update(text2).digest("hex");
3092
+ var checksumOf = (text3) => createHash("sha256").update(text3).digest("hex");
1923
3093
  var migrationName = (ordinal, contentHash) => `${pad(ordinal)}_${contentHash.slice(0, 12)}`;
1924
3094
  function renderMigrationSql({
1925
3095
  name,
@@ -1945,12 +3115,12 @@ async function writeMigration({
1945
3115
  const tip = workspace.journal.entries.at(-1);
1946
3116
  const ordinal = (tip?.ordinal ?? 0) + 1;
1947
3117
  const name = migrationName(ordinal, contentHash);
1948
- const sql2 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
3118
+ const sql3 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
1949
3119
  const metaDir = join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
1950
3120
  await mkdir(metaDir, { recursive: true });
1951
3121
  const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
1952
3122
  `;
1953
- await writeFile(join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql2);
3123
+ await writeFile(join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql3);
1954
3124
  await writeFile(join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
1955
3125
  const journal = {
1956
3126
  ...workspace.journal,
@@ -1961,7 +3131,7 @@ async function writeMigration({
1961
3131
  name,
1962
3132
  content_hash: contentHash,
1963
3133
  previous_content_hash: tip?.content_hash ?? null,
1964
- checksum: checksumOf(sql2),
3134
+ checksum: checksumOf(sql3),
1965
3135
  snapshot_checksum: checksumOf(snapshotText),
1966
3136
  destructive
1967
3137
  }
@@ -2073,11 +3243,11 @@ async function checkWorkspace({
2073
3243
  log(`no committed migrations for '${loaded.definition.namespace}' \u2014 run \`seq-studio orm diff\` and commit the result`);
2074
3244
  return 1;
2075
3245
  }
2076
- let previousId2;
3246
+ let previousId3;
2077
3247
  let previousHash = null;
2078
3248
  for (const entry of workspace.journal.entries) {
2079
- const sql2 = await readMigrationSql(dir, entry);
2080
- if (checksumOf(sql2) !== entry.checksum) {
3249
+ const sql3 = await readMigrationSql(dir, entry);
3250
+ if (checksumOf(sql3) !== entry.checksum) {
2081
3251
  log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
2082
3252
  return 1;
2083
3253
  }
@@ -2087,7 +3257,7 @@ async function checkWorkspace({
2087
3257
  return 1;
2088
3258
  }
2089
3259
  const snapshot = JSON.parse(snapshotText);
2090
- if (entry.ordinal > 1 && snapshot.prevId !== previousId2) {
3260
+ if (entry.ordinal > 1 && snapshot.prevId !== previousId3) {
2091
3261
  log(`snapshot chain broken at ${entry.name} \u2014 regenerate the workspace with \`seq-studio orm diff\``);
2092
3262
  return 1;
2093
3263
  }
@@ -2095,7 +3265,7 @@ async function checkWorkspace({
2095
3265
  log(`content-hash chain broken at ${entry.name} \u2014 migrations must be appended in order by \`seq-studio orm diff\``);
2096
3266
  return 1;
2097
3267
  }
2098
- previousId2 = snapshot.id;
3268
+ previousId3 = snapshot.id;
2099
3269
  previousHash = entry.content_hash;
2100
3270
  }
2101
3271
  if (tip.content_hash !== loaded.contentHash) {
@@ -2117,12 +3287,161 @@ async function checkWorkspace({
2117
3287
  log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
2118
3288
  return 0;
2119
3289
  }
2120
- var firstLine = (sql2) => sql2.split("\n")[0] ?? sql2;
3290
+ var firstLine = (sql3) => sql3.split("\n")[0] ?? sql3;
3291
+
3292
+ // src/migrations/from-yaml.ts
3293
+ function convertV1DefinitionToCanonical(definition) {
3294
+ const raw = {
3295
+ definition_format: CANONICAL_FORMAT,
3296
+ namespace: definition.namespace,
3297
+ description: definition.description ?? void 0,
3298
+ enums: [],
3299
+ tables: definition.tables.map(convertTable),
3300
+ capabilities: {
3301
+ roles: definition.capabilities.roles,
3302
+ uses: [...definition.capabilities.uses]
3303
+ }
3304
+ };
3305
+ const parsed = canonicalDefinitionSchema.safeParse(raw);
3306
+ if (!parsed.success) {
3307
+ const issue = parsed.error.issues[0];
3308
+ throw new OrmDefinitionError(
3309
+ `converted definition is not valid canonical v2: ${issue ? `${issue.path.join(".")}: ${issue.message}` : "unknown issue"}`
3310
+ );
3311
+ }
3312
+ return normalizeCanonical(parsed.data);
3313
+ }
3314
+ function convertTable(table) {
3315
+ const columns = Object.entries(table.columns ?? {});
3316
+ const primaryKey2 = columns.filter(([, column]) => column.primary_key).map(([name]) => name);
3317
+ return {
3318
+ table: table.table,
3319
+ description: table.description,
3320
+ source: table.source,
3321
+ columns: columns.map(([name, column]) => convertColumn(name, column)),
3322
+ // Synced tables reflect their source — no columns, no primary key.
3323
+ primaryKey: primaryKey2.length > 0 ? primaryKey2 : void 0,
3324
+ links: Object.entries(table.links ?? {}).map(([name, link]) => convertLink(table.table, name, link)),
3325
+ indexes: (table.indexes ?? []).map((index3) => ({
3326
+ columns: [...index3.columns],
3327
+ unique: index3.unique ?? false,
3328
+ name: index3.name
3329
+ })),
3330
+ policies: {
3331
+ // Verbatim DSL source strings — the parsed v1 AST is derived state.
3332
+ read: table.policies?.read?.source,
3333
+ write: table.policies?.write?.source
3334
+ },
3335
+ actions: Object.entries(table.actions ?? {}).map(([name, action]) => convertAction(table.table, name, action))
3336
+ };
3337
+ }
3338
+ function convertColumn(name, column) {
3339
+ const isDefaultFn = column.default !== void 0 && isFunctionDefault(column.default, column.type);
3340
+ return {
3341
+ name,
3342
+ columnName: name,
3343
+ type: column.type,
3344
+ nullable: column.primary_key ? false : column.nullable !== false,
3345
+ unique: column.unique === true,
3346
+ default: isDefaultFn ? void 0 : column.default,
3347
+ // isFunctionDefault guarantees one of the two allowlisted functions; the
3348
+ // canonicalDefinitionSchema parse below re-checks the enum.
3349
+ defaultFn: isDefaultFn ? column.default : void 0,
3350
+ description: column.description,
3351
+ renamedFrom: column.renamed_from,
3352
+ cast: column.cast,
3353
+ deprecated: column.deprecated === true
3354
+ };
3355
+ }
3356
+ function convertLink(table, name, link) {
3357
+ if (link.to.includes(".")) {
3358
+ throw new OrmDefinitionError(
3359
+ `link '${name}' on '${table}' targets '${link.to}' \u2014 cross-namespace links have no canonical v2 representation (links are namespace-local in v2)`
3360
+ );
3361
+ }
3362
+ return {
3363
+ name,
3364
+ kind: "toOne",
3365
+ target: link.to,
3366
+ on: link.on,
3367
+ enforce: link.enforce === true,
3368
+ description: link.description
3369
+ };
3370
+ }
3371
+ function convertAction(table, name, action) {
3372
+ return {
3373
+ name,
3374
+ requires: action.requires?.source,
3375
+ params: Object.fromEntries(
3376
+ Object.entries(action.params ?? {}).map(([param, definition]) => [
3377
+ param,
3378
+ { type: definition.type, nullable: definition.optional === true }
3379
+ ])
3380
+ ),
3381
+ // The v2 contract carries one SQL string; statement boundaries survive as
3382
+ // `;\n` (v1 validation guarantees no bare `;` inside a single statement).
3383
+ executes: actionStatementSources(action.executes).join(";\n"),
3384
+ // v1 declared the RETURNING columns inline; v2 types them at compile time,
3385
+ // so the shape collapses to rows-vs-rowsAffected.
3386
+ returns: action.returns ? "rows" : "rowsAffected",
3387
+ audit: action.audit ? { action: action.audit.action, resource: action.audit.resource ?? table } : void 0
3388
+ };
3389
+ }
3390
+
3391
+ // src/migrations/v2.ts
3392
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
3393
+ function rebaseViolations(statements, options) {
3394
+ const allowSecurity = options?.allowSecurityReemission ?? false;
3395
+ return statements.filter((statement) => !(statement.kind === "security" && allowSecurity));
3396
+ }
3397
+ var verifyRebaseIsNoop = (plan, options) => rebaseViolations(plan.statements, options).length === 0;
3398
+ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, snapshot }) {
3399
+ if (!SHA256_HEX.test(previousHash)) {
3400
+ throw new OrmDefinitionError(`rebase previousHash is not a sha256 hex digest: '${previousHash}'`);
3401
+ }
3402
+ if (!SHA256_HEX.test(newHash)) {
3403
+ throw new OrmDefinitionError(`rebase newHash is not a sha256 hex digest: '${newHash}'`);
3404
+ }
3405
+ if (previousHash === newHash) {
3406
+ throw new OrmDefinitionError("rebase previousHash and newHash are identical \u2014 nothing to rebase");
3407
+ }
3408
+ if (!Number.isInteger(ordinal) || ordinal < 2) {
3409
+ throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
3410
+ }
3411
+ const name = migrationName(ordinal, newHash);
3412
+ const sql3 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
3413
+ const snapshotText = `${JSON.stringify(snapshot, null, 2)}
3414
+ `;
3415
+ const entry = {
3416
+ ordinal,
3417
+ name,
3418
+ content_hash: newHash,
3419
+ previous_content_hash: previousHash,
3420
+ checksum: checksumOf(sql3),
3421
+ snapshot_checksum: checksumOf(snapshotText),
3422
+ destructive: false
3423
+ };
3424
+ return { name, entry, sql: sql3, snapshotText };
3425
+ }
3426
+ function renderRebaseMigrationSql({
3427
+ name,
3428
+ namespace,
3429
+ previousHash,
3430
+ newHash
3431
+ }) {
3432
+ return [
3433
+ `-- ${name}`,
3434
+ `-- namespace: ${namespace}`,
3435
+ `-- content hash: ${newHash}`,
3436
+ `-- rebase: v1 definitions (${previousHash}) converted to canonical v2 \u2014 verified no-op, zero statements`,
3437
+ "-- generated by `seq-studio orm diff --rebase` \u2014 review and commit, do not edit"
3438
+ ].join("\n") + "\n";
3439
+ }
2121
3440
 
2122
3441
  // src/schema/canonical.ts
2123
3442
  function parseCanonicalDefinition(value) {
2124
3443
  const canonical = asCanonicalShape(value);
2125
- const files = canonical.tables.map((table, index2) => {
3444
+ const files = canonical.tables.map((table, index3) => {
2126
3445
  const input = {
2127
3446
  namespace: canonical.namespace,
2128
3447
  table: table["table"],
@@ -2139,10 +3458,10 @@ function parseCanonicalDefinition(value) {
2139
3458
  const issue = parsed.error.issues[0];
2140
3459
  throw new OrmDefinitionError(
2141
3460
  `stored definition for table ${String(table["table"])} is invalid: ${issue?.message ?? "unknown issue"}`,
2142
- `canonical[${index2}]`
3461
+ `canonical[${index3}]`
2143
3462
  );
2144
3463
  }
2145
- return { file: `canonical[${index2}]`, table: parsed.data };
3464
+ return { file: `canonical[${index3}]`, table: parsed.data };
2146
3465
  });
2147
3466
  const config = namespaceConfigSchema.safeParse({
2148
3467
  namespace: canonical.namespace,
@@ -2199,6 +3518,6 @@ function canonicalActions(value) {
2199
3518
  );
2200
3519
  }
2201
3520
 
2202
- export { ACTION_REQUIRES_DENIED_SQLSTATE, ACTION_VALIDATES_FAILED_SQLSTATE, AUDIT_ACTIONS, CAPABILITIES_CLAIM_KEY, CAPABILITY_KEY_PATTERN, CLAIMS_SETTING, CLAIM_KEY_PATTERN, COLUMN_TYPES, COMPARE_OPS, CONSUMER_ROLE_SUFFIX, DEFINITION_FILE_EXTENSIONS, HASH_DOMAIN, IDENTIFIER_PATTERN, MAX_DEFINITION_FILE_BYTES, MAX_IDENTIFIER_LENGTH, MIGRATIONS_DIRNAME, NAMESPACE_CONFIG_FILENAMES, NAMESPACE_PATTERN, ORM_DATA_TOKEN_HEADER, ORM_SCHEMA_PREFIX, ORM_SERVICE_DATA_TOKEN_HEADER, OrmDefinitionError, PredicateParseError, QUALIFIED_TABLE_PATTERN, RESERVED_COLUMN_NAMES, RESERVED_TABLE_NAMES, STATEMENT_KINDS, SYNCED_SOURCE_PATTERN, actionFunctionName, actionFunctionStatements, actionParamArg, actionParamOrder, actionParamSchema, actionPrepareChecks, actionSchema, actionsInterfaceName, applyMigrationHints, buildActionParamsSchema, buildDrizzleEntities, buildNamespaceDefinition, canonicalizeDefinition, capabilityExpression, capabilityPinsSchema, castParts, checksumOf, claimExpression, classifyDestructive, columnSchema, columnTypeSchema, compileActionRequires, compileActionStatements, compileNamespace, compilePredicate, computeDefinitionHash, consumerRoleName, customRolesBlockSchema, customRolesDeclared, declaredCapabilityKeys, diffCommand, diffStatements, emptyJournal, emptySnapshot, extractNamedParams, generateNamespaceTypes, generateSnapshot, identifierSchema, isFunctionDefault, isOwnedStatement, isQualifiedReference, loadCommittedMigrations, loadCommittedTipMigration, loadNamespaceDefinition, migrationName, namespaceConfigSchema, namespaceSchema, normalizeSnapshot, ormSchemaName, parseCanonicalDefinition, parseCapabilityPin, parseDefinitionSources, parsePredicate, parseTableFile, parsedPredicateSchema, pendingKitStatements, planMigration, predicateOperands, quoteIdentifier, quoteLiteral, readMigrationSql, readMigrationWorkspace, readSnapshot, referencedCapabilities, referencedColumns, renderMigrationSql, securityStatements, sqlTypeName, stableJson, tableFileSchema, tableReferenceSchema, typesInterfaceName, validateCapabilityPins, walkPredicate, writeMigration };
3521
+ export { ACTION_REQUIRES_DENIED_SQLSTATE, ACTION_VALIDATES_FAILED_SQLSTATE, AUDIT_ACTIONS, CANONICAL_FORMAT, CAPABILITIES_CLAIM_KEY, CAPABILITY_KEY_PATTERN, CLAIMS_CONTRACT, CLAIMS_SETTING, CLAIM_KEY_PATTERN, CMP_OPS, COLUMN_BRAND, COLUMN_DEFAULT_FNS, COLUMN_TYPES2 as COLUMN_TYPES, COMPARE_OPS, CONSUMER_ROLE_SUFFIX, ColumnBuilder, DEFINITION_FILE_EXTENSIONS, ENUM_BRAND, HASH_DOMAIN, IDENTIFIER_PATTERN, IDENTIFIER_RE, INPUT_REF_KINDS, IR_VERSION, IrVersionError, LINK_BRAND, MAX_DEFINITION_FILE_BYTES, MAX_IDENTIFIER_LENGTH, MIGRATIONS_DIRNAME, NAMESPACE_CONFIG_BRAND, NAMESPACE_CONFIG_FILE, NAMESPACE_CONFIG_FILENAMES, NAMESPACE_PATTERN, ORM_DATA_TOKEN_HEADER, ORM_SCHEMA_PREFIX, ORM_SERVICE_DATA_TOKEN_HEADER, OrmDefinitionError, PredicateParseError, QUALIFIED_TABLE_PATTERN, RESERVED_COLUMN_NAMES, RESERVED_TABLE_NAMES, SCHEMA_DIR, STATEMENT_KINDS, SYNCED_SOURCE_PATTERN, TABLE_BRAND, actionFunctionName, actionFunctionStatements, actionParamArg, actionParamOrder, actionParamSchema, actionPrepareChecks, actionSchema, actionWriteTargets, actionsInterfaceName, activeAuthoringContext, applyMigrationHints, applyMigrationHintsV2, beginAuthoringContext, buildActionParamsSchema, buildDrizzleEntities, buildDrizzleEntitiesV2, buildNamespaceDefinition, buildRebaseJournalEntry, canonicalActionSchema, canonicalColumnSchema, canonicalDefinitionSchema, canonicalEnumSchema, canonicalIndexSchema, canonicalLinkSchema, canonicalPoliciesSchema, canonicalTableSchema, canonicalizeDefinition, capabilityExpression, castParts, checksumOf, claimExpression, classifyDestructive, columnSchema, columnTypeSchema2 as columnTypeSchema, compileActionRequires, compileActionStatements, compileNamespace, compilePredicate, compiledArtifactSchema, compiledOperationSchema, computeCanonicalHash, computeDefinitionHash, consumerRoleName, convertV1DefinitionToCanonical, declaredCapabilityKeys, defineEnum, defineNamespace, defineSyncedTable, defineTable, deriveColumnName, diffCommand, diffStatements, emptyJournal, emptySnapshot, endAuthoringContext, extractNamedParams, generateNamespaceTypes, generateSnapshot, generateSnapshotV2, identifierSchema, indexByHash, isColumnBuilder, isEnumRef, isFunctionDefault, isOwnedStatement, isQualifiedReference, isSeqLink, isSeqNamespaceConfig, isSeqTable, limitIrSchema, loadCommittedMigrations, loadCommittedTipMigration, loadNamespaceDefinition, loadNamespaceFromDir, migrationName, namespaceConfigSchema, namespaceSchema, normalizeCanonical, normalizeIr, normalizeSnapshot, orderIrSchema, ormSchemaName, parseCanonicalDefinition, parseDefinitionSources, parsePredicate, parseTableFile, parsedPredicateSchema, pendingKitStatements, planMigration, planMigrationV2, policyExpressionSchema, policyVariantSchema, predIrSchema, predicateOperands, quoteIdentifier, quoteLiteral, readMigrationSql, readMigrationWorkspace, readSnapshot, rebaseViolations, referencedCapabilities, referencedColumns, renderMigrationSql, rootIrSchema, securityStatements, selectionIrSchema, serializeNamespace, sqlTypeName, stableJson, t, tableFileSchema, tableMetaIrSchema, tableReferenceSchema, toMany, toOne, toPlatformUser, typesInterfaceName, valueIrSchema, variableDefaultIrSchema, variableIrSchema, verifyRebaseIsNoop, walkPredicate, writeMigration };
2203
3522
  //# sourceMappingURL=index.js.map
2204
3523
  //# sourceMappingURL=index.js.map