@sequenceholdings/orm 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +11 -1
  2. package/dist/authoring.cjs +1066 -59
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +289 -24
  5. package/dist/authoring.d.ts +289 -24
  6. package/dist/authoring.js +1056 -60
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +415 -9
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +235 -1
  11. package/dist/canonical.d.ts +235 -1
  12. package/dist/canonical.js +411 -10
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +1764 -225
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +1765 -226
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs.map +1 -1
  19. package/dist/client.d.cts +1 -1
  20. package/dist/client.d.ts +1 -1
  21. package/dist/client.js.map +1 -1
  22. package/dist/compiler.cjs +506 -54
  23. package/dist/compiler.cjs.map +1 -1
  24. package/dist/compiler.d.cts +39 -231
  25. package/dist/compiler.d.ts +39 -231
  26. package/dist/compiler.js +504 -55
  27. package/dist/compiler.js.map +1 -1
  28. package/dist/drizzle.cjs +14 -0
  29. package/dist/drizzle.cjs.map +1 -0
  30. package/dist/drizzle.d.cts +1 -0
  31. package/dist/drizzle.d.ts +1 -0
  32. package/dist/drizzle.js +3 -0
  33. package/dist/drizzle.js.map +1 -0
  34. package/dist/dynamic-input.cjs +391 -0
  35. package/dist/dynamic-input.cjs.map +1 -0
  36. package/dist/dynamic-input.d.cts +111 -0
  37. package/dist/dynamic-input.d.ts +111 -0
  38. package/dist/dynamic-input.js +380 -0
  39. package/dist/dynamic-input.js.map +1 -0
  40. package/dist/index.cjs +2223 -219
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +125 -9
  43. package/dist/index.d.ts +125 -9
  44. package/dist/index.js +2199 -220
  45. package/dist/index.js.map +1 -1
  46. package/dist/ir.cjs +249 -14
  47. package/dist/ir.cjs.map +1 -1
  48. package/dist/ir.d.cts +20 -80
  49. package/dist/ir.d.ts +20 -80
  50. package/dist/ir.js +249 -14
  51. package/dist/ir.js.map +1 -1
  52. package/dist/predicate-v2.cjs +1093 -0
  53. package/dist/predicate-v2.cjs.map +1 -0
  54. package/dist/predicate-v2.d.cts +139 -0
  55. package/dist/predicate-v2.d.ts +139 -0
  56. package/dist/predicate-v2.js +1083 -0
  57. package/dist/predicate-v2.js.map +1 -0
  58. package/dist/sdl.cjs +344 -55
  59. package/dist/sdl.cjs.map +1 -1
  60. package/dist/sdl.d.cts +1 -3
  61. package/dist/sdl.d.ts +1 -3
  62. package/dist/sdl.js +344 -55
  63. package/dist/sdl.js.map +1 -1
  64. package/package.json +16 -1
  65. package/templates/orm-namespace/CLAUDE.md +6 -5
@@ -0,0 +1,1093 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+
5
+ // src/predicate-v2/ast.ts
6
+ var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
7
+ function walkPolicy(node, visit) {
8
+ visit(node);
9
+ if (node.kind === "and" || node.kind === "or") {
10
+ for (const operand of node.operands) walkPolicy(operand, visit);
11
+ }
12
+ if (node.kind === "not") walkPolicy(node.operand, visit);
13
+ if (node.kind === "exists") walkPolicy(node.where, visit);
14
+ }
15
+
16
+ // src/predicate-v2/errors.ts
17
+ var PolicyParseError = class extends Error {
18
+ constructor(message, position) {
19
+ super(`${message} (at position ${position})`);
20
+ this.position = position;
21
+ }
22
+ name = "PolicyParseError";
23
+ };
24
+ var PolicyCompileError = class extends Error {
25
+ constructor(message, code, candidates = []) {
26
+ super(candidates.length > 0 ? `${message} (candidates: ${candidates.join(", ")})` : message);
27
+ this.code = code;
28
+ this.candidates = candidates;
29
+ }
30
+ name = "PolicyCompileError";
31
+ };
32
+
33
+ // src/constants.ts
34
+ var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
35
+ var CANONICAL_FORMAT = 2;
36
+ var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
37
+ var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
38
+ var COLUMN_TYPES = [
39
+ "uuid",
40
+ "text",
41
+ "integer",
42
+ "bigint",
43
+ "numeric",
44
+ "double",
45
+ "boolean",
46
+ "timestamptz",
47
+ "date",
48
+ "jsonb"
49
+ ];
50
+ var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
51
+ var canonicalEnumSchema = zod.z.object({
52
+ name: identifier,
53
+ values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
54
+ });
55
+ var canonicalColumnSchema = zod.z.object({
56
+ /** Property name in TS (camelCase); columnName is the Postgres name. */
57
+ name: zod.z.string().min(1).max(63),
58
+ columnName: identifier,
59
+ type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
60
+ nullable: zod.z.boolean().default(true),
61
+ unique: zod.z.boolean().default(false),
62
+ /** Literal default, or one of the allowlisted SQL functions. */
63
+ default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
64
+ defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
65
+ description: zod.z.string().max(500).optional(),
66
+ /** Migration hints — same semantics as v1. */
67
+ renamedFrom: identifier.optional(),
68
+ cast: zod.z.string().max(200).optional(),
69
+ deprecated: zod.z.boolean().default(false)
70
+ });
71
+ var canonicalLinkSchema = zod.z.object({
72
+ name: zod.z.string().min(1).max(63),
73
+ kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
74
+ /** Target table name (namespace-local). Absent for toPlatformUser. */
75
+ target: identifier.optional(),
76
+ /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
77
+ on: identifier,
78
+ /** Referenced column on the target (default: target's PK / platform user `sub`). */
79
+ references: identifier.optional(),
80
+ /** enforce: true emits a real FK constraint (same-namespace toOne only). */
81
+ enforce: zod.z.boolean().default(false),
82
+ description: zod.z.string().max(500).optional()
83
+ });
84
+ var canonicalIndexSchema = zod.z.object({
85
+ columns: zod.z.array(identifier).min(1),
86
+ unique: zod.z.boolean().default(false),
87
+ name: identifier.optional()
88
+ });
89
+ var policyVariantSchema = zod.z.object({
90
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
91
+ when: zod.z.string().min(1).max(1e3),
92
+ /** Exactly one of: allow-all bypass, or a row predicate. */
93
+ allow: zod.z.literal(true).optional(),
94
+ policy: zod.z.string().min(1).max(2e3).optional()
95
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
96
+ message: "A variant declares exactly one of allow: true or policy"
97
+ });
98
+ var policyExpressionSchema = zod.z.union([
99
+ zod.z.string().min(1).max(2e3),
100
+ zod.z.object({
101
+ default: zod.z.string().min(1).max(2e3),
102
+ variants: zod.z.array(policyVariantSchema).max(10).default([])
103
+ })
104
+ ]);
105
+ var canonicalPoliciesSchema = zod.z.object({
106
+ read: policyExpressionSchema.optional(),
107
+ write: policyExpressionSchema.optional(),
108
+ insert: policyExpressionSchema.optional(),
109
+ update: policyExpressionSchema.optional()
110
+ });
111
+ function isSingleViewQuerySql(value) {
112
+ const query = value.trim();
113
+ if (!/^\(*(?:select|with)\b/i.test(query) || query.includes("\0")) return false;
114
+ let state = "normal";
115
+ for (let index = 0; index < query.length; index += 1) {
116
+ const char = query[index];
117
+ const next = query[index + 1];
118
+ if (state === "single") {
119
+ if (char === "'" && next === "'") index += 1;
120
+ else if (char === "'") state = "normal";
121
+ continue;
122
+ }
123
+ if (state === "double") {
124
+ if (char === '"' && next === '"') index += 1;
125
+ else if (char === '"') state = "normal";
126
+ continue;
127
+ }
128
+ if (char === "'") state = "single";
129
+ else if (char === '"') state = "double";
130
+ else if (char === ";" || char === "-" && next === "-" || char === "/" && next === "*") return false;
131
+ }
132
+ return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
133
+ }
134
+ var canonicalActionSchema = zod.z.object({
135
+ name: identifier,
136
+ requires: zod.z.string().max(1e3).optional(),
137
+ params: zod.z.record(zod.z.string(), zod.z.object({
138
+ type: columnTypeSchema,
139
+ nullable: zod.z.boolean().default(false)
140
+ })).default({}),
141
+ executes: zod.z.string().min(1).max(1e4),
142
+ returns: zod.z.enum(["rows", "rowsAffected", "scalar"]).default("rowsAffected"),
143
+ audit: zod.z.object({ action: zod.z.string(), resource: zod.z.string() }).optional()
144
+ });
145
+ var canonicalCowMutationSchema = zod.z.object({
146
+ kind: zod.z.enum(["override", "revert"]),
147
+ name: identifier,
148
+ /** Synced table (same namespace) the overlay copies from / falls back to. */
149
+ synced: identifier,
150
+ /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
151
+ pk: zod.z.string().min(1).max(63),
152
+ /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
153
+ copy: zod.z.array(zod.z.string().min(1).max(63)).default([]),
154
+ /** Override: overlay properties written by the UPDATE (never the pk). Empty for revert. */
155
+ set: zod.z.array(zod.z.string().min(1).max(63)).default([])
156
+ }).superRefine((mutation, ctx) => {
157
+ if (mutation.kind === "override") {
158
+ if (mutation.set.length === 0) {
159
+ ctx.addIssue({ code: "custom", path: ["set"], message: "override mutations must set at least one column" });
160
+ }
161
+ if (!mutation.copy.includes(mutation.pk)) {
162
+ ctx.addIssue({ code: "custom", path: ["copy"], message: "override copy list must include the pk" });
163
+ }
164
+ } else if (mutation.set.length > 0 || mutation.copy.length > 0) {
165
+ ctx.addIssue({ code: "custom", path: ["set"], message: "revert mutations take no copy/set columns" });
166
+ }
167
+ if (mutation.set.includes(mutation.pk)) {
168
+ ctx.addIssue({ code: "custom", path: ["set"], message: "set must not include the pk \u2014 identity columns are pinned" });
169
+ }
170
+ for (const [field, values] of [["copy", mutation.copy], ["set", mutation.set]]) {
171
+ if (new Set(values).size !== values.length) {
172
+ ctx.addIssue({ code: "custom", path: [field], message: `${field} must not contain duplicate columns` });
173
+ }
174
+ }
175
+ });
176
+ var canonicalTableSchema = zod.z.object({
177
+ table: identifier,
178
+ description: zod.z.string().max(2e3).optional(),
179
+ /** Table-level rename hint (new in v2). */
180
+ renamedFrom: identifier.optional(),
181
+ /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
182
+ source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
183
+ columns: zod.z.array(canonicalColumnSchema).default([]),
184
+ primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
185
+ links: zod.z.array(canonicalLinkSchema).default([]),
186
+ indexes: zod.z.array(canonicalIndexSchema).default([]),
187
+ policies: canonicalPoliciesSchema.default({}),
188
+ /**
189
+ * v1-conversion carry-over only. v2 has no action surface — the runtime
190
+ * neither compiles nor executes these; writes are mutations. Kept parseable
191
+ * so stored definitions and their content hashes stay stable.
192
+ */
193
+ actions: zod.z.array(canonicalActionSchema).default([]),
194
+ /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
195
+ cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
196
+ });
197
+ var canonicalViewSchema = zod.z.object({
198
+ view: identifier,
199
+ description: zod.z.string().max(2e3).optional(),
200
+ columns: zod.z.array(canonicalColumnSchema).min(1),
201
+ /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
202
+ primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
203
+ links: zod.z.array(canonicalLinkSchema).default([]),
204
+ policies: zod.z.object({ read: policyExpressionSchema }).strict(),
205
+ query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
206
+ message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
207
+ }),
208
+ /** Namespace-local logical relation names, sorted for deterministic hashing. */
209
+ dependencies: zod.z.array(identifier).max(500)
210
+ }).superRefine((view, ctx) => {
211
+ for (const [index, column] of view.columns.entries()) {
212
+ if (column.default !== void 0 || column.defaultFn !== void 0) {
213
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
214
+ }
215
+ if (column.unique) {
216
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
217
+ }
218
+ if (column.renamedFrom !== void 0 || column.cast !== void 0) {
219
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
220
+ }
221
+ }
222
+ for (const [index, link] of view.links.entries()) {
223
+ if (link.enforce) {
224
+ ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
225
+ }
226
+ }
227
+ });
228
+ zod.z.object({
229
+ definition_format: zod.z.literal(CANONICAL_FORMAT),
230
+ namespace: identifier.max(41),
231
+ description: zod.z.string().max(2e3).optional(),
232
+ enums: zod.z.array(canonicalEnumSchema).default([]),
233
+ tables: zod.z.array(canonicalTableSchema).min(0).max(500),
234
+ /** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
235
+ views: zod.z.array(canonicalViewSchema).max(500).optional(),
236
+ /** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
237
+ capabilities: zod.z.object({
238
+ roles: zod.z.unknown().optional(),
239
+ uses: zod.z.array(zod.z.string()).default([])
240
+ }).default({ uses: [] })
241
+ }).superRefine((definition, ctx) => {
242
+ const seen = /* @__PURE__ */ new Set();
243
+ for (const [index, table] of definition.tables.entries()) {
244
+ if (seen.has(table.table)) {
245
+ ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
246
+ }
247
+ seen.add(table.table);
248
+ }
249
+ const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
250
+ for (const [tableIndex, table] of definition.tables.entries()) {
251
+ const cowMutations = table.cowMutations ?? [];
252
+ if (cowMutations.length === 0) continue;
253
+ const path = (index, ...rest) => ["tables", tableIndex, "cowMutations", index, ...rest];
254
+ if (table.source !== void 0) {
255
+ ctx.addIssue({ code: "custom", path: path(0), message: `synced table '${table.table}' cannot declare cow mutations \u2014 it is read-only` });
256
+ continue;
257
+ }
258
+ for (const [index, mutation] of cowMutations.entries()) {
259
+ const synced = tableByName.get(mutation.synced);
260
+ if (!synced || synced.source === void 0) {
261
+ ctx.addIssue({
262
+ code: "custom",
263
+ path: path(index, "synced"),
264
+ message: `cow mutation '${mutation.name}' references '${mutation.synced}', which is not a synced table in this namespace`
265
+ });
266
+ continue;
267
+ }
268
+ const overlayColumnsByName = new Map(table.columns.map((column) => [column.name, column]));
269
+ const syncedColumnsByName = new Map(synced.columns.map((column) => [column.name, column]));
270
+ let columnsExist = true;
271
+ for (const property of [mutation.pk, ...mutation.copy, ...mutation.set]) {
272
+ if (!overlayColumnsByName.has(property)) {
273
+ columnsExist = false;
274
+ ctx.addIssue({ code: "custom", path: path(index), message: `cow mutation '${mutation.name}': column '${property}' does not exist on overlay '${table.table}'` });
275
+ }
276
+ }
277
+ for (const property of [mutation.pk, ...mutation.copy]) {
278
+ if (!syncedColumnsByName.has(property)) {
279
+ columnsExist = false;
280
+ ctx.addIssue({ code: "custom", path: path(index), message: `cow mutation '${mutation.name}': column '${property}' does not exist on synced '${mutation.synced}'` });
281
+ }
282
+ }
283
+ if (!columnsExist) continue;
284
+ const overlayPk = table.primaryKey ?? [];
285
+ const syncedPk = synced.primaryKey ?? [];
286
+ if (overlayPk.length !== 1 || overlayPk[0] !== mutation.pk || syncedPk.length !== 1 || syncedPk[0] !== mutation.pk) {
287
+ ctx.addIssue({
288
+ code: "custom",
289
+ path: path(index, "pk"),
290
+ message: `cow mutation '${mutation.name}': pk '${mutation.pk}' must be the single-column primary key on both '${table.table}' and '${mutation.synced}'`
291
+ });
292
+ }
293
+ for (const property of [mutation.pk, ...mutation.copy]) {
294
+ const overlayColumn = overlayColumnsByName.get(property);
295
+ const syncedColumn = syncedColumnsByName.get(property);
296
+ if (!overlayColumn || !syncedColumn) continue;
297
+ if (JSON.stringify(overlayColumn.type) !== JSON.stringify(syncedColumn.type)) {
298
+ ctx.addIssue({
299
+ code: "custom",
300
+ path: path(index),
301
+ message: `cow mutation '${mutation.name}': column '${property}' must have the same type on overlay and synced`
302
+ });
303
+ }
304
+ if (overlayColumn.columnName !== syncedColumn.columnName) {
305
+ ctx.addIssue({
306
+ code: "custom",
307
+ path: path(index),
308
+ message: `cow mutation '${mutation.name}': column '${property}' maps to different Postgres names on overlay ('${overlayColumn.columnName}') and synced ('${syncedColumn.columnName}')`
309
+ });
310
+ }
311
+ if (syncedColumn.nullable !== false && overlayColumn.nullable === false) {
312
+ ctx.addIssue({
313
+ code: "custom",
314
+ path: path(index),
315
+ message: `cow mutation '${mutation.name}': column '${property}' is nullable on synced but NOT NULL on overlay \u2014 a NULL synced value would fail the copy`
316
+ });
317
+ }
318
+ }
319
+ }
320
+ }
321
+ for (const [index, view] of (definition.views ?? []).entries()) {
322
+ if (seen.has(view.view)) {
323
+ ctx.addIssue({ code: "custom", path: ["views", index, "view"], message: `duplicate relation '${view.view}'` });
324
+ }
325
+ seen.add(view.view);
326
+ }
327
+ const views = definition.views ?? [];
328
+ const viewByName = new Map(views.map((view) => [view.view, view]));
329
+ for (const [viewIndex, view] of views.entries()) {
330
+ for (const [dependencyIndex, dependency] of view.dependencies.entries()) {
331
+ if (!seen.has(dependency)) {
332
+ ctx.addIssue({
333
+ code: "custom",
334
+ path: ["views", viewIndex, "dependencies", dependencyIndex],
335
+ message: `view '${view.view}' depends on unknown relation '${dependency}'`
336
+ });
337
+ }
338
+ }
339
+ }
340
+ const visiting = /* @__PURE__ */ new Set();
341
+ const visited = /* @__PURE__ */ new Set();
342
+ let cycleReported = false;
343
+ const visit = (name, path) => {
344
+ if (visited.has(name) || cycleReported) return;
345
+ if (visiting.has(name)) {
346
+ cycleReported = true;
347
+ ctx.addIssue({
348
+ code: "custom",
349
+ path: ["views"],
350
+ message: `view dependencies form a cycle: ${[...path, name].join(" -> ")}`
351
+ });
352
+ return;
353
+ }
354
+ visiting.add(name);
355
+ const view = viewByName.get(name);
356
+ for (const dependency of view?.dependencies ?? []) {
357
+ if (viewByName.has(dependency)) visit(dependency, [...path, name]);
358
+ }
359
+ visiting.delete(name);
360
+ visited.add(name);
361
+ };
362
+ for (const view of views) visit(view.view, []);
363
+ });
364
+
365
+ // src/ir/index.ts
366
+ var IR_VERSION = 1;
367
+ var CLAIMS_CONTRACT = {
368
+ sub: "string",
369
+ org: "string?",
370
+ roles: "string[]",
371
+ capabilities: "string[]"
372
+ };
373
+ var scalarLiteral = zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]);
374
+ var valueIrSchema = zod.z.discriminatedUnion("kind", [
375
+ zod.z.object({ kind: zod.z.literal("literal"), value: zod.z.union([scalarLiteral, zod.z.array(scalarLiteral)]) }),
376
+ zod.z.object({ kind: zod.z.literal("variable"), name: zod.z.string() }),
377
+ zod.z.object({ kind: zod.z.literal("claim"), key: zod.z.enum(["sub", "org"]) })
378
+ ]);
379
+ var CMP_OPS = [
380
+ "eq",
381
+ "neq",
382
+ "gt",
383
+ "gte",
384
+ "lt",
385
+ "lte",
386
+ "in",
387
+ "nin",
388
+ "like",
389
+ "ilike",
390
+ "isNull",
391
+ "contains",
392
+ "hasKey"
393
+ ];
394
+ var predIrSchema = zod.z.lazy(
395
+ () => zod.z.discriminatedUnion("kind", [
396
+ zod.z.object({ kind: zod.z.literal("and"), operands: zod.z.array(predIrSchema).min(1) }),
397
+ zod.z.object({ kind: zod.z.literal("or"), operands: zod.z.array(predIrSchema).min(1) }),
398
+ zod.z.object({ kind: zod.z.literal("not"), operand: predIrSchema }),
399
+ zod.z.object({ kind: zod.z.literal("cmp"), op: zod.z.enum(CMP_OPS), column: zod.z.string(), value: valueIrSchema }),
400
+ zod.z.object({ kind: zod.z.literal("exists"), link: zod.z.string(), where: predIrSchema }),
401
+ zod.z.object({
402
+ kind: zod.z.literal("aggPredicate"),
403
+ link: zod.z.string(),
404
+ fn: zod.z.literal("count"),
405
+ op: zod.z.enum(["eq", "neq", "gt", "gte", "lt", "lte"]),
406
+ value: zod.z.number(),
407
+ where: predIrSchema.optional()
408
+ }),
409
+ zod.z.object({ kind: zod.z.literal("capability"), key: zod.z.string() }),
410
+ zod.z.object({ kind: zod.z.literal("role"), key: zod.z.string() }),
411
+ zod.z.object({ kind: zod.z.literal("dynamicFilter"), slot: zod.z.string() }),
412
+ zod.z.object({ kind: zod.z.literal("const"), value: zod.z.boolean() })
413
+ ])
414
+ );
415
+ var orderIrSchema = zod.z.discriminatedUnion("kind", [
416
+ zod.z.object({
417
+ kind: zod.z.literal("column"),
418
+ column: zod.z.string(),
419
+ /** Order through a to-one link: `link` is set and `column` is on the target. */
420
+ link: zod.z.string().optional(),
421
+ direction: zod.z.enum(["asc", "desc"]),
422
+ nulls: zod.z.enum(["first", "last"]).optional()
423
+ }),
424
+ /** §4f: `$orderBy: [FooOrderBy!]` variable — validated + lowered at runtime. */
425
+ zod.z.object({ kind: zod.z.literal("dynamic"), slot: zod.z.string() })
426
+ ]);
427
+ var limitIrSchema = zod.z.object({
428
+ /** Fixed value or a variable, clamped to [1, max]; absent → default. */
429
+ variable: zod.z.string().optional(),
430
+ value: zod.z.number().int().positive().optional(),
431
+ default: zod.z.number().int().positive(),
432
+ max: zod.z.number().int().positive()
433
+ });
434
+ var selectionIrSchema = zod.z.lazy(
435
+ () => zod.z.object({
436
+ typename: zod.z.string(),
437
+ columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
438
+ relations: zod.z.array(zod.z.object({
439
+ alias: zod.z.string(),
440
+ link: zod.z.string(),
441
+ selection: selectionIrSchema,
442
+ where: predIrSchema.optional(),
443
+ orderBy: zod.z.array(orderIrSchema).optional(),
444
+ limit: limitIrSchema.optional(),
445
+ offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional()
446
+ })),
447
+ aggregates: zod.z.array(zod.z.object({
448
+ alias: zod.z.string(),
449
+ link: zod.z.string().optional(),
450
+ where: predIrSchema.optional(),
451
+ count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
452
+ sum: zod.z.array(zod.z.string()).optional(),
453
+ avg: zod.z.array(zod.z.string()).optional(),
454
+ nodes: selectionIrSchema.optional()
455
+ }))
456
+ })
457
+ );
458
+ var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
459
+ var selectRootSchema = zod.z.object({
460
+ kind: zod.z.literal("select"),
461
+ alias: zod.z.string(),
462
+ table: zod.z.string(),
463
+ selection: selectionIrSchema,
464
+ /** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
465
+ where: predIrSchema,
466
+ orderBy: zod.z.array(orderIrSchema).default([]),
467
+ distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
468
+ limit: limitIrSchema,
469
+ offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
470
+ includeIf: includeIfSchema.optional()
471
+ });
472
+ var byPkRootSchema = zod.z.object({
473
+ kind: zod.z.literal("byPk"),
474
+ alias: zod.z.string(),
475
+ table: zod.z.string(),
476
+ selection: selectionIrSchema,
477
+ pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
478
+ /** Policy predicate — ANDed with the PK match. */
479
+ where: predIrSchema,
480
+ includeIf: includeIfSchema.optional()
481
+ });
482
+ var aggregateRootSchema = zod.z.object({
483
+ kind: zod.z.literal("aggregate"),
484
+ alias: zod.z.string(),
485
+ table: zod.z.string(),
486
+ where: predIrSchema,
487
+ count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
488
+ sum: zod.z.array(zod.z.string()).optional(),
489
+ avg: zod.z.array(zod.z.string()).optional(),
490
+ nodes: selectionIrSchema.optional(),
491
+ includeIf: includeIfSchema.optional()
492
+ });
493
+ var writeValuesSchema = zod.z.union([
494
+ zod.z.object({ slot: zod.z.string() }),
495
+ zod.z.object({ entries: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })) })
496
+ ]);
497
+ var onConflictIrSchema = zod.z.union([
498
+ zod.z.object({ slot: zod.z.string() }),
499
+ zod.z.object({ constraint: zod.z.string(), updateColumns: zod.z.array(zod.z.string()) })
500
+ ]);
501
+ var mutationRootSchema = zod.z.object({
502
+ kind: zod.z.literal("mutation"),
503
+ /**
504
+ * CRUD verbs plus the structured copy-on-write ops. `cowOverride` copies the
505
+ * synced row into the overlay (INSERT…SELECT, ON CONFLICT DO NOTHING) then
506
+ * applies `set` — both statements generated by the executor from this
507
+ * metadata, never authored SQL. `cowRevert` deletes the override so the
508
+ * coalesce view falls back to synced. Both are gated on a matching synced
509
+ * row and carry the write policy in `where` like update/delete.
510
+ */
511
+ op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
512
+ alias: zod.z.string(),
513
+ table: zod.z.string(),
514
+ /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
515
+ cowSynced: zod.z.string().optional(),
516
+ /** cowOverride: logical properties copied INSERT…SELECT from synced (pk included). */
517
+ cowCopyColumns: zod.z.array(zod.z.string()).optional(),
518
+ /** cowOverride: insert-policy WITH CHECK evaluated over the copied rows. */
519
+ cowCopyCheck: predIrSchema.optional(),
520
+ /** insert/insertOne: rows come from a slot ($object/$objects) or literals. */
521
+ objects: zod.z.union([
522
+ zod.z.object({ slot: zod.z.string() }),
523
+ zod.z.object({ rows: zod.z.array(zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema }))) })
524
+ ]).optional(),
525
+ /** Server-pinned columns (e.g. author_sub ← claim('sub')) merged into every inserted row. */
526
+ pinnedColumns: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).default([]),
527
+ onConflict: onConflictIrSchema.optional(),
528
+ set: writeValuesSchema.optional(),
529
+ append: writeValuesSchema.optional(),
530
+ inc: writeValuesSchema.optional(),
531
+ /** update/delete row filter: AND(writePolicy, documentWhere/pk, dynamicFilter?). */
532
+ where: predIrSchema.optional(),
533
+ pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).optional(),
534
+ /** WITH CHECK analogue evaluated against NEW row values (insert/update). */
535
+ insertCheck: predIrSchema.optional(),
536
+ returning: selectionIrSchema.nullable(),
537
+ affectedRows: zod.z.boolean().default(false),
538
+ includeIf: includeIfSchema.optional()
539
+ });
540
+ var rootIrSchema = zod.z.discriminatedUnion("kind", [
541
+ selectRootSchema,
542
+ byPkRootSchema,
543
+ aggregateRootSchema,
544
+ mutationRootSchema
545
+ ]);
546
+ var INPUT_REF_KINDS = [
547
+ "boolExp",
548
+ "orderBy",
549
+ "selectColumn",
550
+ "insertInput",
551
+ "setInput",
552
+ "appendInput",
553
+ "onConflict"
554
+ ];
555
+ var variableDefaultIrSchema = zod.z.object({
556
+ value: zod.z.union([scalarLiteral, zod.z.array(scalarLiteral)])
557
+ });
558
+ var variableIrSchema = zod.z.object({
559
+ name: zod.z.string(),
560
+ type: zod.z.union([
561
+ zod.z.object({ kind: zod.z.literal("scalar"), scalar: zod.z.enum(COLUMN_TYPES) }),
562
+ /** `values` is the declared membership whitelist — the executor validates against it. */
563
+ zod.z.object({ kind: zod.z.literal("enum"), name: zod.z.string(), values: zod.z.array(zod.z.string()).min(1) }),
564
+ /** §4f dynamic slot: validated against `tables[table]` whitelists at runtime. */
565
+ zod.z.object({ kind: zod.z.literal("inputRef"), input: zod.z.enum(INPUT_REF_KINDS), table: zod.z.string() })
566
+ ]),
567
+ nullable: zod.z.boolean(),
568
+ list: zod.z.boolean(),
569
+ /** Absent = no default declared; see {@link variableDefaultIrSchema}. */
570
+ defaultValue: variableDefaultIrSchema.optional()
571
+ });
572
+ var compiledOperationSchema = zod.z.object({
573
+ irVersion: zod.z.literal(IR_VERSION),
574
+ name: zod.z.string(),
575
+ opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
576
+ kind: zod.z.enum(["query", "mutation"]),
577
+ requires: zod.z.object({
578
+ capability: zod.z.string().optional(),
579
+ role: zod.z.string().optional()
580
+ }).nullable(),
581
+ variables: zod.z.array(variableIrSchema),
582
+ /** Every table the op touches — data-token scoping + audit. */
583
+ tableSet: zod.z.array(zod.z.string()).min(1),
584
+ roots: zod.z.array(rootIrSchema).min(1)
585
+ });
586
+ var tableMetaIrSchema = zod.z.object({
587
+ table: zod.z.string(),
588
+ schema: zod.z.string(),
589
+ typename: zod.z.string(),
590
+ /**
591
+ * True when statements over this relation evaluate a claims-gated synced
592
+ * view: synced twins (`source: gold.*`) materialize as views whose baked
593
+ * policy reads `current_setting('request.jwt.claims')`, and namespace views
594
+ * inherit the gate transitively through their declared dependencies. The
595
+ * executor must stamp that GUC (transaction-scoped) before any statement
596
+ * touching such a relation, or the synced arm yields zero rows regardless
597
+ * of the compiled predicates.
598
+ */
599
+ claimsGated: zod.z.boolean().default(false),
600
+ columns: zod.z.array(zod.z.object({
601
+ name: zod.z.string(),
602
+ columnName: zod.z.string(),
603
+ type: zod.z.union([zod.z.enum(COLUMN_TYPES), zod.z.object({ enum: zod.z.string(), values: zod.z.array(zod.z.string()) })]),
604
+ nullable: zod.z.boolean()
605
+ })),
606
+ /** Empty for keyless read-only views; keyed tables and views list logical columns. */
607
+ primaryKey: zod.z.array(zod.z.string()),
608
+ links: zod.z.array(zod.z.object({
609
+ name: zod.z.string(),
610
+ kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
611
+ target: zod.z.string(),
612
+ on: zod.z.string(),
613
+ references: zod.z.string()
614
+ })),
615
+ /** §4f whitelists: what dynamic inputs may reference. */
616
+ filterable: zod.z.array(zod.z.string()),
617
+ orderable: zod.z.array(zod.z.string()),
618
+ insertable: zod.z.array(zod.z.string()),
619
+ settable: zod.z.array(zod.z.string()),
620
+ appendable: zod.z.array(zod.z.string()),
621
+ constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
622
+ /** Depth/node caps for dynamic filter trees on this table. */
623
+ dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
624
+ dynamicFilterMaxNodes: zod.z.number().int().positive().default(64)
625
+ });
626
+ zod.z.object({
627
+ irVersion: zod.z.literal(IR_VERSION),
628
+ namespace: zod.z.string(),
629
+ schemaHash: zod.z.string(),
630
+ opsHash: zod.z.string(),
631
+ tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
632
+ operations: zod.z.record(zod.z.string(), compiledOperationSchema)
633
+ });
634
+
635
+ // src/predicate-v2/parse.ts
636
+ var KEYWORDS = /* @__PURE__ */ new Set(["and", "or", "not", "true", "false", "null", "claim", "capability", "role", "exists"]);
637
+ var OPERATOR_PATTERN = /^(!=|<=|>=|<>|=|<|>)/;
638
+ var IDENTIFIER_START = /[a-zA-Z_]/;
639
+ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
640
+ var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
641
+ var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
642
+ var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
643
+ function tokenize(source) {
644
+ const tokens = [];
645
+ let position = 0;
646
+ while (position < source.length) {
647
+ const char = source[position];
648
+ if (/\s/.test(char)) {
649
+ position += 1;
650
+ continue;
651
+ }
652
+ if (char === "(") {
653
+ tokens.push({ kind: "lparen", value: char, position });
654
+ position += 1;
655
+ continue;
656
+ }
657
+ if (char === ")") {
658
+ tokens.push({ kind: "rparen", value: char, position });
659
+ position += 1;
660
+ continue;
661
+ }
662
+ if (char === ",") {
663
+ tokens.push({ kind: "comma", value: char, position });
664
+ position += 1;
665
+ continue;
666
+ }
667
+ if (char === ".") {
668
+ tokens.push({ kind: "dot", value: char, position });
669
+ position += 1;
670
+ continue;
671
+ }
672
+ if (char === "'") {
673
+ const { value, end } = readString(source, position);
674
+ tokens.push({ kind: "string", value, position });
675
+ position = end;
676
+ continue;
677
+ }
678
+ const operator = OPERATOR_PATTERN.exec(source.slice(position));
679
+ if (operator) {
680
+ const value = operator[0] === "<>" ? "!=" : operator[0];
681
+ tokens.push({ kind: "operator", value, position });
682
+ position += operator[0].length;
683
+ continue;
684
+ }
685
+ const number = NUMBER_PATTERN.exec(source.slice(position));
686
+ if (number && !IDENTIFIER_START.test(char)) {
687
+ tokens.push({ kind: "number", value: number[0], position });
688
+ position += number[0].length;
689
+ continue;
690
+ }
691
+ if (IDENTIFIER_START.test(char)) {
692
+ let end = position;
693
+ while (end < source.length && IDENTIFIER_CHAR.test(source[end])) end += 1;
694
+ tokens.push({ kind: "identifier", value: source.slice(position, end), position });
695
+ position = end;
696
+ continue;
697
+ }
698
+ throw new PolicyParseError(`unexpected character '${char}'`, position);
699
+ }
700
+ tokens.push({ kind: "end", value: "", position: source.length });
701
+ return tokens;
702
+ }
703
+ function readString(source, start) {
704
+ let value = "";
705
+ let position = start + 1;
706
+ while (position < source.length) {
707
+ const char = source[position];
708
+ if (char !== "'") {
709
+ value += char;
710
+ position += 1;
711
+ continue;
712
+ }
713
+ if (source[position + 1] === "'") {
714
+ value += "'";
715
+ position += 2;
716
+ continue;
717
+ }
718
+ return { value, end: position + 1 };
719
+ }
720
+ throw new PolicyParseError("unterminated string literal", start);
721
+ }
722
+ var Parser = class {
723
+ constructor(tokens) {
724
+ this.tokens = tokens;
725
+ }
726
+ index = 0;
727
+ parse() {
728
+ const node = this.parseOr();
729
+ this.expect("end", "end of expression");
730
+ return node;
731
+ }
732
+ parseOr() {
733
+ const operands = [this.parseAnd()];
734
+ while (this.matchKeyword("or")) operands.push(this.parseAnd());
735
+ return operands.length === 1 ? operands[0] : { kind: "or", operands };
736
+ }
737
+ parseAnd() {
738
+ const operands = [this.parseNot()];
739
+ while (this.matchKeyword("and")) operands.push(this.parseNot());
740
+ return operands.length === 1 ? operands[0] : { kind: "and", operands };
741
+ }
742
+ parseNot() {
743
+ if (this.matchKeyword("not")) return { kind: "not", operand: this.parseNot() };
744
+ return this.parsePrimary();
745
+ }
746
+ parsePrimary() {
747
+ if (this.peek().kind === "lparen") {
748
+ this.advance();
749
+ const inner = this.parseOr();
750
+ this.expect("rparen", "')'");
751
+ return inner;
752
+ }
753
+ if (this.peekKeyword("capability") || this.peekKeyword("role")) {
754
+ const fn = this.peek().value;
755
+ const node = this.parseBooleanCall(fn);
756
+ const next = this.peek();
757
+ if (next.kind === "operator") {
758
+ throw new PolicyParseError(`${fn}() is boolean-valued and cannot appear in a comparison`, next.position);
759
+ }
760
+ return node;
761
+ }
762
+ if (this.peekKeyword("exists")) return this.parseExists();
763
+ return this.parseComparison();
764
+ }
765
+ parseBooleanCall(fn) {
766
+ this.advance();
767
+ const key = this.parseCallArgument(fn);
768
+ if (!CAPABILITY_KEY_PATTERN.test(key)) {
769
+ throw new PolicyParseError(`invalid ${fn} key '${key}'`, this.peek().position);
770
+ }
771
+ return { kind: fn, key };
772
+ }
773
+ parseExists() {
774
+ this.advance();
775
+ this.expect("lparen", "'(' after exists");
776
+ const path = this.parsePath("a link path in exists(...)");
777
+ this.expect("comma", "',' between the exists link path and its predicate");
778
+ const where = this.parseOr();
779
+ this.expect("rparen", "')' closing exists(...)");
780
+ return { kind: "exists", path, where };
781
+ }
782
+ parseComparison() {
783
+ const left = this.parseOperand();
784
+ const operator = this.expect("operator", "a comparison operator");
785
+ if (!POLICY_COMPARE_OPS.includes(operator.value)) {
786
+ throw new PolicyParseError(`unsupported operator '${operator.value}'`, operator.position);
787
+ }
788
+ const right = this.parseOperand();
789
+ return { kind: "compare", op: operator.value, left, right };
790
+ }
791
+ parseOperand() {
792
+ const token = this.peek();
793
+ if (token.kind === "string") {
794
+ this.advance();
795
+ return { kind: "literal", value: token.value };
796
+ }
797
+ if (token.kind === "number") {
798
+ this.advance();
799
+ const value = Number(token.value);
800
+ if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) {
801
+ throw new PolicyParseError(
802
+ `numeric literal '${token.value}' exceeds safe integer precision \u2014 quote it as a string`,
803
+ token.position
804
+ );
805
+ }
806
+ return { kind: "literal", value };
807
+ }
808
+ if (token.kind !== "identifier") {
809
+ throw new PolicyParseError(`expected an operand, got '${token.value || "end"}'`, token.position);
810
+ }
811
+ if (token.value === "true" || token.value === "false") {
812
+ this.advance();
813
+ return { kind: "literal", value: token.value === "true" };
814
+ }
815
+ if (token.value === "null") {
816
+ this.advance();
817
+ return { kind: "literal", value: null };
818
+ }
819
+ if (token.value === "claim") {
820
+ this.advance();
821
+ const key = this.parseCallArgument("claim");
822
+ return { kind: "claim", key: this.validateClaimKey(key, token.position) };
823
+ }
824
+ if (token.value === "capability" || token.value === "role") {
825
+ throw new PolicyParseError(`${token.value}() is boolean-valued and cannot appear in a comparison`, token.position);
826
+ }
827
+ return { kind: "path", segments: this.parsePath("a column or link path") };
828
+ }
829
+ validateClaimKey(key, position) {
830
+ if (SCALAR_CLAIM_KEYS.includes(key)) return key;
831
+ if (ARRAY_CLAIM_KEYS.includes(key)) {
832
+ const fn = key === "roles" ? "role('\u2026')" : "capability('\u2026')";
833
+ throw new PolicyParseError(
834
+ `claim('${key}') is not reachable \u2014 '${key}' is an array claim; use ${fn} instead`,
835
+ position
836
+ );
837
+ }
838
+ throw new PolicyParseError(
839
+ `unknown claim key '${key}' \u2014 valid claim() keys are: ${SCALAR_CLAIM_KEYS.map((k) => `'${k}'`).join(", ")}`,
840
+ position
841
+ );
842
+ }
843
+ /** `ident(.ident)*` — first segment must not be a keyword; later segments may be (e.g. `author.role`). */
844
+ parsePath(description) {
845
+ const first = this.peek();
846
+ if (first.kind !== "identifier" || KEYWORDS.has(first.value)) {
847
+ throw new PolicyParseError(`expected ${description}, got '${first.value || "end"}'`, first.position);
848
+ }
849
+ this.advance();
850
+ const segments = [first.value];
851
+ while (this.peek().kind === "dot") {
852
+ this.advance();
853
+ const segment = this.expect("identifier", "a path segment after .");
854
+ segments.push(segment.value);
855
+ }
856
+ return segments;
857
+ }
858
+ parseCallArgument(fn) {
859
+ this.expect("lparen", `'(' after ${fn}`);
860
+ const argument = this.expect("string", `a quoted key in ${fn}(...)`);
861
+ this.expect("rparen", `')' after ${fn} key`);
862
+ return argument.value;
863
+ }
864
+ peek() {
865
+ return this.tokens[this.index];
866
+ }
867
+ peekKeyword(keyword) {
868
+ const token = this.peek();
869
+ return token.kind === "identifier" && token.value === keyword;
870
+ }
871
+ matchKeyword(keyword) {
872
+ if (!this.peekKeyword(keyword)) return false;
873
+ this.advance();
874
+ return true;
875
+ }
876
+ advance() {
877
+ const token = this.peek();
878
+ if (token.kind !== "end") this.index += 1;
879
+ return token;
880
+ }
881
+ expect(kind, description) {
882
+ const token = this.peek();
883
+ if (token.kind !== kind) {
884
+ throw new PolicyParseError(`expected ${description}, got '${token.value || "end"}'`, token.position);
885
+ }
886
+ return this.advance();
887
+ }
888
+ };
889
+ function parsePolicy(source) {
890
+ const trimmed = source.trim();
891
+ if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
892
+ return { source: trimmed, ast: new Parser(tokenize(trimmed)).parse() };
893
+ }
894
+
895
+ // src/predicate-v2/compile.ts
896
+ var POLICY_MAX_LINK_DEPTH = 3;
897
+ var CMP_BY_OP = {
898
+ "=": "eq",
899
+ "!=": "neq",
900
+ "<": "lt",
901
+ "<=": "lte",
902
+ ">": "gt",
903
+ ">=": "gte"
904
+ };
905
+ var FLIPPED = {
906
+ "=": "=",
907
+ "!=": "!=",
908
+ "<": ">",
909
+ "<=": ">=",
910
+ ">": "<",
911
+ ">=": "<="
912
+ };
913
+ function resolveTable(resolver, table) {
914
+ const info = resolver.tables[table];
915
+ if (!info) {
916
+ throw new PolicyCompileError(`unknown table '${table}'`, "unknown-table", Object.keys(resolver.tables));
917
+ }
918
+ return info;
919
+ }
920
+ function resolveColumn(info, ref) {
921
+ const column = info.columns.find((c) => c.name === ref || c.columnName === ref);
922
+ return column?.name;
923
+ }
924
+ function resolveLink(info, ref) {
925
+ return info.links.find((l) => l.name === ref);
926
+ }
927
+ function columnCandidates(info) {
928
+ return [...info.columns.map((c) => c.name), ...info.links.map((l) => l.name)];
929
+ }
930
+ var PolicyCompiler = class {
931
+ constructor(ctx) {
932
+ this.ctx = ctx;
933
+ }
934
+ compile(node, table, depth) {
935
+ switch (node.kind) {
936
+ case "and":
937
+ case "or":
938
+ return { kind: node.kind, operands: node.operands.map((operand) => this.compile(operand, table, depth)) };
939
+ case "not":
940
+ return { kind: "not", operand: this.compile(node.operand, table, depth) };
941
+ case "capability":
942
+ return { kind: "capability", key: node.key };
943
+ case "role":
944
+ return { kind: "role", key: node.key };
945
+ case "exists":
946
+ return this.compileExists(node.path, node.where, table, depth);
947
+ case "compare":
948
+ return this.compileCompare(node, table, depth);
949
+ }
950
+ }
951
+ /** Traverse link path segments (any cardinality), then compile the inner predicate against the final target. */
952
+ compileExists(path, where, table, depth) {
953
+ const { links, finalTable } = this.resolveLinkPath(path, table, depth, { toOneOnly: false });
954
+ let pred = this.compile(where, finalTable, depth + links.length);
955
+ for (let i = links.length - 1; i >= 0; i -= 1) {
956
+ pred = { kind: "exists", link: links[i].name, where: pred };
957
+ }
958
+ return pred;
959
+ }
960
+ resolveLinkPath(path, table, depth, { toOneOnly }) {
961
+ const links = [];
962
+ let current = table;
963
+ for (const segment of path) {
964
+ const info = resolveTable(this.ctx.resolver, current);
965
+ const link = resolveLink(info, segment);
966
+ if (!link) {
967
+ throw new PolicyCompileError(
968
+ `unknown link '${segment}' on table '${current}'`,
969
+ "unknown-link",
970
+ info.links.map((l) => l.name)
971
+ );
972
+ }
973
+ if (toOneOnly && link.kind === "toMany") {
974
+ throw new PolicyCompileError(
975
+ `dotted paths traverse to-one links only \u2014 '${segment}' on '${current}' is to-many; use exists(${path.join(".")}, \u2026)`,
976
+ "not-to-one"
977
+ );
978
+ }
979
+ links.push(link);
980
+ current = link.target;
981
+ if (depth + links.length > this.ctx.maxLinkDepth) {
982
+ throw new PolicyCompileError(
983
+ `link traversal depth exceeds ${this.ctx.maxLinkDepth} (at '${segment}')`,
984
+ "link-depth-exceeded"
985
+ );
986
+ }
987
+ }
988
+ return { links, finalTable: current };
989
+ }
990
+ compileCompare(node, table, depth) {
991
+ const leftIsPath = node.left.kind === "path";
992
+ const rightIsPath = node.right.kind === "path";
993
+ if (leftIsPath && rightIsPath) {
994
+ throw new PolicyCompileError(
995
+ "column-to-column comparisons are not supported in v2.0 policies",
996
+ "unsupported-comparison"
997
+ );
998
+ }
999
+ if (!leftIsPath && !rightIsPath) {
1000
+ throw new PolicyCompileError(
1001
+ "a comparison must reference a column \u2014 claims-to-literal comparisons are not representable in v2.0; use capability() or role()",
1002
+ "unsupported-comparison"
1003
+ );
1004
+ }
1005
+ const [pathSide, valueSide, op] = leftIsPath ? [node.left, node.right, node.op] : [node.right, node.left, FLIPPED[node.op]];
1006
+ const segments = pathSide.segments;
1007
+ const linkSegments = segments.slice(0, -1);
1008
+ const columnRef = segments[segments.length - 1];
1009
+ const { links, finalTable } = this.resolveLinkPath(linkSegments, table, depth, { toOneOnly: true });
1010
+ const info = resolveTable(this.ctx.resolver, finalTable);
1011
+ const column = resolveColumn(info, columnRef);
1012
+ if (!column) {
1013
+ throw new PolicyCompileError(
1014
+ `unknown column '${columnRef}' on table '${finalTable}'`,
1015
+ "unknown-column",
1016
+ columnCandidates(info)
1017
+ );
1018
+ }
1019
+ let cmp = this.buildCmp(op, column, valueSide);
1020
+ for (let i = links.length - 1; i >= 0; i -= 1) {
1021
+ cmp = { kind: "exists", link: links[i].name, where: cmp };
1022
+ }
1023
+ return cmp;
1024
+ }
1025
+ buildCmp(op, column, valueSide) {
1026
+ if (valueSide.kind === "literal" && valueSide.value === null) {
1027
+ if (op !== "=" && op !== "!=") {
1028
+ throw new PolicyCompileError(
1029
+ `null only supports = and != comparisons, got '${op}'`,
1030
+ "invalid-null-comparison"
1031
+ );
1032
+ }
1033
+ return { kind: "cmp", op: "isNull", column, value: { kind: "literal", value: op === "=" } };
1034
+ }
1035
+ const value = valueSide.kind === "claim" ? { kind: "claim", key: valueSide.key } : { kind: "literal", value: valueSide.value };
1036
+ return { kind: "cmp", op: CMP_BY_OP[op], column, value };
1037
+ }
1038
+ };
1039
+ function compilePredicateV2({ expression, table, resolver, maxLinkDepth }) {
1040
+ const parsed = typeof expression === "string" ? parsePolicy(expression) : expression;
1041
+ const compiler = new PolicyCompiler({ resolver, maxLinkDepth: maxLinkDepth ?? POLICY_MAX_LINK_DEPTH });
1042
+ return compiler.compile(parsed.ast, table, 0);
1043
+ }
1044
+ function assertClaimsOnlyWhen(parsed) {
1045
+ walkPolicy(parsed.ast, (node) => {
1046
+ if (node.kind === "compare") {
1047
+ const columnRef = [node.left, node.right].find((operand) => operand.kind === "path");
1048
+ if (columnRef) {
1049
+ throw new PolicyCompileError(
1050
+ `variant 'when' guards are claims-only \u2014 column reference '${columnRef.segments.join(".")}' is not allowed`,
1051
+ "when-not-claims-only"
1052
+ );
1053
+ }
1054
+ throw new PolicyCompileError(
1055
+ "variant 'when' guards support capability() and role() checks only in v2.0",
1056
+ "when-not-claims-only"
1057
+ );
1058
+ }
1059
+ if (node.kind === "exists") {
1060
+ throw new PolicyCompileError(
1061
+ "variant 'when' guards are claims-only \u2014 exists() is not allowed",
1062
+ "when-not-claims-only"
1063
+ );
1064
+ }
1065
+ });
1066
+ }
1067
+ function compilePolicy({ expression, table, resolver, maxLinkDepth }) {
1068
+ if (typeof expression === "string") {
1069
+ return compilePredicateV2({ expression, table, resolver, maxLinkDepth });
1070
+ }
1071
+ const defaultPred = compilePredicateV2({ expression: expression.default, table, resolver, maxLinkDepth });
1072
+ if (expression.variants.length === 0) return defaultPred;
1073
+ const branches = expression.variants.map((variant) => {
1074
+ const parsedWhen = parsePolicy(variant.when);
1075
+ assertClaimsOnlyWhen(parsedWhen);
1076
+ const when = compilePredicateV2({ expression: parsedWhen, table, resolver, maxLinkDepth });
1077
+ const grant = variant.allow === true ? { kind: "const", value: true } : compilePredicateV2({ expression: variant.policy, table, resolver, maxLinkDepth });
1078
+ return { kind: "and", operands: [when, grant] };
1079
+ });
1080
+ return { kind: "or", operands: [defaultPred, ...branches] };
1081
+ }
1082
+
1083
+ exports.POLICY_COMPARE_OPS = POLICY_COMPARE_OPS;
1084
+ exports.POLICY_MAX_LINK_DEPTH = POLICY_MAX_LINK_DEPTH;
1085
+ exports.PolicyCompileError = PolicyCompileError;
1086
+ exports.PolicyParseError = PolicyParseError;
1087
+ exports.assertClaimsOnlyWhen = assertClaimsOnlyWhen;
1088
+ exports.compilePolicy = compilePolicy;
1089
+ exports.compilePredicateV2 = compilePredicateV2;
1090
+ exports.parsePolicy = parsePolicy;
1091
+ exports.walkPolicy = walkPolicy;
1092
+ //# sourceMappingURL=predicate-v2.cjs.map
1093
+ //# sourceMappingURL=predicate-v2.cjs.map