@sequenceholdings/orm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,2308 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+ var api = require('drizzle-kit/api');
5
+ var pgCore = require('drizzle-orm/pg-core');
6
+ var drizzleOrm = require('drizzle-orm');
7
+ var crypto = require('crypto');
8
+ var promises = require('fs/promises');
9
+ var path = require('path');
10
+ var jsYaml = require('js-yaml');
11
+
12
+ // src/codegen/types.ts
13
+ var WIRE_TYPES = {
14
+ uuid: "string",
15
+ text: "string",
16
+ integer: "number",
17
+ bigint: "string",
18
+ numeric: "string",
19
+ double: "number",
20
+ boolean: "boolean",
21
+ timestamptz: "string",
22
+ date: "string",
23
+ jsonb: "unknown"
24
+ };
25
+ function generateNamespaceTypes(definition) {
26
+ const lines = [
27
+ `// Generated by \`seq-studio orm generate\` from the '${definition.namespace}' definitions \u2014 do not edit.`,
28
+ "",
29
+ `export type ${typesInterfaceName(definition.namespace)} = {`
30
+ ];
31
+ for (const table of definition.tables) {
32
+ if (table.source) {
33
+ lines.push(` /** Synced from ${table.source} \u2014 columns reflect the gold source. */`);
34
+ lines.push(` ${table.table}: Record<string, unknown>`);
35
+ continue;
36
+ }
37
+ lines.push(` ${table.table}: {`);
38
+ for (const [name, column] of Object.entries(table.columns ?? {})) {
39
+ if (column.deprecated) continue;
40
+ lines.push(` ${name}: ${columnWireType(column)}`);
41
+ }
42
+ lines.push(" }");
43
+ }
44
+ lines.push("}", "");
45
+ lines.push(...actionTypeLines(definition));
46
+ return lines.join("\n");
47
+ }
48
+ function actionTypeLines(definition) {
49
+ const actions = definition.tables.flatMap((table) => Object.entries(table.actions ?? {}));
50
+ const lines = [`export type ${actionsInterfaceName(definition.namespace)} = {`];
51
+ for (const [name, action] of actions.sort(([a], [b]) => a.localeCompare(b))) {
52
+ lines.push(` ${name}: { params: ${actionParamsType(action)}; result: ${actionResultType(action)} }`);
53
+ }
54
+ lines.push("}", "");
55
+ return lines;
56
+ }
57
+ function actionParamsType(action) {
58
+ const entries = Object.entries(action.params ?? {});
59
+ if (entries.length === 0) return "Record<never, never>";
60
+ const fields = entries.map(([name, param]) => {
61
+ const base = WIRE_TYPES[param.type];
62
+ return param.optional ? `${name}?: ${base} | null` : `${name}: ${base}`;
63
+ });
64
+ return `{ ${fields.join("; ")} }`;
65
+ }
66
+ function actionResultType(action) {
67
+ if (!action.returns) return "{ rowsAffected: number }";
68
+ const columns = Object.entries(action.returns).map(([name, type]) => {
69
+ const base = WIRE_TYPES[type];
70
+ return base === "unknown" ? `${name}: ${base}` : `${name}: ${base} | null`;
71
+ });
72
+ return `{ rows: Array<{ ${columns.join("; ")} }> }`;
73
+ }
74
+ function typesInterfaceName(namespace) {
75
+ return `${pascalCase(namespace)}Tables`;
76
+ }
77
+ function actionsInterfaceName(namespace) {
78
+ return `${pascalCase(namespace)}Actions`;
79
+ }
80
+ function pascalCase(namespace) {
81
+ return namespace.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
82
+ }
83
+ function columnWireType(column) {
84
+ const base = WIRE_TYPES[column.type];
85
+ const notNull = column.primary_key === true || column.nullable === false;
86
+ return notNull || base === "unknown" ? base : `${base} | null`;
87
+ }
88
+
89
+ // src/constants.ts
90
+ var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
91
+ var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
92
+ var MAX_IDENTIFIER_LENGTH = 63;
93
+ var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
94
+ var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
95
+ var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
96
+ var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
97
+ var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
98
+ var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
99
+ var ORM_SCHEMA_PREFIX = "orm_";
100
+ var CONSUMER_ROLE_SUFFIX = "_consumer";
101
+ var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
102
+ var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
103
+ var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
104
+ var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
105
+ var HASH_DOMAIN = "sequence-orm-definitions\0";
106
+ var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
107
+ var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
108
+ var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
109
+
110
+ // src/errors.ts
111
+ var OrmDefinitionError = class extends Error {
112
+ constructor(message, file) {
113
+ super(file ? `${file}: ${message}` : message);
114
+ this.file = file;
115
+ }
116
+ name = "OrmDefinitionError";
117
+ };
118
+ var PredicateParseError = class extends Error {
119
+ constructor(message, position) {
120
+ super(`${message} (at position ${position})`);
121
+ this.position = position;
122
+ }
123
+ name = "PredicateParseError";
124
+ };
125
+
126
+ // src/predicate/ast.ts
127
+ var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
128
+ function walkPredicate(predicate, visit) {
129
+ visit(predicate);
130
+ if (predicate.kind === "and" || predicate.kind === "or") {
131
+ for (const operand of predicate.operands) walkPredicate(operand, visit);
132
+ }
133
+ if (predicate.kind === "not") walkPredicate(predicate.operand, visit);
134
+ }
135
+ function predicateOperands(predicate) {
136
+ const operands = [];
137
+ walkPredicate(predicate, (node) => {
138
+ if (node.kind === "compare") operands.push(node.left, node.right);
139
+ });
140
+ return operands;
141
+ }
142
+ function referencedColumns(predicate) {
143
+ const names = predicateOperands(predicate).flatMap(
144
+ (operand) => operand.kind === "column" ? [operand.name] : []
145
+ );
146
+ return [...new Set(names)];
147
+ }
148
+ function referencedCapabilities(predicate) {
149
+ const keys = [];
150
+ walkPredicate(predicate, (node) => {
151
+ if (node.kind === "capability") keys.push(node.key);
152
+ });
153
+ return [...new Set(keys)];
154
+ }
155
+ var identifierSchema = zod.z.string().max(MAX_IDENTIFIER_LENGTH).regex(IDENTIFIER_PATTERN, "must be a lowercase identifier (a-z, 0-9, _)");
156
+ var namespaceSchema = zod.z.string().regex(NAMESPACE_PATTERN, "must be lowercase, start with a letter, and be at most 41 characters");
157
+ var tableReferenceSchema = zod.z.string().refine(
158
+ (value) => IDENTIFIER_PATTERN.test(value) || QUALIFIED_TABLE_PATTERN.test(value),
159
+ "must be a table name or a schema-qualified table reference"
160
+ );
161
+ var isQualifiedReference = (reference) => reference.includes(".");
162
+
163
+ // src/schema/columns.ts
164
+ var COLUMN_TYPES = [
165
+ "uuid",
166
+ "text",
167
+ "integer",
168
+ "bigint",
169
+ "numeric",
170
+ "double",
171
+ "boolean",
172
+ "timestamptz",
173
+ "date",
174
+ "jsonb"
175
+ ];
176
+ var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
177
+ var FUNCTION_DEFAULTS = {
178
+ "gen_random_uuid()": ["uuid"],
179
+ "now()": ["timestamptz", "date"]
180
+ };
181
+ var CAST_PATTERN = /^([a-z][a-z0-9_]*)::([a-z]+)$/;
182
+ var isFunctionDefault = (value, type) => typeof value === "string" && (FUNCTION_DEFAULTS[value]?.includes(type) ?? false);
183
+ function defaultIssue(value, type) {
184
+ if (typeof value === "boolean") return type === "boolean" ? null : `boolean default is invalid for type '${type}'`;
185
+ if (typeof value === "number") {
186
+ if (!["integer", "bigint", "numeric", "double"].includes(type)) {
187
+ return `numeric default is invalid for type '${type}'`;
188
+ }
189
+ if (["integer", "bigint"].includes(type)) {
190
+ if (!Number.isInteger(value)) return `default for type '${type}' must be an integer`;
191
+ if (!Number.isSafeInteger(value)) {
192
+ return `default for type '${type}' exceeds the safe integer range \u2014 YAML numbers lose precision past 2^53`;
193
+ }
194
+ }
195
+ if (type === "numeric" && Math.abs(value) > Number.MAX_SAFE_INTEGER) {
196
+ return "numeric default exceeds the safe float range \u2014 write it as a string (e.g. '9007199254740993') to keep exact precision";
197
+ }
198
+ return null;
199
+ }
200
+ const allowedTypes = FUNCTION_DEFAULTS[value];
201
+ if (allowedTypes) return allowedTypes.includes(type) ? null : `default '${value}' is invalid for type '${type}'`;
202
+ return null;
203
+ }
204
+ var columnSchema = zod.z.strictObject({
205
+ type: columnTypeSchema,
206
+ primary_key: zod.z.boolean().optional(),
207
+ nullable: zod.z.boolean().optional(),
208
+ unique: zod.z.boolean().optional(),
209
+ default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean()]).optional(),
210
+ description: zod.z.string().optional(),
211
+ renamed_from: identifierSchema.optional(),
212
+ cast: zod.z.string().optional(),
213
+ deprecated: zod.z.boolean().optional()
214
+ }).superRefine((column, ctx) => {
215
+ if (column.default !== void 0) {
216
+ const issue = defaultIssue(column.default, column.type);
217
+ if (issue) ctx.addIssue({ code: "custom", message: issue, path: ["default"] });
218
+ }
219
+ if (column.primary_key && column.nullable) {
220
+ ctx.addIssue({ code: "custom", message: "a primary key column cannot be nullable", path: ["nullable"] });
221
+ }
222
+ if (column.primary_key && column.deprecated) {
223
+ ctx.addIssue({ code: "custom", message: "a primary key column cannot be deprecated", path: ["deprecated"] });
224
+ }
225
+ if (column.cast && !CAST_PATTERN.test(column.cast)) {
226
+ ctx.addIssue({ code: "custom", message: "cast must have the shape '<column>::<type>'", path: ["cast"] });
227
+ }
228
+ });
229
+ function castParts(cast) {
230
+ const match = CAST_PATTERN.exec(cast);
231
+ if (!match) return null;
232
+ return { column: match[1], type: match[2] };
233
+ }
234
+
235
+ // src/predicate/parse.ts
236
+ var KEYWORDS = /* @__PURE__ */ new Set(["and", "or", "not", "true", "false", "null", "claim", "capability"]);
237
+ var OPERATOR_PATTERN = /^(!=|<=|>=|<>|=|<|>)/;
238
+ var IDENTIFIER_START = /[a-z_]/;
239
+ var IDENTIFIER_CHAR = /[a-z0-9_.-]/;
240
+ var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
241
+ function tokenize(source) {
242
+ const tokens = [];
243
+ let position = 0;
244
+ while (position < source.length) {
245
+ const char = source[position];
246
+ if (/\s/.test(char)) {
247
+ position += 1;
248
+ continue;
249
+ }
250
+ if (char === "(") {
251
+ tokens.push({ kind: "lparen", value: char, position });
252
+ position += 1;
253
+ continue;
254
+ }
255
+ if (char === ")") {
256
+ tokens.push({ kind: "rparen", value: char, position });
257
+ position += 1;
258
+ continue;
259
+ }
260
+ if (char === ",") {
261
+ tokens.push({ kind: "comma", value: char, position });
262
+ position += 1;
263
+ continue;
264
+ }
265
+ if (char === "'") {
266
+ const { value, end } = readString(source, position);
267
+ tokens.push({ kind: "string", value, position });
268
+ position = end;
269
+ continue;
270
+ }
271
+ const operator = OPERATOR_PATTERN.exec(source.slice(position));
272
+ if (operator) {
273
+ const value = operator[0] === "<>" ? "!=" : operator[0];
274
+ tokens.push({ kind: "operator", value, position });
275
+ position += operator[0].length;
276
+ continue;
277
+ }
278
+ const number = NUMBER_PATTERN.exec(source.slice(position));
279
+ if (number && !IDENTIFIER_START.test(char)) {
280
+ tokens.push({ kind: "number", value: number[0], position });
281
+ position += number[0].length;
282
+ continue;
283
+ }
284
+ if (IDENTIFIER_START.test(char)) {
285
+ let end = position;
286
+ while (end < source.length && IDENTIFIER_CHAR.test(source[end])) end += 1;
287
+ tokens.push({ kind: "identifier", value: source.slice(position, end), position });
288
+ position = end;
289
+ continue;
290
+ }
291
+ throw new PredicateParseError(`unexpected character '${char}'`, position);
292
+ }
293
+ tokens.push({ kind: "end", value: "", position: source.length });
294
+ return tokens;
295
+ }
296
+ function readString(source, start) {
297
+ let value = "";
298
+ let position = start + 1;
299
+ while (position < source.length) {
300
+ const char = source[position];
301
+ if (char !== "'") {
302
+ value += char;
303
+ position += 1;
304
+ continue;
305
+ }
306
+ if (source[position + 1] === "'") {
307
+ value += "'";
308
+ position += 2;
309
+ continue;
310
+ }
311
+ return { value, end: position + 1 };
312
+ }
313
+ throw new PredicateParseError("unterminated string literal", start);
314
+ }
315
+ var Parser = class {
316
+ constructor(tokens) {
317
+ this.tokens = tokens;
318
+ }
319
+ index = 0;
320
+ parse() {
321
+ const predicate = this.parseOr();
322
+ this.expect("end", "end of expression");
323
+ return predicate;
324
+ }
325
+ parseOr() {
326
+ const operands = [this.parseAnd()];
327
+ while (this.matchKeyword("or")) operands.push(this.parseAnd());
328
+ return operands.length === 1 ? operands[0] : { kind: "or", operands };
329
+ }
330
+ parseAnd() {
331
+ const operands = [this.parseNot()];
332
+ while (this.matchKeyword("and")) operands.push(this.parseNot());
333
+ return operands.length === 1 ? operands[0] : { kind: "and", operands };
334
+ }
335
+ parseNot() {
336
+ if (this.matchKeyword("not")) return { kind: "not", operand: this.parseNot() };
337
+ return this.parsePrimary();
338
+ }
339
+ parsePrimary() {
340
+ if (this.peek().kind === "lparen") {
341
+ this.advance();
342
+ const inner = this.parseOr();
343
+ this.expect("rparen", "')'");
344
+ return inner;
345
+ }
346
+ if (this.peekKeyword("capability")) {
347
+ const capability = this.parseCapability();
348
+ const next = this.peek();
349
+ if (next.kind === "operator") {
350
+ throw new PredicateParseError("capability() is boolean-valued and cannot appear in a comparison", next.position);
351
+ }
352
+ return capability;
353
+ }
354
+ return this.parseComparison();
355
+ }
356
+ parseCapability() {
357
+ this.advance();
358
+ const key = this.parseCallArgument("capability");
359
+ if (!CAPABILITY_KEY_PATTERN.test(key)) {
360
+ throw new PredicateParseError(`invalid capability key '${key}'`, this.peek().position);
361
+ }
362
+ return { kind: "capability", key };
363
+ }
364
+ parseComparison() {
365
+ const left = this.parseOperand();
366
+ const operator = this.expect("operator", "a comparison operator");
367
+ if (!COMPARE_OPS.includes(operator.value)) {
368
+ throw new PredicateParseError(`unsupported operator '${operator.value}'`, operator.position);
369
+ }
370
+ const right = this.parseOperand();
371
+ return { kind: "compare", op: operator.value, left, right };
372
+ }
373
+ parseOperand() {
374
+ const token = this.peek();
375
+ if (token.kind === "string") {
376
+ this.advance();
377
+ return { kind: "literal", value: token.value };
378
+ }
379
+ if (token.kind === "number") {
380
+ this.advance();
381
+ const value = Number(token.value);
382
+ if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) {
383
+ throw new PredicateParseError(
384
+ `numeric literal '${token.value}' exceeds safe integer precision \u2014 quote it as a string`,
385
+ token.position
386
+ );
387
+ }
388
+ return { kind: "literal", value };
389
+ }
390
+ if (token.kind !== "identifier") {
391
+ throw new PredicateParseError(`expected an operand, got '${token.value || "end"}'`, token.position);
392
+ }
393
+ this.advance();
394
+ if (token.value === "true" || token.value === "false") {
395
+ return { kind: "literal", value: token.value === "true" };
396
+ }
397
+ if (token.value === "null") return { kind: "literal", value: null };
398
+ if (token.value === "claim") {
399
+ const key = this.parseCallArgument("claim");
400
+ if (!CLAIM_KEY_PATTERN.test(key)) {
401
+ throw new PredicateParseError(`invalid claim key '${key}'`, token.position);
402
+ }
403
+ return { kind: "claim", key };
404
+ }
405
+ if (token.value === "capability") {
406
+ throw new PredicateParseError("capability() is boolean-valued and cannot appear in a comparison", token.position);
407
+ }
408
+ if (KEYWORDS.has(token.value) || !IDENTIFIER_PATTERN.test(token.value)) {
409
+ throw new PredicateParseError(`invalid column reference '${token.value}'`, token.position);
410
+ }
411
+ return { kind: "column", name: token.value };
412
+ }
413
+ parseCallArgument(fn) {
414
+ this.expect("lparen", `'(' after ${fn}`);
415
+ const argument = this.expect("string", `a quoted key in ${fn}(...)`);
416
+ this.expect("rparen", `')' after ${fn} key`);
417
+ return argument.value;
418
+ }
419
+ peek() {
420
+ return this.tokens[this.index];
421
+ }
422
+ peekKeyword(keyword) {
423
+ const token = this.peek();
424
+ return token.kind === "identifier" && token.value === keyword;
425
+ }
426
+ matchKeyword(keyword) {
427
+ if (!this.peekKeyword(keyword)) return false;
428
+ this.advance();
429
+ return true;
430
+ }
431
+ advance() {
432
+ const token = this.peek();
433
+ if (token.kind !== "end") this.index += 1;
434
+ return token;
435
+ }
436
+ expect(kind, description) {
437
+ const token = this.peek();
438
+ if (token.kind !== kind) {
439
+ throw new PredicateParseError(`expected ${description}, got '${token.value || "end"}'`, token.position);
440
+ }
441
+ return this.advance();
442
+ }
443
+ };
444
+ function parsePredicate(source) {
445
+ const trimmed = source.trim();
446
+ if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
447
+ return { source: trimmed, ast: new Parser(tokenize(trimmed)).parse() };
448
+ }
449
+
450
+ // src/schema/predicates.ts
451
+ var parsedPredicateSchema = zod.z.string().transform((source, ctx) => {
452
+ try {
453
+ return parsePredicate(source);
454
+ } catch (error) {
455
+ ctx.addIssue({ code: "custom", message: error instanceof Error ? error.message : String(error) });
456
+ return zod.z.NEVER;
457
+ }
458
+ });
459
+
460
+ // src/schema/actions.ts
461
+ var EXECUTES_VERB_PATTERN = /^\s*(insert|update|delete)\b/i;
462
+ var DOLLAR_QUOTE_PATTERN = /\$[a-zA-Z0-9_]*\$/;
463
+ var RETURNING_PATTERN = /\breturning\b/i;
464
+ var ESCAPE_STRING_PATTERN = /(?<![a-zA-Z0-9_])[Ee]'/;
465
+ var MASKED_CLAIM_CALL_PATTERN = /\b(?:claim|capability)\(\s*''\s*\)/g;
466
+ var BARE_CLAIM_CALL_PATTERN = /\b(?:claim|capability)\s*\(/;
467
+ var EXISTS_ENTRY_PATTERN = /^exists\(\s*'([^']+)'\s*,\s*([a-z][a-z0-9_]*)\s*\)$/;
468
+ var NAMED_PARAM_PATTERN = /(?<!:):([a-z][a-z0-9_]*)/g;
469
+ var QUOTED_STRING_PATTERN = /'(?:[^']|'')*'/g;
470
+ var SQL_COMMENT_PATTERN = /--|\/\*/;
471
+ var actionParamSchema = zod.z.strictObject({
472
+ type: columnTypeSchema,
473
+ optional: zod.z.boolean().optional(),
474
+ description: zod.z.string().optional()
475
+ });
476
+ var validatesEntrySchema = zod.z.string().transform((source, ctx) => {
477
+ const match = EXISTS_ENTRY_PATTERN.exec(source.trim());
478
+ if (!match) {
479
+ ctx.addIssue({ code: "custom", message: "validates entries must have the shape exists('<table>', <param>)" });
480
+ return zod.z.NEVER;
481
+ }
482
+ const table = tableReferenceSchema.safeParse(match[1]);
483
+ if (!table.success) {
484
+ ctx.addIssue({ code: "custom", message: `invalid table reference '${match[1]}'` });
485
+ return zod.z.NEVER;
486
+ }
487
+ return { source: source.trim(), table: table.data, param: match[2] };
488
+ });
489
+ var AUDIT_ACTIONS = ["create", "update", "delete"];
490
+ var auditSchema = zod.z.strictObject({
491
+ action: zod.z.enum(AUDIT_ACTIONS),
492
+ resource: identifierSchema.optional()
493
+ });
494
+ var actionSchema = zod.z.strictObject({
495
+ description: zod.z.string().optional(),
496
+ requires: parsedPredicateSchema.optional(),
497
+ params: zod.z.record(identifierSchema, actionParamSchema).optional(),
498
+ validates: zod.z.array(validatesEntrySchema).optional(),
499
+ // One statement, or an ordered list run atomically in the action function.
500
+ executes: zod.z.union([zod.z.string().min(1), zod.z.array(zod.z.string().min(1)).min(1)]),
501
+ // Declares the columns the action's RETURNING yields. Present → the
502
+ // function returns those rows (typed in the client); absent → the action
503
+ // reports rows_affected only. The column names/types must match the
504
+ // RETURNING list, which Postgres enforces at CREATE FUNCTION.
505
+ returns: zod.z.record(identifierSchema, columnTypeSchema).optional(),
506
+ audit: auditSchema.optional()
507
+ }).superRefine((action, ctx) => {
508
+ const params = Object.keys(action.params ?? {});
509
+ if (action.requires && referencedColumns(action.requires.ast).length > 0) {
510
+ ctx.addIssue({
511
+ code: "custom",
512
+ message: "requires may reference claims and capabilities only, not columns",
513
+ path: ["requires"]
514
+ });
515
+ }
516
+ for (const issue of executesIssues(action.executes, new Set(params), action.returns !== void 0)) {
517
+ ctx.addIssue({ code: "custom", message: issue, path: ["executes"] });
518
+ }
519
+ if (action.returns && Object.keys(action.returns).length === 0) {
520
+ ctx.addIssue({ code: "custom", message: "returns must declare at least one column", path: ["returns"] });
521
+ }
522
+ for (const [index2, entry] of (action.validates ?? []).entries()) {
523
+ if (!params.includes(entry.param)) {
524
+ ctx.addIssue({
525
+ code: "custom",
526
+ message: `validates references undeclared param '${entry.param}'`,
527
+ path: ["validates", index2]
528
+ });
529
+ }
530
+ }
531
+ });
532
+ function extractNamedParams(sql2) {
533
+ const withoutStrings = sql2.replace(QUOTED_STRING_PATTERN, "''");
534
+ const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
535
+ return [...new Set(names)];
536
+ }
537
+ function actionStatementSources(executes) {
538
+ return (Array.isArray(executes) ? executes : [executes]).map((statement) => statement.trim());
539
+ }
540
+ function executesIssues(executes, declaredParams, hasReturns) {
541
+ const issues = [];
542
+ const statements = actionStatementSources(executes);
543
+ statements.forEach((statement, index2) => {
544
+ const withoutStrings = statement.replace(QUOTED_STRING_PATTERN, "''");
545
+ const at = statements.length > 1 ? ` (statement ${index2 + 1})` : "";
546
+ if (!EXECUTES_VERB_PATTERN.test(statement)) issues.push(`executes must be INSERT, UPDATE, or DELETE${at}`);
547
+ if (withoutStrings.replace(/;\s*$/, "").includes(";")) issues.push(`each executes entry must be a single statement${at}`);
548
+ if (SQL_COMMENT_PATTERN.test(withoutStrings)) issues.push(`executes must not contain SQL comments${at}`);
549
+ if (DOLLAR_QUOTE_PATTERN.test(withoutStrings)) issues.push(`executes must not use dollar-quoted strings${at}`);
550
+ if (ESCAPE_STRING_PATTERN.test(withoutStrings)) issues.push(`executes must not use escape strings (E'\u2026')${at}`);
551
+ if (withoutStrings.includes('"')) issues.push(`executes must not use double-quoted identifiers${at}`);
552
+ if (BARE_CLAIM_CALL_PATTERN.test(withoutStrings.replace(MASKED_CLAIM_CALL_PATTERN, ""))) {
553
+ issues.push(`executes has a malformed claim()/capability() call \u2014 use e.g. claim('sub')${at}`);
554
+ }
555
+ if (RETURNING_PATTERN.test(withoutStrings) && !(hasReturns && index2 === statements.length - 1)) {
556
+ issues.push(
557
+ hasReturns ? `only the last executes statement may use RETURNING${at}` : `executes must not use RETURNING unless the action declares returns${at}`
558
+ );
559
+ }
560
+ });
561
+ const last = statements.at(-1);
562
+ if (hasReturns && last && !RETURNING_PATTERN.test(last.replace(QUOTED_STRING_PATTERN, "''"))) {
563
+ issues.push("the last executes statement must use RETURNING to populate the declared returns columns");
564
+ }
565
+ const undeclared = extractNamedParams(statements.join("\n")).filter((name) => !declaredParams.has(name));
566
+ if (undeclared.length > 0) issues.push(`executes references undeclared params: ${undeclared.join(", ")}`);
567
+ return issues;
568
+ }
569
+
570
+ // src/engine/types.ts
571
+ var SQL_TYPE_NAMES = {
572
+ double: "double precision",
573
+ timestamptz: "timestamp with time zone"
574
+ };
575
+ var sqlTypeName = (type) => SQL_TYPE_NAMES[type] ?? type;
576
+
577
+ // src/compiler/sql.ts
578
+ var CLAIMS_SETTING = "request.jwt.claims";
579
+ var CAPABILITIES_CLAIM_KEY = "capabilities";
580
+ var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
581
+ var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
582
+ var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
583
+ var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
584
+ var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
585
+
586
+ // src/compiler/predicates.ts
587
+ var SQL_COMPARE_OPS = {
588
+ "=": "=",
589
+ "!=": "<>",
590
+ "<": "<",
591
+ "<=": "<=",
592
+ ">": ">",
593
+ ">=": ">="
594
+ };
595
+ function compileOperand(operand, other, columnTypes) {
596
+ if (operand.kind === "column") return quoteIdentifier(operand.name);
597
+ if (operand.kind === "claim") {
598
+ const expression = claimExpression(operand.key);
599
+ const columnType = other.kind === "column" ? columnTypes?.[other.name] : void 0;
600
+ return columnType && columnType !== "text" ? `(${expression})::${sqlTypeName(columnType)}` : expression;
601
+ }
602
+ const { value } = operand;
603
+ if (value === null) return "NULL";
604
+ if (typeof value === "string") return quoteLiteral(value);
605
+ return String(value);
606
+ }
607
+ function compileComparison(op, left, right, columnTypes) {
608
+ const nullSide = left.kind === "literal" && left.value === null ? right : right.kind === "literal" && right.value === null ? left : null;
609
+ if (nullSide && (op === "=" || op === "!=")) {
610
+ return `${compileOperand(nullSide, nullSide, columnTypes)} IS${op === "!=" ? " NOT" : ""} NULL`;
611
+ }
612
+ return `${compileOperand(left, right, columnTypes)} ${SQL_COMPARE_OPS[op]} ${compileOperand(right, left, columnTypes)}`;
613
+ }
614
+ function compilePredicate(predicate, columnTypes) {
615
+ switch (predicate.kind) {
616
+ case "and":
617
+ case "or": {
618
+ const joined = predicate.operands.map((operand) => compilePredicate(operand, columnTypes)).join(` ${predicate.kind.toUpperCase()} `);
619
+ return `(${joined})`;
620
+ }
621
+ case "not":
622
+ return `(NOT ${compilePredicate(predicate.operand, columnTypes)})`;
623
+ case "compare":
624
+ return `(${compileComparison(predicate.op, predicate.left, predicate.right, columnTypes)})`;
625
+ case "capability":
626
+ return `(${capabilityExpression(predicate.key)})`;
627
+ }
628
+ }
629
+
630
+ // src/compiler/actions.ts
631
+ var QUOTED_STRING_PATTERN2 = /'(?:[^']|'')*'/g;
632
+ var TOKEN_PATTERN = /\bclaim\(\s*'((?:[^']|'')*)'\s*\)|\bcapability\(\s*'((?:[^']|'')*)'\s*\)|'(?:[^']|'')*'|(?<!:):([a-z][a-z0-9_]*)/g;
633
+ var BARE_CALL_PATTERN = /\b(?:claim|capability)\s*\(/;
634
+ var PARAM_SCHEMAS = {
635
+ uuid: zod.z.string(),
636
+ text: zod.z.string(),
637
+ integer: zod.z.union([zod.z.number(), zod.z.string()]),
638
+ bigint: zod.z.union([zod.z.number(), zod.z.string()]),
639
+ numeric: zod.z.union([zod.z.number(), zod.z.string()]),
640
+ double: zod.z.union([zod.z.number(), zod.z.string()]),
641
+ boolean: zod.z.union([zod.z.boolean(), zod.z.string()]),
642
+ timestamptz: zod.z.string(),
643
+ date: zod.z.string(),
644
+ jsonb: zod.z.unknown()
645
+ };
646
+ function buildActionParamsSchema(action) {
647
+ const shape = {};
648
+ const entries = Object.entries(action.params ?? {});
649
+ for (const [name, param] of entries) {
650
+ shape[name] = param.optional ? PARAM_SCHEMAS[param.type].nullish() : PARAM_SCHEMAS[param.type];
651
+ }
652
+ return zod.z.strictObject(shape).superRefine((obj, ctx) => {
653
+ for (const [name, param] of entries) {
654
+ if (!param.optional && param.type === "jsonb" && obj[name] === void 0) {
655
+ ctx.addIssue({ code: "custom", message: "Required", path: [name] });
656
+ }
657
+ }
658
+ });
659
+ }
660
+ var actionParamOrder = (action) => Object.keys(action.params ?? {});
661
+ var actionParamArg = (name) => `p_${name}`;
662
+ function compileActionStatements(action, renderParam = actionParamArg) {
663
+ return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
664
+ }
665
+ function compileStatement(sql2, renderParam) {
666
+ const compiled = sql2.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
667
+ if (claimKey !== void 0) {
668
+ if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
669
+ return claimExpression(claimKey);
670
+ }
671
+ if (capabilityKey !== void 0) {
672
+ if (!CAPABILITY_KEY_PATTERN.test(capabilityKey)) {
673
+ throw new OrmDefinitionError(`invalid capability key in executes: ${token}`);
674
+ }
675
+ return capabilityExpression(capabilityKey);
676
+ }
677
+ if (paramName !== void 0) return renderParam(paramName);
678
+ return token;
679
+ });
680
+ if (BARE_CALL_PATTERN.test(compiled.replace(QUOTED_STRING_PATTERN2, "''"))) {
681
+ throw new OrmDefinitionError("executes contains a claim()/capability() call outside the grammar");
682
+ }
683
+ return compiled;
684
+ }
685
+ function compileActionRequires(action) {
686
+ return action.requires ? compilePredicate(action.requires.ast) : null;
687
+ }
688
+ function referencedExecuteCapabilities(action) {
689
+ const keys = [];
690
+ for (const statement of actionStatementSources(action.executes)) {
691
+ for (const match of statement.matchAll(TOKEN_PATTERN)) {
692
+ if (match[2] !== void 0) keys.push(match[2]);
693
+ }
694
+ }
695
+ return keys;
696
+ }
697
+
698
+ // src/engine/actions.ts
699
+ var ACTION_REQUIRES_DENIED_SQLSTATE = "ORMRQ";
700
+ var ACTION_VALIDATES_FAILED_SQLSTATE = "ORMVL";
701
+ var actionFunctionName = (actionName) => `action_${actionName}`;
702
+ function returnProjection(returns, lastStatement) {
703
+ const projection = Object.keys(returns).map(quoteIdentifier).join(", ");
704
+ return `WITH orm_returning AS (${lastStatement}) SELECT ${projection} FROM orm_returning`;
705
+ }
706
+ var allActions = (definition) => definition.tables.flatMap((table) => Object.entries(table.actions ?? {}).map(([name, action]) => ({ name, action }))).sort((a, b) => a.name.localeCompare(b.name));
707
+ function actionFunctionStatements(definition) {
708
+ const schema = ormSchemaName(definition.namespace);
709
+ return [dropAllActionFunctions(schema), ...allActions(definition).flatMap(({ name, action }) => createActionFunction(definition, name, action))];
710
+ }
711
+ function actionPrepareChecks(definition) {
712
+ const actions = allActions(definition);
713
+ if (actions.length === 0) return [];
714
+ const statements = [`SET LOCAL search_path TO ${quoteIdentifier(ormSchemaName(definition.namespace))}`];
715
+ for (const { name, action } of actions) {
716
+ const order = actionParamOrder(action);
717
+ const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
718
+ const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
719
+ compiled.forEach((sql2, index2) => {
720
+ const checked = action.returns && index2 === compiled.length - 1 ? returnProjection(action.returns, sql2) : sql2;
721
+ statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index2}`)}${signature} AS ${checked}`);
722
+ });
723
+ }
724
+ return statements;
725
+ }
726
+ function dropAllActionFunctions(schema) {
727
+ return `DO $orm_reconcile_actions$
728
+ DECLARE
729
+ r record;
730
+ BEGIN
731
+ FOR r IN
732
+ SELECT p.oid::regprocedure AS sig
733
+ FROM pg_catalog.pg_proc p
734
+ JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
735
+ WHERE n.nspname = ${quoteLiteral(schema)} AND p.proname LIKE 'action\\_%'
736
+ LOOP
737
+ EXECUTE 'DROP FUNCTION ' || r.sig;
738
+ END LOOP;
739
+ END
740
+ $orm_reconcile_actions$`;
741
+ }
742
+ function createActionFunction(definition, name, action) {
743
+ const schema = quoteIdentifier(ormSchemaName(definition.namespace));
744
+ const fn = `${schema}.${quoteIdentifier(actionFunctionName(name))}`;
745
+ const order = actionParamOrder(action);
746
+ const signature = order.map((paramName) => `${quoteIdentifier(actionParamArg(paramName))} ${sqlTypeName(action.params[paramName].type)}`).join(", ");
747
+ const argTypes = order.map((paramName) => sqlTypeName(action.params[paramName].type)).join(", ");
748
+ const role = quoteIdentifier(consumerRoleName(definition.namespace));
749
+ const statements = compileActionStatements(action);
750
+ const last = statements[statements.length - 1];
751
+ const leading = statements.slice(0, -1).map((statement) => ` ${statement};`);
752
+ const create = action.returns ? (
753
+ // RETURNS TABLE + RETURN QUERY over the declared-order re-projection.
754
+ `CREATE FUNCTION ${fn}(${signature})
755
+ RETURNS TABLE(${Object.entries(action.returns).map(([column, type]) => `${quoteIdentifier(column)} ${sqlTypeName(type)}`).join(", ")})
756
+ LANGUAGE plpgsql
757
+ SECURITY INVOKER
758
+ SET search_path = ${schema}
759
+ AS $orm_action$
760
+ -- RETURNS TABLE names OUT params (id, \u2026) that collide with the re-projection's
761
+ -- columns; resolve unqualified names to the column.
762
+ #variable_conflict use_column
763
+ BEGIN
764
+ ${[functionGuards(definition, name, action), ...leading, ` RETURN QUERY ${returnProjection(action.returns, last)};`].filter(Boolean).join("\n")}
765
+ END
766
+ $orm_action$`
767
+ ) : (
768
+ // No returns: report the last statement's affected count.
769
+ `CREATE FUNCTION ${fn}(${signature})
770
+ RETURNS bigint
771
+ LANGUAGE plpgsql
772
+ SECURITY INVOKER
773
+ SET search_path = ${schema}
774
+ AS $orm_action$
775
+ DECLARE
776
+ v_count bigint;
777
+ BEGIN
778
+ ${[functionGuards(definition, name, action), ...leading, ` ${last};`].filter(Boolean).join("\n")}
779
+ GET DIAGNOSTICS v_count = ROW_COUNT;
780
+ RETURN v_count;
781
+ END
782
+ $orm_action$`
783
+ );
784
+ return [
785
+ create,
786
+ `REVOKE ALL ON FUNCTION ${fn}(${argTypes}) FROM PUBLIC`,
787
+ `GRANT EXECUTE ON FUNCTION ${fn}(${argTypes}) TO ${role}`
788
+ ];
789
+ }
790
+ function functionGuards(definition, name, action) {
791
+ const lines = [];
792
+ const requires = compileActionRequires(action);
793
+ if (requires) {
794
+ lines.push(
795
+ ` IF NOT (${requires}) THEN`,
796
+ ` RAISE EXCEPTION 'action ${name} requires a grant the caller does not hold' USING ERRCODE = ${quoteLiteral(ACTION_REQUIRES_DENIED_SQLSTATE)};`,
797
+ ` END IF;`
798
+ );
799
+ }
800
+ for (const entry of action.validates ?? []) {
801
+ const { schema, table, key } = resolveValidatesTarget(definition, entry);
802
+ const arg = quoteIdentifier(actionParamArg(entry.param));
803
+ lines.push(
804
+ ` IF ${arg} IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${quoteIdentifier(schema)}.${quoteIdentifier(table)} WHERE ${quoteIdentifier(key)} = ${arg} FOR SHARE) THEN`,
805
+ ` RAISE EXCEPTION 'validates failed: no ${entry.table} row for ${entry.param}' USING ERRCODE = ${quoteLiteral(ACTION_VALIDATES_FAILED_SQLSTATE)};`,
806
+ ` END IF;`
807
+ );
808
+ }
809
+ return lines.join("\n");
810
+ }
811
+ function resolveValidatesTarget(definition, entry) {
812
+ if (entry.table.includes(".")) {
813
+ const [schema, table] = entry.table.split(".");
814
+ return { schema, table, key: "id" };
815
+ }
816
+ const target = definition.tables.find((table) => table.table === entry.table);
817
+ const key = target && Object.entries(target.columns ?? {}).find(([, column]) => column.primary_key)?.[0];
818
+ if (!target || !key) {
819
+ throw new OrmDefinitionError(`action validates target '${entry.table}' is not a local table with a primary key`);
820
+ }
821
+ return { schema: ormSchemaName(definition.namespace), table: entry.table, key };
822
+ }
823
+ var linkSchema = zod.z.strictObject({
824
+ to: tableReferenceSchema,
825
+ on: identifierSchema,
826
+ enforce: zod.z.boolean().optional(),
827
+ description: zod.z.string().optional()
828
+ });
829
+ var indexSchema = zod.z.union([
830
+ zod.z.array(identifierSchema).min(1),
831
+ zod.z.strictObject({
832
+ columns: zod.z.array(identifierSchema).min(1),
833
+ unique: zod.z.boolean().optional(),
834
+ name: identifierSchema.optional()
835
+ })
836
+ ]).transform((index2) => Array.isArray(index2) ? { columns: index2 } : index2);
837
+ var policiesSchema = zod.z.strictObject({
838
+ read: parsedPredicateSchema.optional(),
839
+ write: parsedPredicateSchema.optional()
840
+ });
841
+ var tableFileSchema = zod.z.strictObject({
842
+ namespace: namespaceSchema,
843
+ table: identifierSchema,
844
+ description: zod.z.string().optional(),
845
+ columns: zod.z.record(identifierSchema, columnSchema).optional(),
846
+ source: zod.z.string().regex(SYNCED_SOURCE_PATTERN, "source must reference a gold-layer table").optional(),
847
+ links: zod.z.record(identifierSchema, linkSchema).optional(),
848
+ indexes: zod.z.array(indexSchema).optional(),
849
+ policies: policiesSchema.optional(),
850
+ actions: zod.z.record(identifierSchema, actionSchema).optional()
851
+ }).superRefine((table, ctx) => {
852
+ if (RESERVED_TABLE_NAMES.has(table.table)) {
853
+ ctx.addIssue({ code: "custom", message: `'${table.table}' is a reserved table name`, path: ["table"] });
854
+ }
855
+ for (const name of Object.keys(table.columns ?? {})) {
856
+ if (RESERVED_COLUMN_NAMES.has(name)) {
857
+ ctx.addIssue({
858
+ code: "custom",
859
+ message: `column '${name}' collides with the Data API's reserved query parameters (select, order, limit, offset) \u2014 it could never be filtered`,
860
+ path: ["columns", name]
861
+ });
862
+ }
863
+ }
864
+ if (table.source) validateSyncedTable(table, ctx);
865
+ else validateColumnTable(table, ctx);
866
+ validateLinks(table, ctx);
867
+ validatePolicyColumns(table, ctx);
868
+ validateDerivedNameLengths(table, ctx);
869
+ });
870
+ var indexName = (table, definition) => definition.name ?? `ix_${table}_${definition.columns.join("_")}`;
871
+ var policyName = (table, suffix) => `${table}_${suffix}`;
872
+ var foreignKeyName = (table, link) => `fk_${table}_${link}`;
873
+ function validateSyncedTable(table, ctx) {
874
+ const forbidden = [
875
+ ["columns", "synced tables reflect their source; columns are not declared"],
876
+ ["actions", "synced tables are read-only; actions are not allowed"],
877
+ ["indexes", "synced tables are system-owned; indexes are not allowed"]
878
+ ];
879
+ for (const [field, message] of forbidden) {
880
+ if (table[field] !== void 0) ctx.addIssue({ code: "custom", message, path: [field] });
881
+ }
882
+ if (table.policies?.write) {
883
+ ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
884
+ }
885
+ }
886
+ function validateRenames(columns, names, ctx) {
887
+ const renameSources = /* @__PURE__ */ new Map();
888
+ for (const name of names) {
889
+ const renamedFrom = columns[name]?.renamed_from;
890
+ if (!renamedFrom) continue;
891
+ if (renamedFrom === name || names.includes(renamedFrom)) {
892
+ ctx.addIssue({
893
+ code: "custom",
894
+ message: "renamed_from must name a column that no longer exists",
895
+ path: ["columns", name, "renamed_from"]
896
+ });
897
+ }
898
+ const claimed = renameSources.get(renamedFrom);
899
+ if (claimed) {
900
+ ctx.addIssue({
901
+ code: "custom",
902
+ message: `renamed_from '${renamedFrom}' is already claimed by '${claimed}'`,
903
+ path: ["columns", name, "renamed_from"]
904
+ });
905
+ }
906
+ renameSources.set(renamedFrom, name);
907
+ }
908
+ }
909
+ function validateColumnTable(table, ctx) {
910
+ const columns = table.columns ?? {};
911
+ const names = Object.keys(columns);
912
+ if (names.length === 0) {
913
+ ctx.addIssue({ code: "custom", message: "a table must declare columns or a source", path: ["columns"] });
914
+ return;
915
+ }
916
+ const primaryKeys = names.filter((name) => columns[name]?.primary_key);
917
+ if (primaryKeys.length !== 1) {
918
+ ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
919
+ }
920
+ validateRenames(columns, names, ctx);
921
+ for (const name of names) {
922
+ const column = columns[name];
923
+ if (!column) continue;
924
+ const cast = column.cast ? castParts(column.cast) : null;
925
+ if (column.cast && cast?.column !== name) {
926
+ ctx.addIssue({
927
+ code: "custom",
928
+ message: "cast must reference the column it is declared on",
929
+ path: ["columns", name, "cast"]
930
+ });
931
+ }
932
+ if (cast && cast.type !== column.type) {
933
+ ctx.addIssue({
934
+ code: "custom",
935
+ message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
936
+ path: ["columns", name, "cast"]
937
+ });
938
+ }
939
+ }
940
+ for (const [position, index2] of (table.indexes ?? []).entries()) {
941
+ if (new Set(index2.columns).size !== index2.columns.length) {
942
+ ctx.addIssue({
943
+ code: "custom",
944
+ message: "index names a column more than once",
945
+ path: ["indexes", position]
946
+ });
947
+ }
948
+ for (const column of index2.columns) {
949
+ if (!names.includes(column)) {
950
+ ctx.addIssue({
951
+ code: "custom",
952
+ message: `index references unknown column '${column}'`,
953
+ path: ["indexes", position]
954
+ });
955
+ }
956
+ }
957
+ }
958
+ }
959
+ function validateLinks(table, ctx) {
960
+ const columnNames = Object.keys(table.columns ?? {});
961
+ for (const [name, link] of Object.entries(table.links ?? {})) {
962
+ if (!table.source && !columnNames.includes(link.on)) {
963
+ ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name, "on"] });
964
+ }
965
+ if (link.enforce && link.to.includes(".")) {
966
+ ctx.addIssue({
967
+ code: "custom",
968
+ message: "enforce is only valid for links inside the namespace; cross-boundary links stay soft",
969
+ path: ["links", name, "enforce"]
970
+ });
971
+ }
972
+ if (link.enforce && table.source) {
973
+ ctx.addIssue({
974
+ code: "custom",
975
+ message: "a synced table cannot enforce foreign keys \u2014 its DDL is owned by the sync",
976
+ path: ["links", name, "enforce"]
977
+ });
978
+ }
979
+ }
980
+ }
981
+ function validateDerivedNameLengths(table, ctx) {
982
+ const worstPolicy = policyName(table.table, "insert");
983
+ if (!table.source && worstPolicy.length > MAX_IDENTIFIER_LENGTH) {
984
+ ctx.addIssue({
985
+ code: "custom",
986
+ message: `table name is too long \u2014 the derived policy name '${worstPolicy}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit`,
987
+ path: ["table"]
988
+ });
989
+ }
990
+ for (const [position, index2] of (table.indexes ?? []).entries()) {
991
+ const name = indexName(table.table, index2);
992
+ if (name.length > MAX_IDENTIFIER_LENGTH) {
993
+ ctx.addIssue({
994
+ code: "custom",
995
+ message: `derived index name '${name}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 set a shorter explicit name`,
996
+ path: ["indexes", position]
997
+ });
998
+ }
999
+ }
1000
+ for (const [name, link] of Object.entries(table.links ?? {})) {
1001
+ const constraint = foreignKeyName(table.table, name);
1002
+ if (link.enforce && constraint.length > MAX_IDENTIFIER_LENGTH) {
1003
+ ctx.addIssue({
1004
+ code: "custom",
1005
+ message: `derived foreign-key name '${constraint}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the link or table name`,
1006
+ path: ["links", name]
1007
+ });
1008
+ }
1009
+ }
1010
+ for (const name of Object.keys(table.actions ?? {})) {
1011
+ const fnName = `action_${name}`;
1012
+ if (fnName.length > MAX_IDENTIFIER_LENGTH) {
1013
+ ctx.addIssue({
1014
+ code: "custom",
1015
+ message: `derived action function name '${fnName}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the action name`,
1016
+ path: ["actions", name]
1017
+ });
1018
+ }
1019
+ }
1020
+ }
1021
+ var TYPE_GROUPS = {
1022
+ integer: "numeric",
1023
+ bigint: "numeric",
1024
+ numeric: "numeric",
1025
+ double: "numeric",
1026
+ text: "text",
1027
+ uuid: "uuid",
1028
+ date: "temporal",
1029
+ timestamptz: "temporal",
1030
+ boolean: "boolean",
1031
+ jsonb: "jsonb"
1032
+ };
1033
+ var columnTypeGroup = (type) => TYPE_GROUPS[type];
1034
+ function validatePolicyColumns(table, ctx) {
1035
+ if (table.source) return;
1036
+ const columns = table.columns ?? {};
1037
+ const columnNames = Object.keys(columns);
1038
+ for (const kind of ["read", "write"]) {
1039
+ const predicate = table.policies?.[kind];
1040
+ if (!predicate) continue;
1041
+ for (const column of referencedColumns(predicate.ast)) {
1042
+ if (!columnNames.includes(column)) {
1043
+ ctx.addIssue({
1044
+ code: "custom",
1045
+ message: `policy references unknown column '${column}'`,
1046
+ path: ["policies", kind]
1047
+ });
1048
+ }
1049
+ }
1050
+ walkPredicate(predicate.ast, (node) => {
1051
+ if (node.kind !== "compare") return;
1052
+ const issue = comparisonTypeIssue(node.left, node.right, columns);
1053
+ if (issue) ctx.addIssue({ code: "custom", message: issue, path: ["policies", kind] });
1054
+ });
1055
+ }
1056
+ }
1057
+ function comparisonTypeIssue(left, right, columns) {
1058
+ const groupOf = (operand) => operand.kind === "column" ? columns[operand.name] ? TYPE_GROUPS[columns[operand.name].type] : null : null;
1059
+ if (left.kind === "column" && right.kind === "column") {
1060
+ const leftGroup = groupOf(left);
1061
+ const rightGroup = groupOf(right);
1062
+ if (leftGroup && rightGroup && leftGroup !== rightGroup) {
1063
+ return `policy compares '${left.name}' (${columns[left.name].type}) with '${right.name}' (${columns[right.name].type}) \u2014 incomparable types`;
1064
+ }
1065
+ return null;
1066
+ }
1067
+ const column = left.kind === "column" ? left : right.kind === "column" ? right : null;
1068
+ const literal = left.kind === "literal" ? left : right.kind === "literal" ? right : null;
1069
+ if (!column || !literal || literal.value === null) return null;
1070
+ const columnGroup = groupOf(column);
1071
+ if (!columnGroup) return null;
1072
+ if (typeof literal.value === "string") return null;
1073
+ const literalGroup = typeof literal.value === "number" ? "numeric" : "boolean";
1074
+ if (literalGroup !== columnGroup) {
1075
+ return `policy compares '${column.name}' (${columns[column.name].type}) with a ${typeof literal.value} literal \u2014 incomparable types`;
1076
+ }
1077
+ return null;
1078
+ }
1079
+
1080
+ // src/engine/security.ts
1081
+ var CONSUMER_TABLE_PRIVILEGES = ["SELECT", "INSERT", "UPDATE", "DELETE"];
1082
+ var POLICY_SUFFIXES = ["read", "insert", "update", "delete", "write"];
1083
+ function securityStatements(definition) {
1084
+ const schema = quoteIdentifier(ormSchemaName(definition.namespace));
1085
+ const role = quoteIdentifier(consumerRoleName(definition.namespace));
1086
+ const tables = columnTables(definition);
1087
+ const force = tables.map(
1088
+ (table) => `ALTER TABLE ${schema}.${quoteIdentifier(table.table)} FORCE ROW LEVEL SECURITY`
1089
+ );
1090
+ const policies = tables.flatMap((table) => policyStatements(schema, table));
1091
+ const grants = tables.map(
1092
+ (table) => `GRANT ${CONSUMER_TABLE_PRIVILEGES.join(", ")} ON ${schema}.${quoteIdentifier(table.table)} TO ${role}`
1093
+ );
1094
+ return [...force, ...policies, `GRANT USAGE ON SCHEMA ${schema} TO ${role}`, ...grants];
1095
+ }
1096
+ function policyDropStatements(schemaName, tables) {
1097
+ const schema = quoteIdentifier(schemaName);
1098
+ return tables.flatMap(
1099
+ (table) => POLICY_SUFFIXES.map(
1100
+ (suffix) => `DROP POLICY IF EXISTS ${quoteIdentifier(policyName(table, suffix))} ON ${schema}.${quoteIdentifier(table)}`
1101
+ )
1102
+ );
1103
+ }
1104
+ function policyStatements(schema, table) {
1105
+ const qualified = `${schema}.${quoteIdentifier(table.table)}`;
1106
+ const policyName2 = (suffix) => quoteIdentifier(policyName(table.table, suffix));
1107
+ const statements = POLICY_SUFFIXES.map((suffix) => `DROP POLICY IF EXISTS ${policyName2(suffix)} ON ${qualified}`);
1108
+ const columnTypes = Object.fromEntries(
1109
+ Object.entries(table.columns ?? {}).map(([name, column]) => [name, column.type])
1110
+ );
1111
+ const read = table.policies?.read;
1112
+ if (read) {
1113
+ statements.push(`CREATE POLICY ${policyName2("read")} ON ${qualified} FOR SELECT USING (${compilePredicate(read.ast, columnTypes)})`);
1114
+ }
1115
+ const write = table.policies?.write;
1116
+ if (write) {
1117
+ const predicate = compilePredicate(write.ast, columnTypes);
1118
+ statements.push(`CREATE POLICY ${policyName2("insert")} ON ${qualified} FOR INSERT WITH CHECK (${predicate})`);
1119
+ statements.push(
1120
+ `CREATE POLICY ${policyName2("update")} ON ${qualified} FOR UPDATE USING (${predicate}) WITH CHECK (${predicate})`
1121
+ );
1122
+ statements.push(`CREATE POLICY ${policyName2("delete")} ON ${qualified} FOR DELETE USING (${predicate})`);
1123
+ }
1124
+ return statements;
1125
+ }
1126
+ var columnTables = (definition) => definition.tables.filter((table) => !table.source);
1127
+ var FACTORIES = {
1128
+ uuid: (name) => pgCore.uuid(name),
1129
+ text: (name) => pgCore.text(name),
1130
+ integer: (name) => pgCore.integer(name),
1131
+ // Mode 'bigint' keeps int8 lossless — mode 'number' silently rounds past
1132
+ // 2^53. The read executor serializes BigInt to string for JSON.
1133
+ bigint: (name) => pgCore.bigint(name, { mode: "bigint" }),
1134
+ numeric: (name) => pgCore.numeric(name),
1135
+ double: (name) => pgCore.doublePrecision(name),
1136
+ boolean: (name) => pgCore.boolean(name),
1137
+ timestamptz: (name) => pgCore.timestamp(name, { withTimezone: true }),
1138
+ date: (name) => pgCore.date(name),
1139
+ jsonb: (name) => pgCore.jsonb(name)
1140
+ };
1141
+ function buildColumn(name, definition) {
1142
+ let column = FACTORIES[definition.type](name);
1143
+ if (definition.primary_key) column = column.primaryKey();
1144
+ if (definition.nullable === false && !definition.primary_key) column = column.notNull();
1145
+ if (definition.unique && !definition.primary_key) column = column.unique();
1146
+ if (definition.default !== void 0) {
1147
+ const value = definition.default;
1148
+ column = column.default(
1149
+ typeof value === "string" && isFunctionDefault(value, definition.type) ? drizzleOrm.sql.raw(value) : value
1150
+ );
1151
+ }
1152
+ return column;
1153
+ }
1154
+
1155
+ // src/engine/tables.ts
1156
+ var SCHEMA_IMPORT_KEY = "$schema";
1157
+ function buildDrizzleEntities(definition) {
1158
+ const schemaName = ormSchemaName(definition.namespace);
1159
+ const schema = pgCore.pgSchema(schemaName);
1160
+ const imports = { [SCHEMA_IMPORT_KEY]: schema };
1161
+ const tables = {};
1162
+ const built = /* @__PURE__ */ new Map();
1163
+ for (const table of sortByEnforcedLinks(columnTables(definition))) {
1164
+ const drizzleTable = buildTable(schema, table, built);
1165
+ built.set(table.table, drizzleTable);
1166
+ imports[table.table] = drizzleTable;
1167
+ tables[table.table] = drizzleTable;
1168
+ }
1169
+ return { imports, tables, schemaName };
1170
+ }
1171
+ function buildTable(schema, table, built) {
1172
+ const columns = Object.fromEntries(
1173
+ Object.entries(table.columns ?? {}).map(([name, column]) => [name, buildColumn(name, column)])
1174
+ );
1175
+ const drizzleTable = schema.table(table.table, columns, (self) => [
1176
+ ...buildIndexes(table, self),
1177
+ ...buildForeignKeys(table, self, built)
1178
+ ]);
1179
+ return drizzleTable.enableRLS();
1180
+ }
1181
+ function buildIndexes(table, self) {
1182
+ return (table.indexes ?? []).map((definition) => {
1183
+ const name = indexName(table.table, definition);
1184
+ const factory = definition.unique ? pgCore.uniqueIndex(name) : pgCore.index(name);
1185
+ const columns = definition.columns.map((column) => self[column]);
1186
+ return factory.on(...columns);
1187
+ });
1188
+ }
1189
+ function buildForeignKeys(table, self, built) {
1190
+ return Object.entries(table.links ?? {}).filter(([, link]) => link.enforce).map(([name, link]) => {
1191
+ const target = link.to === table.table ? self : built.get(link.to);
1192
+ const targetColumn = target && findPrimaryKeyColumn(target);
1193
+ if (!target || !targetColumn) {
1194
+ throw new OrmDefinitionError(`enforced link '${name}' on '${table.table}' has no built target '${link.to}'`);
1195
+ }
1196
+ return pgCore.foreignKey({
1197
+ name: foreignKeyName(table.table, name),
1198
+ columns: [self[link.on]],
1199
+ foreignColumns: [targetColumn]
1200
+ });
1201
+ });
1202
+ }
1203
+ function findPrimaryKeyColumn(target) {
1204
+ const columns = Object.values(target).filter(
1205
+ (value) => value !== null && typeof value === "object" && "primary" in value
1206
+ );
1207
+ return columns.find((column) => column.primary);
1208
+ }
1209
+ function sortByEnforcedLinks(tables) {
1210
+ const remaining = new Map(tables.map((table) => [table.table, table]));
1211
+ const sorted = [];
1212
+ const placed = /* @__PURE__ */ new Set();
1213
+ while (remaining.size > 0) {
1214
+ const ready = [...remaining.values()].filter(
1215
+ (table) => enforcedTargets(table).every((target) => placed.has(target) || !remaining.has(target))
1216
+ );
1217
+ if (ready.length === 0) {
1218
+ throw new OrmDefinitionError(`enforced links form a cycle among: ${[...remaining.keys()].join(", ")}`);
1219
+ }
1220
+ for (const table of ready) {
1221
+ sorted.push(table);
1222
+ placed.add(table.table);
1223
+ remaining.delete(table.table);
1224
+ }
1225
+ }
1226
+ return sorted;
1227
+ }
1228
+ var enforcedTargets = (table) => Object.values(table.links ?? {}).filter((link) => link.enforce && !isQualifiedReference(link.to) && link.to !== table.table).map((link) => link.to);
1229
+
1230
+ // src/engine/snapshot.ts
1231
+ function generateSnapshot(definition, previousId2) {
1232
+ const { imports, schemaName } = buildDrizzleEntities(definition);
1233
+ return api.generateDrizzleJson(imports, previousId2, [schemaName]);
1234
+ }
1235
+ var emptySnapshot = (namespace) => api.generateDrizzleJson({}, void 0, [ormSchemaName(namespace)]);
1236
+ var CURRENT_SNAPSHOT_VERSION = api.generateDrizzleJson({}).version;
1237
+ function normalizeSnapshot(snapshot) {
1238
+ const upgraded = api.upPgSnapshot(snapshot);
1239
+ const version = upgraded.version;
1240
+ if (version !== CURRENT_SNAPSHOT_VERSION) {
1241
+ throw new OrmDefinitionError(
1242
+ `snapshot format '${String(version)}' is not supported by this drizzle-kit (expected '${CURRENT_SNAPSHOT_VERSION}') \u2014 upgrade @sequenceholdings/orm`
1243
+ );
1244
+ }
1245
+ return upgraded;
1246
+ }
1247
+ function diffStatements(previous, current) {
1248
+ assertUnambiguousDiff(previous, current);
1249
+ return api.generateMigration(previous, current);
1250
+ }
1251
+ function assertUnambiguousDiff(previous, current) {
1252
+ const previousTables = tablesOf(previous);
1253
+ const currentTables = tablesOf(current);
1254
+ const removedTables = [...previousTables.keys()].filter((name) => !currentTables.has(name));
1255
+ const addedTables = [...currentTables.keys()].filter((name) => !previousTables.has(name));
1256
+ if (removedTables.length > 0 && addedTables.length > 0) {
1257
+ throw new OrmDefinitionError(
1258
+ `ambiguous diff: table(s) added (${addedTables.join(", ")}) and removed (${removedTables.join(", ")}) in the same change \u2014 ship the add and the drop as separate changes`
1259
+ );
1260
+ }
1261
+ for (const [name, previousColumns] of previousTables) {
1262
+ const currentColumns = currentTables.get(name);
1263
+ if (!currentColumns) continue;
1264
+ const removed = [...previousColumns].filter((column) => !currentColumns.has(column));
1265
+ const added = [...currentColumns].filter((column) => !previousColumns.has(column));
1266
+ if (removed.length > 0 && added.length > 0) {
1267
+ throw new OrmDefinitionError(
1268
+ `ambiguous diff on '${name}': column(s) added (${added.join(", ")}) and removed (${removed.join(", ")}) in the same change \u2014 declare \`renamed_from\` for a rename, or ship the add and the drop as separate changes`
1269
+ );
1270
+ }
1271
+ }
1272
+ }
1273
+ function tablesOf(snapshot) {
1274
+ const tables = snapshot.tables ?? {};
1275
+ return new Map(
1276
+ Object.values(tables).map((table) => [`${table.schema}.${table.name}`, new Set(Object.keys(table.columns))])
1277
+ );
1278
+ }
1279
+ function applyMigrationHints(previous, definition) {
1280
+ const adjusted = structuredClone(previous);
1281
+ const statements = [];
1282
+ const schemaName = ormSchemaName(definition.namespace);
1283
+ const qualified = (table) => `${quoteIdentifier(schemaName)}.${quoteIdentifier(table)}`;
1284
+ for (const table of columnTables(definition)) {
1285
+ const snapshotTable = findTable(adjusted, schemaName, table.table);
1286
+ if (!snapshotTable) continue;
1287
+ for (const [name, column] of Object.entries(table.columns ?? {})) {
1288
+ if (column.renamed_from) {
1289
+ const previousColumn = snapshotTable.columns[column.renamed_from];
1290
+ if (previousColumn && snapshotTable.columns[name]) {
1291
+ throw new OrmDefinitionError(
1292
+ `'${table.table}.${name}' declares renamed_from '${column.renamed_from}', but both columns exist in the previous schema \u2014 a rename cannot overwrite a live column`
1293
+ );
1294
+ }
1295
+ if (previousColumn) {
1296
+ statements.push(
1297
+ `ALTER TABLE ${qualified(table.table)} RENAME COLUMN ${quoteIdentifier(column.renamed_from)} TO ${quoteIdentifier(name)}`
1298
+ );
1299
+ delete snapshotTable.columns[column.renamed_from];
1300
+ snapshotTable.columns[name] = { ...previousColumn, name };
1301
+ } else if (!snapshotTable.columns[name]) {
1302
+ throw new OrmDefinitionError(
1303
+ `'${table.table}.${name}' declares renamed_from '${column.renamed_from}', but the previous schema has neither column \u2014 check the declared source name`
1304
+ );
1305
+ }
1306
+ }
1307
+ const cast = column.cast ? castParts(column.cast) : null;
1308
+ if (cast) {
1309
+ const current = snapshotTable.columns[name];
1310
+ const targetType = sqlTypeName(column.type);
1311
+ if (current && current.type !== targetType) {
1312
+ statements.push(
1313
+ `ALTER TABLE ${qualified(table.table)} ALTER COLUMN ${quoteIdentifier(name)} TYPE ${targetType} USING ${quoteIdentifier(name)}::${targetType}`
1314
+ );
1315
+ current.type = targetType;
1316
+ }
1317
+ }
1318
+ }
1319
+ }
1320
+ return { statements, adjusted };
1321
+ }
1322
+ function findTable(snapshot, schema, table) {
1323
+ const tables = snapshot.tables ?? {};
1324
+ return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
1325
+ }
1326
+ function snapshotTableNames(snapshot, schema) {
1327
+ const tables = snapshot.tables ?? {};
1328
+ return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
1329
+ }
1330
+ var OWNED_STATEMENT = new RegExp(
1331
+ [
1332
+ String.raw`^\s*(CREATE|DROP)\s+POLICY\b`,
1333
+ String.raw`^\s*(GRANT|REVOKE)\b`,
1334
+ String.raw`^\s*(CREATE|DROP|ALTER)\s+ROLE\b`,
1335
+ String.raw`^\s*(CREATE|DROP)\s+(OR\s+REPLACE\s+)?VIEW\b`,
1336
+ // Only ever appears in an RLS toggle (ALTER TABLE … [ENABLE|FORCE] ROW LEVEL
1337
+ // SECURITY) — a three-word phrase, never an identifier.
1338
+ String.raw`\bROW LEVEL SECURITY\b`
1339
+ ].join("|"),
1340
+ "i"
1341
+ );
1342
+ function isOwnedStatement(statement) {
1343
+ return OWNED_STATEMENT.test(statement);
1344
+ }
1345
+ async function pendingKitStatements({
1346
+ definition,
1347
+ db
1348
+ }) {
1349
+ const { imports, schemaName } = buildDrizzleEntities(definition);
1350
+ const { statementsToExecute } = await withExitGuard(
1351
+ () => api.pushSchema(imports, shimExecute(db), [schemaName])
1352
+ );
1353
+ return statementsToExecute.filter((statement) => !OWNED_STATEMENT.test(statement));
1354
+ }
1355
+ function shimExecute(db) {
1356
+ const execute = db.execute.bind(db);
1357
+ return {
1358
+ execute: async (query) => {
1359
+ const result = await execute(query);
1360
+ if (result && typeof result === "object" && "rows" in result) return result;
1361
+ return { rows: result };
1362
+ }
1363
+ };
1364
+ }
1365
+ var exitGuardChain = Promise.resolve();
1366
+ function withExitGuard(run) {
1367
+ const next = exitGuardChain.then(() => guardedRun(run));
1368
+ exitGuardChain = next.then(
1369
+ () => void 0,
1370
+ () => void 0
1371
+ );
1372
+ return next;
1373
+ }
1374
+ async function guardedRun(run) {
1375
+ const originalExit = process.exit;
1376
+ process.exit = ((code) => {
1377
+ throw new Error(`drizzle-kit attempted process.exit(${code ?? 0}) during pushSchema introspection`);
1378
+ });
1379
+ try {
1380
+ return await run();
1381
+ } finally {
1382
+ process.exit = originalExit;
1383
+ }
1384
+ }
1385
+
1386
+ // src/engine/index.ts
1387
+ var STATEMENT_KINDS = ["hint", "ddl", "security", "function"];
1388
+ async function planMigration({
1389
+ definition,
1390
+ previousSnapshot
1391
+ }) {
1392
+ const schema = ormSchemaName(definition.namespace);
1393
+ const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(definition.namespace);
1394
+ const hints = applyMigrationHints(previous, definition);
1395
+ const snapshot = generateSnapshot(definition, previousId(previous));
1396
+ const ddl = await diffStatements(hints.adjusted, snapshot);
1397
+ const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
1398
+ const statements = [
1399
+ ...policyDrops.map((sql2) => ({ kind: "security", sql: sql2 })),
1400
+ ...hints.statements.map((sql2) => ({ kind: "hint", sql: sql2 })),
1401
+ ...ddl.map((sql2) => ({ kind: "ddl", sql: sql2.replace(/;\s*$/, "") })),
1402
+ ...securityStatements(definition).map((sql2) => ({ kind: "security", sql: sql2 })),
1403
+ // PREPARE each action's executes first (plpgsql bodies don't check columns
1404
+ // at CREATE), then create the functions. Both reference the tables the DDL
1405
+ // above creates, so they close the plan.
1406
+ ...actionPrepareChecks(definition).map((sql2) => ({ kind: "function", sql: sql2 })),
1407
+ ...actionFunctionStatements(definition).map((sql2) => ({ kind: "function", sql: sql2 }))
1408
+ ];
1409
+ return { namespace: definition.namespace, schema, statements, snapshot };
1410
+ }
1411
+ var compileNamespace = (definition) => planMigration({ definition });
1412
+ var previousId = (snapshot) => snapshot.id;
1413
+ function stableJson(value) {
1414
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
1415
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
1416
+ 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)}`);
1417
+ return `{${entries.join(",")}}`;
1418
+ }
1419
+ function emptyToNull(value) {
1420
+ if (value == null) return null;
1421
+ if (Array.isArray(value)) return value.length === 0 ? null : value;
1422
+ if (typeof value === "object" && Object.keys(value).length === 0) return null;
1423
+ return value;
1424
+ }
1425
+ function canonicalTable(table) {
1426
+ return {
1427
+ table: table.table,
1428
+ description: table.description ?? null,
1429
+ columns: table.columns ?? null,
1430
+ source: table.source ?? null,
1431
+ links: emptyToNull(table.links),
1432
+ indexes: emptyToNull(table.indexes),
1433
+ policies: {
1434
+ read: table.policies?.read?.source ?? null,
1435
+ write: table.policies?.write?.source ?? null
1436
+ },
1437
+ actions: emptyToNull(
1438
+ Object.fromEntries(
1439
+ Object.entries(table.actions ?? {}).map(([name, action]) => [
1440
+ name,
1441
+ {
1442
+ description: action.description ?? null,
1443
+ requires: action.requires?.source ?? null,
1444
+ params: emptyToNull(action.params),
1445
+ validates: emptyToNull(action.validates?.map((entry) => entry.source)),
1446
+ executes: action.executes,
1447
+ returns: emptyToNull(action.returns),
1448
+ audit: action.audit ?? null
1449
+ }
1450
+ ])
1451
+ )
1452
+ )
1453
+ };
1454
+ }
1455
+ function canonicalCapabilities(capabilities) {
1456
+ const { definitions, capabilities: declared } = capabilities.roles;
1457
+ const isEmpty = Object.keys(definitions).length === 0 && Object.keys(declared).length === 0 && capabilities.uses.length === 0;
1458
+ if (isEmpty) return null;
1459
+ return {
1460
+ roles: {
1461
+ definitions: Object.fromEntries(
1462
+ Object.entries(definitions).map(([key, def]) => [
1463
+ key,
1464
+ {
1465
+ ...def.description !== void 0 ? { description: def.description } : {},
1466
+ default: def.default,
1467
+ inherits: [...def.inherits].sort(),
1468
+ grants: [...def.grants].sort()
1469
+ }
1470
+ ])
1471
+ ),
1472
+ capabilities: Object.fromEntries(
1473
+ Object.entries(declared).map(([key, cap]) => [
1474
+ key,
1475
+ cap.description !== void 0 ? { description: cap.description } : {}
1476
+ ])
1477
+ )
1478
+ },
1479
+ uses: [...capabilities.uses].sort()
1480
+ };
1481
+ }
1482
+ function canonicalizeDefinition(definition) {
1483
+ return {
1484
+ namespace: definition.namespace,
1485
+ description: definition.description,
1486
+ capabilities: canonicalCapabilities(definition.capabilities),
1487
+ tables: definition.tables.map(canonicalTable)
1488
+ };
1489
+ }
1490
+ function computeDefinitionHash(definition) {
1491
+ return crypto.createHash("sha256").update(HASH_DOMAIN).update(stableJson(canonicalizeDefinition(definition))).digest("hex");
1492
+ }
1493
+ var CUSTOM_ROLE_KEY_RE = /^[a-z0-9-]+$/;
1494
+ var customRoleKeySchema = zod.z.string().min(1).max(64).regex(
1495
+ CUSTOM_ROLE_KEY_RE,
1496
+ "Role and capability keys must match [a-z0-9-]+"
1497
+ );
1498
+ var MAX_CUSTOM_ROLE_DEFINITIONS = 50;
1499
+ var MAX_CUSTOM_ROLE_CAPABILITIES = 100;
1500
+ var customRoleCapabilityDeclarationSchema = zod.z.object({
1501
+ description: zod.z.string().max(500).optional()
1502
+ });
1503
+ var customRoleDefinitionSchema = zod.z.object({
1504
+ description: zod.z.string().max(500).optional(),
1505
+ /**
1506
+ * Grants this role to the host's entire runtime audience. Resolved at the
1507
+ * app layer at claim time — every caller who passes the host floor
1508
+ * (`end_user`) holds the default roles; no tuple is written for it.
1509
+ */
1510
+ default: zod.z.boolean().default(false),
1511
+ /** Role keys whose grants this role also receives (privilege waterfall). */
1512
+ inherits: zod.z.array(customRoleKeySchema).default([]),
1513
+ /** Capability keys this role grants directly. */
1514
+ grants: zod.z.array(customRoleKeySchema).default([])
1515
+ });
1516
+ var customRolesBlockSchema = zod.z.object({
1517
+ /** Roles grantable to principals, keyed by role key. */
1518
+ definitions: zod.z.record(customRoleKeySchema, customRoleDefinitionSchema).default({}),
1519
+ /** Leaf capability keys the host's code checks, keyed by capability key. */
1520
+ capabilities: zod.z.record(customRoleKeySchema, customRoleCapabilityDeclarationSchema).default({})
1521
+ }).superRefine((block, ctx) => {
1522
+ const roleKeys = Object.keys(block.definitions);
1523
+ const capabilityKeys = new Set(Object.keys(block.capabilities));
1524
+ if (roleKeys.length > MAX_CUSTOM_ROLE_DEFINITIONS) {
1525
+ ctx.addIssue({
1526
+ code: "custom",
1527
+ message: `At most ${MAX_CUSTOM_ROLE_DEFINITIONS} role definitions are allowed`,
1528
+ path: ["definitions"]
1529
+ });
1530
+ }
1531
+ if (capabilityKeys.size > MAX_CUSTOM_ROLE_CAPABILITIES) {
1532
+ ctx.addIssue({
1533
+ code: "custom",
1534
+ message: `At most ${MAX_CUSTOM_ROLE_CAPABILITIES} capabilities are allowed`,
1535
+ path: ["capabilities"]
1536
+ });
1537
+ }
1538
+ for (const key of roleKeys) {
1539
+ if (capabilityKeys.has(key)) {
1540
+ ctx.addIssue({
1541
+ code: "custom",
1542
+ message: `Key "${key}" is declared as both a role and a capability`,
1543
+ path: ["definitions", key]
1544
+ });
1545
+ }
1546
+ }
1547
+ const roleKeySet = new Set(roleKeys);
1548
+ for (const [roleKey, definition] of Object.entries(block.definitions)) {
1549
+ const seenInherits = /* @__PURE__ */ new Set();
1550
+ for (const inherited of definition.inherits) {
1551
+ if (seenInherits.has(inherited)) {
1552
+ ctx.addIssue({
1553
+ code: "custom",
1554
+ message: `Role "${roleKey}" lists "${inherited}" in inherits more than once`,
1555
+ path: ["definitions", roleKey, "inherits"]
1556
+ });
1557
+ }
1558
+ seenInherits.add(inherited);
1559
+ if (!roleKeySet.has(inherited)) {
1560
+ ctx.addIssue({
1561
+ code: "custom",
1562
+ message: `Role "${roleKey}" inherits unknown role "${inherited}"`,
1563
+ path: ["definitions", roleKey, "inherits"]
1564
+ });
1565
+ }
1566
+ }
1567
+ const seenGrants = /* @__PURE__ */ new Set();
1568
+ for (const granted of definition.grants) {
1569
+ if (seenGrants.has(granted)) {
1570
+ ctx.addIssue({
1571
+ code: "custom",
1572
+ message: `Role "${roleKey}" lists "${granted}" in grants more than once`,
1573
+ path: ["definitions", roleKey, "grants"]
1574
+ });
1575
+ }
1576
+ seenGrants.add(granted);
1577
+ if (!capabilityKeys.has(granted)) {
1578
+ ctx.addIssue({
1579
+ code: "custom",
1580
+ message: `Role "${roleKey}" grants unknown capability "${granted}"`,
1581
+ path: ["definitions", roleKey, "grants"]
1582
+ });
1583
+ }
1584
+ }
1585
+ }
1586
+ const state = /* @__PURE__ */ new Map();
1587
+ const visit = (key, path) => {
1588
+ if (state.get(key) === "done") return null;
1589
+ if (state.get(key) === "visiting") return [...path, key];
1590
+ state.set(key, "visiting");
1591
+ const definition = block.definitions[key];
1592
+ for (const inherited of definition?.inherits ?? []) {
1593
+ if (!roleKeySet.has(inherited)) continue;
1594
+ const cycle = visit(inherited, [...path, key]);
1595
+ if (cycle) return cycle;
1596
+ }
1597
+ state.set(key, "done");
1598
+ return null;
1599
+ };
1600
+ for (const roleKey of roleKeys) {
1601
+ const cycle = visit(roleKey, []);
1602
+ if (cycle) {
1603
+ ctx.addIssue({
1604
+ code: "custom",
1605
+ message: `Role inheritance cycle: ${cycle.join(" -> ")}`,
1606
+ path: ["definitions"]
1607
+ });
1608
+ break;
1609
+ }
1610
+ }
1611
+ });
1612
+ function customRolesDeclared(block) {
1613
+ if (!block) return false;
1614
+ return Object.keys(block.definitions).length > 0 || Object.keys(block.capabilities).length > 0;
1615
+ }
1616
+ var CUSTOM_CAPABILITY_PIN_RE = /^(?:org\/)?[a-z0-9-]+$/;
1617
+ var MAX_CAPABILITY_PINS = 100;
1618
+ var capabilityPinsSchema = zod.z.array(
1619
+ zod.z.string().min(1).max(68).regex(
1620
+ CUSTOM_CAPABILITY_PIN_RE,
1621
+ 'Pins must be "org/<key>" or a bare "<key>" matching [a-z0-9-]+'
1622
+ )
1623
+ ).max(MAX_CAPABILITY_PINS).default([]);
1624
+ function parseCapabilityPin(pin) {
1625
+ return pin.startsWith("org/") ? { scope: "org", key: pin.slice("org/".length) } : { scope: "host", key: pin };
1626
+ }
1627
+ function validateCapabilityPins({
1628
+ uses,
1629
+ roles
1630
+ }) {
1631
+ const errors = [];
1632
+ const seen = /* @__PURE__ */ new Set();
1633
+ const roleKeys = new Set(Object.keys(roles?.definitions ?? {}));
1634
+ for (const pin of uses) {
1635
+ if (seen.has(pin)) errors.push(`Pin "${pin}" is listed more than once`);
1636
+ seen.add(pin);
1637
+ const parsed = parseCapabilityPin(pin);
1638
+ if (parsed.scope === "host" && roleKeys.has(parsed.key)) {
1639
+ errors.push(`Pin "${pin}" references a role \u2014 roles are never pinned`);
1640
+ }
1641
+ }
1642
+ return errors;
1643
+ }
1644
+
1645
+ // src/schema/namespace.ts
1646
+ var EMPTY_CAPABILITIES = { roles: { definitions: {}, capabilities: {} }, uses: [] };
1647
+ function declaredCapabilityKeys(capabilities) {
1648
+ return /* @__PURE__ */ new Set([...Object.keys(capabilities.roles.capabilities), ...capabilities.uses]);
1649
+ }
1650
+ function buildNamespaceDefinition(files, config) {
1651
+ if (files.length === 0) throw new OrmDefinitionError("no table definitions found");
1652
+ const namespace = files[0].table.namespace;
1653
+ if (config && config.namespace !== namespace) {
1654
+ throw new OrmDefinitionError(`namespace '${config.namespace}' conflicts with '${namespace}'`, "namespace.yaml");
1655
+ }
1656
+ for (const { file, table } of files) {
1657
+ if (table.namespace !== namespace) {
1658
+ throw new OrmDefinitionError(`namespace '${table.namespace}' conflicts with '${namespace}'`, file);
1659
+ }
1660
+ }
1661
+ const seen = /* @__PURE__ */ new Map();
1662
+ for (const { file, table } of files) {
1663
+ const existing = seen.get(table.table);
1664
+ if (existing) throw new OrmDefinitionError(`table '${table.table}' is already defined in ${existing}`, file);
1665
+ seen.set(table.table, file);
1666
+ }
1667
+ const actionsSeen = /* @__PURE__ */ new Map();
1668
+ for (const { file, table } of files) {
1669
+ for (const name of Object.keys(table.actions ?? {})) {
1670
+ const existing = actionsSeen.get(name);
1671
+ if (existing) throw new OrmDefinitionError(`action '${name}' is already defined in ${existing}`, file);
1672
+ actionsSeen.set(name, file);
1673
+ }
1674
+ }
1675
+ const capabilities = config?.capabilities ? { roles: config.capabilities.roles ?? EMPTY_CAPABILITIES.roles, uses: config.capabilities.uses ?? [] } : EMPTY_CAPABILITIES;
1676
+ const pinIssues = validateCapabilityPins({ uses: capabilities.uses, roles: capabilities.roles });
1677
+ if (pinIssues.length > 0) throw new OrmDefinitionError(pinIssues.join("; "), "namespace.yaml");
1678
+ const tables = files.map(({ table }) => table).sort((a, b) => a.table.localeCompare(b.table));
1679
+ const byName = new Map(tables.map((table) => [table.table, table]));
1680
+ validateLocalLinks(files, byName);
1681
+ validateActionValidates(files, byName);
1682
+ validateIndexNames(files);
1683
+ validateCapabilityRefs(files, declaredCapabilityKeys(capabilities));
1684
+ return { namespace, description: config?.description ?? null, capabilities, tables };
1685
+ }
1686
+ function validateActionValidates(files, byName) {
1687
+ for (const { file, table } of files) {
1688
+ for (const [actionName, action] of Object.entries(table.actions ?? {})) {
1689
+ for (const entry of action.validates ?? []) {
1690
+ if (entry.table.includes(".")) continue;
1691
+ const target = byName.get(entry.table);
1692
+ if (!target) {
1693
+ throw new OrmDefinitionError(`action '${actionName}' validates against unknown table '${entry.table}'`, file);
1694
+ }
1695
+ if (target.source) {
1696
+ throw new OrmDefinitionError(
1697
+ `action '${actionName}' validates against synced table '${entry.table}' \u2014 synced tables can't be validates targets`,
1698
+ file
1699
+ );
1700
+ }
1701
+ const paramType = action.params?.[entry.param]?.type;
1702
+ const targetPrimaryKey = Object.values(target.columns ?? {}).find((column) => column.primary_key);
1703
+ if (paramType && targetPrimaryKey && columnTypeGroup(paramType) !== columnTypeGroup(targetPrimaryKey.type)) {
1704
+ throw new OrmDefinitionError(
1705
+ `action '${actionName}' validates '${entry.param}' (${paramType}) against '${entry.table}' whose primary key is ${targetPrimaryKey.type}`,
1706
+ file
1707
+ );
1708
+ }
1709
+ }
1710
+ }
1711
+ }
1712
+ }
1713
+ function validateIndexNames(files) {
1714
+ const seen = /* @__PURE__ */ new Map();
1715
+ const claim = (name, label, file) => {
1716
+ const existing = seen.get(name);
1717
+ if (existing) {
1718
+ throw new OrmDefinitionError(`relation name '${name}' (${label}) collides with ${existing}`, file);
1719
+ }
1720
+ seen.set(name, `${label} (${file})`);
1721
+ };
1722
+ for (const { file, table } of files) {
1723
+ claim(table.table, `table '${table.table}'`, file);
1724
+ }
1725
+ for (const { file, table } of files) {
1726
+ if (table.source) continue;
1727
+ claim(`${table.table}_pkey`, `implicit primary-key index of '${table.table}'`, file);
1728
+ for (const [name, column] of Object.entries(table.columns ?? {})) {
1729
+ if (column.unique && !column.primary_key) {
1730
+ claim(`${table.table}_${name}_unique`, `implicit unique constraint of '${table.table}.${name}'`, file);
1731
+ }
1732
+ }
1733
+ }
1734
+ for (const { file, table } of files) {
1735
+ for (const definition of table.indexes ?? []) {
1736
+ claim(indexName(table.table, definition), `index on '${table.table}'`, file);
1737
+ }
1738
+ }
1739
+ }
1740
+ function validateLocalLinks(files, byName) {
1741
+ for (const { file, table } of files) {
1742
+ for (const [name, link] of Object.entries(table.links ?? {})) {
1743
+ if (isQualifiedReference(link.to)) continue;
1744
+ const target = byName.get(link.to);
1745
+ if (!target) {
1746
+ throw new OrmDefinitionError(`link '${name}' references unknown table '${link.to}'`, file);
1747
+ }
1748
+ if (link.enforce && target.source) {
1749
+ throw new OrmDefinitionError(`link '${name}' cannot enforce a foreign key onto a synced table`, file);
1750
+ }
1751
+ const sourceType = table.columns?.[link.on]?.type;
1752
+ const targetPrimaryKey = Object.values(target.columns ?? {}).find((column) => column.primary_key);
1753
+ if (sourceType && targetPrimaryKey && columnTypeGroup(sourceType) !== columnTypeGroup(targetPrimaryKey.type)) {
1754
+ throw new OrmDefinitionError(
1755
+ `link '${name}' can never join: '${link.on}' is ${sourceType} but '${link.to}' has a ${targetPrimaryKey.type} primary key`,
1756
+ file
1757
+ );
1758
+ }
1759
+ }
1760
+ }
1761
+ }
1762
+ function validateCapabilityRefs(files, declared) {
1763
+ for (const { file, table } of files) {
1764
+ const referenced = [
1765
+ ...table.policies?.read ? referencedCapabilities(table.policies.read.ast) : [],
1766
+ ...table.policies?.write ? referencedCapabilities(table.policies.write.ast) : [],
1767
+ ...Object.values(table.actions ?? {}).flatMap((action) => [
1768
+ ...action.requires ? referencedCapabilities(action.requires.ast) : [],
1769
+ ...referencedExecuteCapabilities(action)
1770
+ ])
1771
+ ];
1772
+ for (const key of referenced) {
1773
+ if (!declared.has(key)) {
1774
+ throw new OrmDefinitionError(
1775
+ `capability('${key}') is not declared \u2014 add it under capabilities.roles.capabilities or capabilities.uses in namespace.yaml`,
1776
+ file
1777
+ );
1778
+ }
1779
+ }
1780
+ }
1781
+ }
1782
+ var namespaceConfigSchema = zod.z.strictObject({
1783
+ namespace: namespaceSchema,
1784
+ description: zod.z.string().optional(),
1785
+ capabilities: zod.z.strictObject({
1786
+ roles: customRolesBlockSchema.optional(),
1787
+ uses: capabilityPinsSchema.optional()
1788
+ }).optional()
1789
+ });
1790
+
1791
+ // src/loader.ts
1792
+ function parseTableFile(file, contents) {
1793
+ const parsed = tableFileSchema.safeParse(parseYaml(file, contents));
1794
+ if (!parsed.success) throw new OrmDefinitionError(formatIssues(parsed.error), file);
1795
+ return { file, table: parsed.data };
1796
+ }
1797
+ function parseDefinitionSources(sources) {
1798
+ let config;
1799
+ const tables = [];
1800
+ for (const source of sources) {
1801
+ if (NAMESPACE_CONFIG_FILENAMES.has(source.path)) {
1802
+ if (config) throw new OrmDefinitionError("duplicate namespace config file", source.path);
1803
+ const parsed = namespaceConfigSchema.safeParse(parseYaml(source.path, source.contents));
1804
+ if (!parsed.success) throw new OrmDefinitionError(formatIssues(parsed.error), source.path);
1805
+ config = parsed.data;
1806
+ continue;
1807
+ }
1808
+ tables.push(parseTableFile(source.path, source.contents));
1809
+ }
1810
+ return buildNamespaceDefinition(tables, config);
1811
+ }
1812
+ async function loadNamespaceDefinition(dir) {
1813
+ const files = await listDefinitionFiles(dir);
1814
+ if (files.length === 0) throw new OrmDefinitionError(`no ${DEFINITION_FILE_EXTENSIONS.join("/")} files in ${dir}`);
1815
+ const sources = [];
1816
+ for (const file of files) {
1817
+ const contents = await promises.readFile(path.join(dir, file), "utf8");
1818
+ if (Buffer.byteLength(contents, "utf8") > MAX_DEFINITION_FILE_BYTES) {
1819
+ throw new OrmDefinitionError(`exceeds the maximum definition file size of ${MAX_DEFINITION_FILE_BYTES} bytes`, file);
1820
+ }
1821
+ sources.push({ path: file, contents });
1822
+ }
1823
+ const definition = parseDefinitionSources(sources);
1824
+ return { definition, contentHash: computeDefinitionHash(definition), files };
1825
+ }
1826
+ function parseYaml(file, contents) {
1827
+ try {
1828
+ return jsYaml.load(contents);
1829
+ } catch (error) {
1830
+ throw new OrmDefinitionError(error instanceof Error ? error.message : "invalid YAML", file);
1831
+ }
1832
+ }
1833
+ var isDefinitionFile = (name) => DEFINITION_FILE_EXTENSIONS.some((extension) => path.extname(name) === extension);
1834
+ async function listDefinitionFiles(dir) {
1835
+ const entries = await promises.readdir(dir, { withFileTypes: true });
1836
+ return entries.filter((entry) => entry.isFile() && isDefinitionFile(entry.name)).map((entry) => entry.name).sort();
1837
+ }
1838
+ function formatIssues(error) {
1839
+ return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
1840
+ }
1841
+
1842
+ // src/migrations/classify.ts
1843
+ var TYPE_CHANGE = /\bALTER\s+COLUMN\s+"?[a-z_][a-z0-9_]*"?\s+(?:SET\s+DATA\s+)?TYPE\b/i;
1844
+ var RULES = [
1845
+ { pattern: /^\s*DROP\s+SCHEMA\b/i, reason: "drops the namespace schema and everything in it" },
1846
+ { pattern: /^\s*DROP\s+TABLE\b/i, reason: "drops a table and all of its rows" },
1847
+ { pattern: /^\s*TRUNCATE\b/i, reason: "deletes every row" },
1848
+ { pattern: /\bDROP\s+COLUMN\b/i, reason: "drops a column and its data" },
1849
+ {
1850
+ pattern: TYPE_CHANGE,
1851
+ reason: "changes a column type without a declared cast (add `cast: '<column>::<type>'` to make the conversion explicit)"
1852
+ }
1853
+ ];
1854
+ function classifyDestructive(statements) {
1855
+ const flagged = [];
1856
+ for (const statement of statements) {
1857
+ if (statement.kind === "ddl") {
1858
+ const rule = RULES.find(({ pattern }) => pattern.test(statement.sql));
1859
+ if (rule) flagged.push({ sql: statement.sql, reason: rule.reason });
1860
+ } else if (statement.kind === "hint" && TYPE_CHANGE.test(statement.sql)) {
1861
+ flagged.push({ sql: statement.sql, reason: "a declared cast changes a column type and can lose data (numeric\u2192integer rounds, a narrowing cast truncates)" });
1862
+ }
1863
+ }
1864
+ return flagged;
1865
+ }
1866
+ var MIGRATIONS_DIRNAME = "migrations";
1867
+ var META_DIRNAME = "meta";
1868
+ var JOURNAL_FILENAME = "journal.json";
1869
+ var journalEntrySchema = zod.z.object({
1870
+ ordinal: zod.z.number().int().positive(),
1871
+ name: zod.z.string().min(1),
1872
+ content_hash: zod.z.string().length(64),
1873
+ /** Definitions hash the migration was diffed FROM — null on the first. Replay checks an environment sits at this base. */
1874
+ previous_content_hash: zod.z.string().length(64).nullable(),
1875
+ checksum: zod.z.string().length(64),
1876
+ /** sha256 of the snapshot FILE — the next diff builds on this snapshot, so a hand-edit must fail `--check` too. */
1877
+ snapshot_checksum: zod.z.string().length(64),
1878
+ destructive: zod.z.boolean()
1879
+ });
1880
+ var journalSchema = zod.z.object({
1881
+ version: zod.z.literal(1),
1882
+ namespace: zod.z.string().min(1),
1883
+ entries: zod.z.array(journalEntrySchema)
1884
+ });
1885
+ var emptyJournal = (namespace) => ({ version: 1, namespace, entries: [] });
1886
+ async function readMigrationWorkspace(dir, namespace) {
1887
+ const journalPath = path.join(dir, MIGRATIONS_DIRNAME, META_DIRNAME, JOURNAL_FILENAME);
1888
+ const raw = await promises.readFile(journalPath, "utf8").catch(() => null);
1889
+ if (raw === null) return { journal: emptyJournal(namespace), tipSnapshot: void 0 };
1890
+ const parsed = journalSchema.safeParse(parseJson(journalPath, raw));
1891
+ if (!parsed.success) {
1892
+ throw new OrmDefinitionError(`invalid migration journal: ${parsed.error.issues[0]?.message ?? "unreadable"}`, journalPath);
1893
+ }
1894
+ if (parsed.data.namespace !== namespace) {
1895
+ throw new OrmDefinitionError(
1896
+ `migration journal belongs to namespace '${parsed.data.namespace}', definitions are '${namespace}'`,
1897
+ journalPath
1898
+ );
1899
+ }
1900
+ parsed.data.entries.forEach((entry, index2) => {
1901
+ if (entry.ordinal !== index2 + 1) {
1902
+ throw new OrmDefinitionError(`migration journal entries must be contiguous from 1 (found ${entry.name})`, journalPath);
1903
+ }
1904
+ });
1905
+ const tip = parsed.data.entries.at(-1);
1906
+ return { journal: parsed.data, tipSnapshot: tip ? await readSnapshot(dir, tip.ordinal) : void 0 };
1907
+ }
1908
+ async function readSnapshotText(dir, ordinal) {
1909
+ const path$1 = path.join(dir, MIGRATIONS_DIRNAME, META_DIRNAME, `${pad(ordinal)}.snapshot.json`);
1910
+ return promises.readFile(path$1, "utf8").catch(() => {
1911
+ throw new OrmDefinitionError("missing migration snapshot", path$1);
1912
+ });
1913
+ }
1914
+ async function readSnapshot(dir, ordinal) {
1915
+ const path$1 = path.join(dir, MIGRATIONS_DIRNAME, META_DIRNAME, `${pad(ordinal)}.snapshot.json`);
1916
+ return parseJson(path$1, await readSnapshotText(dir, ordinal));
1917
+ }
1918
+ async function readMigrationSql(dir, entry) {
1919
+ const path$1 = path.join(dir, MIGRATIONS_DIRNAME, `${entry.name}.sql`);
1920
+ return promises.readFile(path$1, "utf8").catch(() => {
1921
+ throw new OrmDefinitionError("missing migration file", path$1);
1922
+ });
1923
+ }
1924
+ var checksumOf = (text2) => crypto.createHash("sha256").update(text2).digest("hex");
1925
+ var migrationName = (ordinal, contentHash) => `${pad(ordinal)}_${contentHash.slice(0, 12)}`;
1926
+ function renderMigrationSql({
1927
+ name,
1928
+ namespace,
1929
+ contentHash,
1930
+ plan
1931
+ }) {
1932
+ const header = [
1933
+ `-- ${name}`,
1934
+ `-- namespace: ${namespace}`,
1935
+ `-- content hash: ${contentHash}`,
1936
+ "-- generated by `seq-studio orm diff` \u2014 review and commit, do not edit"
1937
+ ].join("\n");
1938
+ return [header, ...plan.statements.map((statement) => `${statement.sql};`)].join("\n\n") + "\n";
1939
+ }
1940
+ async function writeMigration({
1941
+ dir,
1942
+ workspace,
1943
+ plan,
1944
+ contentHash,
1945
+ destructive
1946
+ }) {
1947
+ const tip = workspace.journal.entries.at(-1);
1948
+ const ordinal = (tip?.ordinal ?? 0) + 1;
1949
+ const name = migrationName(ordinal, contentHash);
1950
+ const sql2 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
1951
+ const metaDir = path.join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
1952
+ await promises.mkdir(metaDir, { recursive: true });
1953
+ const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
1954
+ `;
1955
+ await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql2);
1956
+ await promises.writeFile(path.join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
1957
+ const journal = {
1958
+ ...workspace.journal,
1959
+ entries: [
1960
+ ...workspace.journal.entries,
1961
+ {
1962
+ ordinal,
1963
+ name,
1964
+ content_hash: contentHash,
1965
+ previous_content_hash: tip?.content_hash ?? null,
1966
+ checksum: checksumOf(sql2),
1967
+ snapshot_checksum: checksumOf(snapshotText),
1968
+ destructive
1969
+ }
1970
+ ]
1971
+ };
1972
+ await promises.writeFile(path.join(metaDir, JOURNAL_FILENAME), `${JSON.stringify(journal, null, 2)}
1973
+ `);
1974
+ return { name, ordinal, destructive };
1975
+ }
1976
+ var pad = (ordinal) => String(ordinal).padStart(4, "0");
1977
+ function parseJson(path, raw) {
1978
+ try {
1979
+ return JSON.parse(raw);
1980
+ } catch {
1981
+ throw new OrmDefinitionError("not valid JSON", path);
1982
+ }
1983
+ }
1984
+
1985
+ // src/migrations/committed.ts
1986
+ async function loadCommittedTipMigration(dir, loaded) {
1987
+ const workspace = await readMigrationWorkspace(dir, loaded.definition.namespace);
1988
+ const tip = workspace.journal.entries.at(-1);
1989
+ if (!tip) return null;
1990
+ if (tip.content_hash !== loaded.contentHash) {
1991
+ throw new OrmDefinitionError(
1992
+ `definitions changed since ${tip.name} \u2014 run \`seq-studio orm diff\` and commit the new migration`,
1993
+ `${MIGRATIONS_DIRNAME}/meta/journal.json`
1994
+ );
1995
+ }
1996
+ const previousSnapshot = tip.ordinal > 1 ? await readSnapshot(dir, tip.ordinal - 1) : void 0;
1997
+ const plan = await planMigration({ definition: loaded.definition, previousSnapshot });
1998
+ const regenerated = renderMigrationSql({
1999
+ name: tip.name,
2000
+ namespace: loaded.definition.namespace,
2001
+ contentHash: loaded.contentHash,
2002
+ plan
2003
+ });
2004
+ if (regenerated !== await readMigrationSql(dir, tip)) {
2005
+ throw new OrmDefinitionError(
2006
+ `${tip.name}.sql does not regenerate from the definitions \u2014 run \`seq-studio orm diff --check\``,
2007
+ MIGRATIONS_DIRNAME
2008
+ );
2009
+ }
2010
+ return {
2011
+ name: tip.name,
2012
+ ordinal: tip.ordinal,
2013
+ previous_content_hash: tip.previous_content_hash,
2014
+ destructive: tip.destructive,
2015
+ checksum: tip.checksum,
2016
+ statements: plan.statements
2017
+ };
2018
+ }
2019
+ async function loadCommittedMigrations(dir, loaded) {
2020
+ const workspace = await readMigrationWorkspace(dir, loaded.definition.namespace);
2021
+ const entries = workspace.journal.entries;
2022
+ const tip = entries.at(-1);
2023
+ if (!tip) return [];
2024
+ if (tip.content_hash !== loaded.contentHash) {
2025
+ throw new OrmDefinitionError(
2026
+ `definitions changed since ${tip.name} \u2014 run \`seq-studio orm diff\` and commit the new migration`,
2027
+ `${MIGRATIONS_DIRNAME}/meta/journal.json`
2028
+ );
2029
+ }
2030
+ return entries.map((entry) => ({
2031
+ name: entry.name,
2032
+ contentHash: entry.content_hash,
2033
+ previousContentHash: entry.previous_content_hash
2034
+ }));
2035
+ }
2036
+
2037
+ // src/migrations/diff.ts
2038
+ async function diffCommand({ dir, allowDestructive, check, log }) {
2039
+ const loaded = await loadNamespaceDefinition(dir);
2040
+ const workspace = await readMigrationWorkspace(dir, loaded.definition.namespace);
2041
+ const tip = workspace.journal.entries.at(-1);
2042
+ if (check) return checkWorkspace({ dir, loaded, workspace, tip, log });
2043
+ if (tip?.content_hash === loaded.contentHash) {
2044
+ log(`no changes \u2014 ${MIGRATIONS_DIRNAME}/${tip.name}.sql already matches the definitions`);
2045
+ return 0;
2046
+ }
2047
+ const plan = await planMigration({ definition: loaded.definition, previousSnapshot: workspace.tipSnapshot });
2048
+ const destructive = classifyDestructive(plan.statements);
2049
+ if (destructive.length > 0 && !allowDestructive) {
2050
+ log("destructive migration blocked \u2014 these statements lose data:");
2051
+ for (const statement of destructive) log(` ${firstLine(statement.sql)} -- ${statement.reason}`);
2052
+ log("retire columns in two phases (`deprecated: true` first, drop later), declare type changes as `cast:`, or re-run with --allow-destructive to consent");
2053
+ return 1;
2054
+ }
2055
+ const written = await writeMigration({
2056
+ dir,
2057
+ workspace,
2058
+ plan,
2059
+ contentHash: loaded.contentHash,
2060
+ destructive: destructive.length > 0
2061
+ });
2062
+ log(
2063
+ `wrote ${MIGRATIONS_DIRNAME}/${written.name}.sql \u2014 ${plan.statements.length} statement(s)` + (written.destructive ? " (DESTRUCTIVE \u2014 consented via --allow-destructive)" : "")
2064
+ );
2065
+ return 0;
2066
+ }
2067
+ async function checkWorkspace({
2068
+ dir,
2069
+ loaded,
2070
+ workspace,
2071
+ tip,
2072
+ log
2073
+ }) {
2074
+ if (!tip) {
2075
+ log(`no committed migrations for '${loaded.definition.namespace}' \u2014 run \`seq-studio orm diff\` and commit the result`);
2076
+ return 1;
2077
+ }
2078
+ let previousId2;
2079
+ let previousHash = null;
2080
+ for (const entry of workspace.journal.entries) {
2081
+ const sql2 = await readMigrationSql(dir, entry);
2082
+ if (checksumOf(sql2) !== entry.checksum) {
2083
+ log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
2084
+ return 1;
2085
+ }
2086
+ const snapshotText = await readSnapshotText(dir, entry.ordinal);
2087
+ if (checksumOf(snapshotText) !== entry.snapshot_checksum) {
2088
+ log(`the snapshot for ${entry.name} does not match its journal checksum \u2014 committed snapshots must not be edited (the next diff builds on it)`);
2089
+ return 1;
2090
+ }
2091
+ const snapshot = JSON.parse(snapshotText);
2092
+ if (entry.ordinal > 1 && snapshot.prevId !== previousId2) {
2093
+ log(`snapshot chain broken at ${entry.name} \u2014 regenerate the workspace with \`seq-studio orm diff\``);
2094
+ return 1;
2095
+ }
2096
+ if (entry.previous_content_hash !== previousHash) {
2097
+ log(`content-hash chain broken at ${entry.name} \u2014 migrations must be appended in order by \`seq-studio orm diff\``);
2098
+ return 1;
2099
+ }
2100
+ previousId2 = snapshot.id;
2101
+ previousHash = entry.content_hash;
2102
+ }
2103
+ if (tip.content_hash !== loaded.contentHash) {
2104
+ log(`definitions changed since ${tip.name} \u2014 run \`seq-studio orm diff\` and commit the new migration`);
2105
+ return 1;
2106
+ }
2107
+ const previousSnapshot = tip.ordinal > 1 ? await readSnapshot(dir, tip.ordinal - 1) : void 0;
2108
+ const plan = await planMigration({ definition: loaded.definition, previousSnapshot });
2109
+ const regenerated = renderMigrationSql({
2110
+ name: tip.name,
2111
+ namespace: loaded.definition.namespace,
2112
+ contentHash: loaded.contentHash,
2113
+ plan
2114
+ });
2115
+ if (regenerated !== await readMigrationSql(dir, tip)) {
2116
+ log(`${MIGRATIONS_DIRNAME}/${tip.name}.sql does not regenerate from the definitions \u2014 re-run \`seq-studio orm diff\``);
2117
+ return 1;
2118
+ }
2119
+ log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
2120
+ return 0;
2121
+ }
2122
+ var firstLine = (sql2) => sql2.split("\n")[0] ?? sql2;
2123
+
2124
+ // src/schema/canonical.ts
2125
+ function parseCanonicalDefinition(value) {
2126
+ const canonical = asCanonicalShape(value);
2127
+ const files = canonical.tables.map((table, index2) => {
2128
+ const input = {
2129
+ namespace: canonical.namespace,
2130
+ table: table["table"],
2131
+ description: table["description"] ?? void 0,
2132
+ columns: table["columns"] ?? void 0,
2133
+ source: table["source"] ?? void 0,
2134
+ links: table["links"] ?? void 0,
2135
+ indexes: table["indexes"] ?? void 0,
2136
+ policies: canonicalPolicies(table["policies"]),
2137
+ actions: canonicalActions(table["actions"])
2138
+ };
2139
+ const parsed = tableFileSchema.safeParse(input);
2140
+ if (!parsed.success) {
2141
+ const issue = parsed.error.issues[0];
2142
+ throw new OrmDefinitionError(
2143
+ `stored definition for table ${String(table["table"])} is invalid: ${issue?.message ?? "unknown issue"}`,
2144
+ `canonical[${index2}]`
2145
+ );
2146
+ }
2147
+ return { file: `canonical[${index2}]`, table: parsed.data };
2148
+ });
2149
+ const config = namespaceConfigSchema.safeParse({
2150
+ namespace: canonical.namespace,
2151
+ description: canonical.description ?? void 0,
2152
+ capabilities: canonical.capabilities ?? void 0
2153
+ });
2154
+ if (!config.success) {
2155
+ const issue = config.error.issues[0];
2156
+ throw new OrmDefinitionError(`stored namespace config is invalid: ${issue?.message ?? "unknown issue"}`, "canonical");
2157
+ }
2158
+ return buildNamespaceDefinition(files, config.data);
2159
+ }
2160
+ function asCanonicalShape(value) {
2161
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
2162
+ throw new OrmDefinitionError("stored definition is not an object");
2163
+ }
2164
+ const record = value;
2165
+ if (typeof record["namespace"] !== "string" || !Array.isArray(record["tables"])) {
2166
+ throw new OrmDefinitionError("stored definition is missing namespace/tables");
2167
+ }
2168
+ return {
2169
+ namespace: record["namespace"],
2170
+ // Pre-capabilities rows lack both fields — rehydrate as unset.
2171
+ description: typeof record["description"] === "string" ? record["description"] : null,
2172
+ capabilities: record["capabilities"] ?? void 0,
2173
+ tables: record["tables"]
2174
+ };
2175
+ }
2176
+ function canonicalPolicies(value) {
2177
+ if (value === null || typeof value !== "object") return void 0;
2178
+ const record = value;
2179
+ const policies = {};
2180
+ if (typeof record["read"] === "string") policies["read"] = record["read"];
2181
+ if (typeof record["write"] === "string") policies["write"] = record["write"];
2182
+ return Object.keys(policies).length > 0 ? policies : void 0;
2183
+ }
2184
+ function canonicalActions(value) {
2185
+ if (value === null || typeof value !== "object") return void 0;
2186
+ const entries = Object.entries(value);
2187
+ if (entries.length === 0) return void 0;
2188
+ return Object.fromEntries(
2189
+ entries.map(([name, action]) => [
2190
+ name,
2191
+ {
2192
+ description: action["description"] ?? void 0,
2193
+ requires: action["requires"] ?? void 0,
2194
+ params: action["params"] ?? void 0,
2195
+ validates: action["validates"] ?? void 0,
2196
+ executes: action["executes"],
2197
+ returns: action["returns"] ?? void 0,
2198
+ audit: action["audit"] ?? void 0
2199
+ }
2200
+ ])
2201
+ );
2202
+ }
2203
+
2204
+ exports.ACTION_REQUIRES_DENIED_SQLSTATE = ACTION_REQUIRES_DENIED_SQLSTATE;
2205
+ exports.ACTION_VALIDATES_FAILED_SQLSTATE = ACTION_VALIDATES_FAILED_SQLSTATE;
2206
+ exports.AUDIT_ACTIONS = AUDIT_ACTIONS;
2207
+ exports.CAPABILITIES_CLAIM_KEY = CAPABILITIES_CLAIM_KEY;
2208
+ exports.CAPABILITY_KEY_PATTERN = CAPABILITY_KEY_PATTERN;
2209
+ exports.CLAIMS_SETTING = CLAIMS_SETTING;
2210
+ exports.CLAIM_KEY_PATTERN = CLAIM_KEY_PATTERN;
2211
+ exports.COLUMN_TYPES = COLUMN_TYPES;
2212
+ exports.COMPARE_OPS = COMPARE_OPS;
2213
+ exports.CONSUMER_ROLE_SUFFIX = CONSUMER_ROLE_SUFFIX;
2214
+ exports.DEFINITION_FILE_EXTENSIONS = DEFINITION_FILE_EXTENSIONS;
2215
+ exports.HASH_DOMAIN = HASH_DOMAIN;
2216
+ exports.IDENTIFIER_PATTERN = IDENTIFIER_PATTERN;
2217
+ exports.MAX_DEFINITION_FILE_BYTES = MAX_DEFINITION_FILE_BYTES;
2218
+ exports.MAX_IDENTIFIER_LENGTH = MAX_IDENTIFIER_LENGTH;
2219
+ exports.MIGRATIONS_DIRNAME = MIGRATIONS_DIRNAME;
2220
+ exports.NAMESPACE_CONFIG_FILENAMES = NAMESPACE_CONFIG_FILENAMES;
2221
+ exports.NAMESPACE_PATTERN = NAMESPACE_PATTERN;
2222
+ exports.ORM_DATA_TOKEN_HEADER = ORM_DATA_TOKEN_HEADER;
2223
+ exports.ORM_SCHEMA_PREFIX = ORM_SCHEMA_PREFIX;
2224
+ exports.ORM_SERVICE_DATA_TOKEN_HEADER = ORM_SERVICE_DATA_TOKEN_HEADER;
2225
+ exports.OrmDefinitionError = OrmDefinitionError;
2226
+ exports.PredicateParseError = PredicateParseError;
2227
+ exports.QUALIFIED_TABLE_PATTERN = QUALIFIED_TABLE_PATTERN;
2228
+ exports.RESERVED_COLUMN_NAMES = RESERVED_COLUMN_NAMES;
2229
+ exports.RESERVED_TABLE_NAMES = RESERVED_TABLE_NAMES;
2230
+ exports.STATEMENT_KINDS = STATEMENT_KINDS;
2231
+ exports.SYNCED_SOURCE_PATTERN = SYNCED_SOURCE_PATTERN;
2232
+ exports.actionFunctionName = actionFunctionName;
2233
+ exports.actionFunctionStatements = actionFunctionStatements;
2234
+ exports.actionParamArg = actionParamArg;
2235
+ exports.actionParamOrder = actionParamOrder;
2236
+ exports.actionParamSchema = actionParamSchema;
2237
+ exports.actionPrepareChecks = actionPrepareChecks;
2238
+ exports.actionSchema = actionSchema;
2239
+ exports.actionsInterfaceName = actionsInterfaceName;
2240
+ exports.applyMigrationHints = applyMigrationHints;
2241
+ exports.buildActionParamsSchema = buildActionParamsSchema;
2242
+ exports.buildDrizzleEntities = buildDrizzleEntities;
2243
+ exports.buildNamespaceDefinition = buildNamespaceDefinition;
2244
+ exports.canonicalizeDefinition = canonicalizeDefinition;
2245
+ exports.capabilityExpression = capabilityExpression;
2246
+ exports.capabilityPinsSchema = capabilityPinsSchema;
2247
+ exports.castParts = castParts;
2248
+ exports.checksumOf = checksumOf;
2249
+ exports.claimExpression = claimExpression;
2250
+ exports.classifyDestructive = classifyDestructive;
2251
+ exports.columnSchema = columnSchema;
2252
+ exports.columnTypeSchema = columnTypeSchema;
2253
+ exports.compileActionRequires = compileActionRequires;
2254
+ exports.compileActionStatements = compileActionStatements;
2255
+ exports.compileNamespace = compileNamespace;
2256
+ exports.compilePredicate = compilePredicate;
2257
+ exports.computeDefinitionHash = computeDefinitionHash;
2258
+ exports.consumerRoleName = consumerRoleName;
2259
+ exports.customRolesBlockSchema = customRolesBlockSchema;
2260
+ exports.customRolesDeclared = customRolesDeclared;
2261
+ exports.declaredCapabilityKeys = declaredCapabilityKeys;
2262
+ exports.diffCommand = diffCommand;
2263
+ exports.diffStatements = diffStatements;
2264
+ exports.emptyJournal = emptyJournal;
2265
+ exports.emptySnapshot = emptySnapshot;
2266
+ exports.extractNamedParams = extractNamedParams;
2267
+ exports.generateNamespaceTypes = generateNamespaceTypes;
2268
+ exports.generateSnapshot = generateSnapshot;
2269
+ exports.identifierSchema = identifierSchema;
2270
+ exports.isFunctionDefault = isFunctionDefault;
2271
+ exports.isOwnedStatement = isOwnedStatement;
2272
+ exports.isQualifiedReference = isQualifiedReference;
2273
+ exports.loadCommittedMigrations = loadCommittedMigrations;
2274
+ exports.loadCommittedTipMigration = loadCommittedTipMigration;
2275
+ exports.loadNamespaceDefinition = loadNamespaceDefinition;
2276
+ exports.migrationName = migrationName;
2277
+ exports.namespaceConfigSchema = namespaceConfigSchema;
2278
+ exports.namespaceSchema = namespaceSchema;
2279
+ exports.normalizeSnapshot = normalizeSnapshot;
2280
+ exports.ormSchemaName = ormSchemaName;
2281
+ exports.parseCanonicalDefinition = parseCanonicalDefinition;
2282
+ exports.parseCapabilityPin = parseCapabilityPin;
2283
+ exports.parseDefinitionSources = parseDefinitionSources;
2284
+ exports.parsePredicate = parsePredicate;
2285
+ exports.parseTableFile = parseTableFile;
2286
+ exports.parsedPredicateSchema = parsedPredicateSchema;
2287
+ exports.pendingKitStatements = pendingKitStatements;
2288
+ exports.planMigration = planMigration;
2289
+ exports.predicateOperands = predicateOperands;
2290
+ exports.quoteIdentifier = quoteIdentifier;
2291
+ exports.quoteLiteral = quoteLiteral;
2292
+ exports.readMigrationSql = readMigrationSql;
2293
+ exports.readMigrationWorkspace = readMigrationWorkspace;
2294
+ exports.readSnapshot = readSnapshot;
2295
+ exports.referencedCapabilities = referencedCapabilities;
2296
+ exports.referencedColumns = referencedColumns;
2297
+ exports.renderMigrationSql = renderMigrationSql;
2298
+ exports.securityStatements = securityStatements;
2299
+ exports.sqlTypeName = sqlTypeName;
2300
+ exports.stableJson = stableJson;
2301
+ exports.tableFileSchema = tableFileSchema;
2302
+ exports.tableReferenceSchema = tableReferenceSchema;
2303
+ exports.typesInterfaceName = typesInterfaceName;
2304
+ exports.validateCapabilityPins = validateCapabilityPins;
2305
+ exports.walkPredicate = walkPredicate;
2306
+ exports.writeMigration = writeMigration;
2307
+ //# sourceMappingURL=index.cjs.map
2308
+ //# sourceMappingURL=index.cjs.map