@sequenceholdings/orm 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring.cjs +53 -7
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +11 -7
- package/dist/authoring.d.ts +11 -7
- package/dist/authoring.js +53 -7
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +48 -2
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +72 -72
- package/dist/canonical.d.ts +72 -72
- package/dist/canonical.js +48 -2
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +337 -86
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +337 -86
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +1 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +493 -52
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +22 -7
- package/dist/compiler.d.ts +22 -7
- package/dist/compiler.js +493 -53
- package/dist/compiler.js.map +1 -1
- package/dist/{constants-C6EG6Wq7.d.cts → constants-Dou1RkTo.d.cts} +7 -2
- package/dist/{constants-C6EG6Wq7.d.ts → constants-Dou1RkTo.d.ts} +7 -2
- package/dist/dynamic-input.d.cts +1 -1
- package/dist/dynamic-input.d.ts +1 -1
- package/dist/index.cjs +329 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -58
- package/dist/index.d.ts +66 -58
- package/dist/index.js +329 -131
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +50 -3
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +71 -71
- package/dist/ir.d.ts +71 -71
- package/dist/ir.js +50 -3
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +145 -22
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +41 -4
- package/dist/predicate-v2.d.ts +41 -4
- package/dist/predicate-v2.js +145 -22
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +53 -3
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +3 -1
- package/dist/sdl.d.ts +3 -1
- package/dist/sdl.js +53 -3
- package/dist/sdl.js.map +1 -1
- package/package.json +1 -1
- package/dist/{definition-DjfC7Lll.d.cts → definition-B99BsOAO.d.cts} +4 -4
- package/dist/{definition-DjfC7Lll.d.ts → definition-B99BsOAO.d.ts} +4 -4
package/dist/cli.js
CHANGED
|
@@ -56,18 +56,22 @@ var init_rehearsal_poll = __esm({
|
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
// src/constants.ts
|
|
59
|
-
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
59
|
+
var NAMESPACE_PATTERN = /^(?!(?:events|native_sources)$)[a-z][a-z0-9_]{0,40}$/;
|
|
60
60
|
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
61
61
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
62
62
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
63
63
|
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
64
|
+
var MAX_PERSISTED_READ_ROWS = 50001;
|
|
64
65
|
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
65
66
|
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
|
|
66
67
|
var SYNCED_SOURCE_PATTERN = QUALIFIED_TABLE_PATTERN;
|
|
67
68
|
var ORM_SCHEMA_PREFIX = "orm_";
|
|
68
69
|
var CONSUMER_ROLE_SUFFIX = "_consumer";
|
|
69
|
-
var ormSchemaName = (namespace) =>
|
|
70
|
-
|
|
70
|
+
var ormSchemaName = (namespace) => {
|
|
71
|
+
if (!NAMESPACE_PATTERN.test(namespace)) throw new Error(`invalid or reserved ORM namespace '${namespace}'`);
|
|
72
|
+
return `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
73
|
+
};
|
|
74
|
+
var consumerRoleName = (namespace) => `${ormSchemaName(namespace)}${CONSUMER_ROLE_SUFFIX}`;
|
|
71
75
|
var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
72
76
|
var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
|
|
73
77
|
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
@@ -535,6 +539,7 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
535
539
|
function validateSource({ scope, source, table }) {
|
|
536
540
|
const { fail: fail4 } = scope;
|
|
537
541
|
if (source.kind === "etl" && table.source === void 0) fail4(`etl source '${source.name}' must reference a synced table`);
|
|
542
|
+
if (source.kind === "etl" && table.primaryKey?.length === 0) fail4(`etl source '${source.name}' cannot use an unkeyed synced table for resolved row identity`);
|
|
538
543
|
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
539
544
|
fail4(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
540
545
|
}
|
|
@@ -983,7 +988,7 @@ var canonicalTableSchema = z.object({
|
|
|
983
988
|
/** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
|
|
984
989
|
source: z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
|
|
985
990
|
columns: z.array(canonicalColumnSchema).default([]),
|
|
986
|
-
primaryKey: z.array(columnIdentifier).
|
|
991
|
+
primaryKey: z.array(columnIdentifier).max(8).optional(),
|
|
987
992
|
links: z.array(canonicalLinkSchema).default([]),
|
|
988
993
|
indexes: z.array(canonicalIndexSchema).default([]),
|
|
989
994
|
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
@@ -1019,6 +1024,29 @@ var canonicalTableSchema = z.object({
|
|
|
1019
1024
|
* data plane. Omitted otherwise.
|
|
1020
1025
|
*/
|
|
1021
1026
|
materializes: identifier2.optional()
|
|
1027
|
+
}).superRefine((table, ctx) => {
|
|
1028
|
+
if (table.primaryKey?.length !== 0) return;
|
|
1029
|
+
const fail4 = (message, path = ["primaryKey"]) => ctx.addIssue({ code: "custom", path, message: `unkeyed table '${table.table}' ${message}` });
|
|
1030
|
+
if (table.source === void 0 || table.columns.length === 0) {
|
|
1031
|
+
fail4("requires a typed synced source");
|
|
1032
|
+
}
|
|
1033
|
+
if ([table.policies.write, table.policies.insert, table.policies.update].some((policy) => policy !== void 0)) {
|
|
1034
|
+
fail4("is read-only and cannot declare write policies", ["policies"]);
|
|
1035
|
+
}
|
|
1036
|
+
const ownedCollections = [table.indexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
|
|
1037
|
+
const ownedMetadata = [table.cellMetadata, table.internal, table.materializes, table.renamedFrom];
|
|
1038
|
+
if (ownedCollections.some((items) => items !== void 0 && items.length > 0) || ownedMetadata.some((value) => value !== void 0)) {
|
|
1039
|
+
fail4("cannot declare owned storage, constraints or identity operations");
|
|
1040
|
+
}
|
|
1041
|
+
for (const [index3, column] of table.columns.entries()) {
|
|
1042
|
+
const ownedColumnMetadata = [column.default, column.defaultFn, column.onUpdateFn, column.renamedFrom, column.cast, column.cell];
|
|
1043
|
+
if (column.unique || ownedColumnMetadata.some((value) => value !== void 0)) {
|
|
1044
|
+
fail4("cannot declare column uniqueness, mutation defaults or migration metadata", ["columns", index3]);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
for (const [index3, link] of table.links.entries()) {
|
|
1048
|
+
if (link.enforce || link.kind === "toMany") fail4("cannot provide an identity for this link", ["links", index3]);
|
|
1049
|
+
}
|
|
1022
1050
|
});
|
|
1023
1051
|
var MAX_VIEW_QUERY = 1e5;
|
|
1024
1052
|
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
@@ -1075,6 +1103,26 @@ var canonicalViewSchema = z.object({
|
|
|
1075
1103
|
}
|
|
1076
1104
|
}
|
|
1077
1105
|
});
|
|
1106
|
+
function validateUnkeyedLinkIdentities({ definition, ctx }) {
|
|
1107
|
+
const tables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1108
|
+
const relations = [
|
|
1109
|
+
...definition.tables.map((table, index3) => ({ kind: "tables", index: index3, links: table.links })),
|
|
1110
|
+
...(definition.views ?? []).map((view, index3) => ({ kind: "views", index: index3, links: view.links }))
|
|
1111
|
+
];
|
|
1112
|
+
for (const relation of relations) {
|
|
1113
|
+
for (const [linkIndex, link] of relation.links.entries()) {
|
|
1114
|
+
if (link.kind !== "toOne" || link.namespace !== void 0 && link.namespace !== definition.namespace) continue;
|
|
1115
|
+
const target = link.target === void 0 ? void 0 : tables.get(link.target);
|
|
1116
|
+
if (target?.source !== void 0 && target.primaryKey?.length === 0) {
|
|
1117
|
+
ctx.addIssue({
|
|
1118
|
+
code: "custom",
|
|
1119
|
+
path: [relation.kind, relation.index, "links", linkIndex],
|
|
1120
|
+
message: `link '${link.name}' cannot use unkeyed table '${target.table}' as a to-one identity`
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1078
1126
|
var CELL_PART_TYPES = {
|
|
1079
1127
|
at: "timestamptz",
|
|
1080
1128
|
by: "text",
|
|
@@ -1138,7 +1186,7 @@ var exportGateSchema = z.union([z.literal(true), z.object({ capability: z.string
|
|
|
1138
1186
|
var relationExportSchema = z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
|
|
1139
1187
|
var localDefinitionSchema = z.object({
|
|
1140
1188
|
definition_format: z.literal(CANONICAL_FORMAT),
|
|
1141
|
-
namespace: identifier2.max(41),
|
|
1189
|
+
namespace: identifier2.max(41).regex(NAMESPACE_PATTERN, "invalid or reserved ORM namespace"),
|
|
1142
1190
|
description: z.string().max(2e3).optional(),
|
|
1143
1191
|
enums: z.array(canonicalEnumSchema).default([]),
|
|
1144
1192
|
tables: z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -1172,6 +1220,7 @@ var localDefinitionSchema = z.object({
|
|
|
1172
1220
|
}
|
|
1173
1221
|
}
|
|
1174
1222
|
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1223
|
+
validateUnkeyedLinkIdentities({ definition, ctx });
|
|
1175
1224
|
const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
|
|
1176
1225
|
for (const name2 of Object.keys(definition.exports ?? {})) {
|
|
1177
1226
|
if (!relations.has(name2)) ctx.addIssue({ code: "custom", path: ["exports", name2], message: `export '${name2}' is not a local object` });
|
|
@@ -1494,7 +1543,7 @@ var limitIrSchema = z.object({
|
|
|
1494
1543
|
variable: z.string().optional(),
|
|
1495
1544
|
value: z.number().int().positive().optional(),
|
|
1496
1545
|
default: z.number().int().positive(),
|
|
1497
|
-
max: z.number().int().positive()
|
|
1546
|
+
max: z.number().int().positive().max(MAX_PERSISTED_READ_ROWS)
|
|
1498
1547
|
});
|
|
1499
1548
|
var selectionIrSchema = z.lazy(
|
|
1500
1549
|
() => z.object({
|
|
@@ -2177,6 +2226,7 @@ var Parser2 = class {
|
|
|
2177
2226
|
return node;
|
|
2178
2227
|
}
|
|
2179
2228
|
if (this.peekKeyword("exists")) return this.parseExists();
|
|
2229
|
+
if (this.peekKeyword("visible") && this.tokens[this.index + 1]?.kind === "lparen") return this.parseVisible();
|
|
2180
2230
|
return this.parseComparison();
|
|
2181
2231
|
}
|
|
2182
2232
|
parseBooleanCall(fn) {
|
|
@@ -2196,6 +2246,13 @@ var Parser2 = class {
|
|
|
2196
2246
|
this.expect("rparen", "')' closing exists(...)");
|
|
2197
2247
|
return { kind: "exists", path, where };
|
|
2198
2248
|
}
|
|
2249
|
+
parseVisible() {
|
|
2250
|
+
this.advance();
|
|
2251
|
+
this.expect("lparen", "'(' after visible");
|
|
2252
|
+
const path = this.parsePath("a link path in visible(...)");
|
|
2253
|
+
this.expect("rparen", "')' closing visible(...)");
|
|
2254
|
+
return { kind: "visible", path };
|
|
2255
|
+
}
|
|
2199
2256
|
parseComparison() {
|
|
2200
2257
|
const left = this.parseOperand();
|
|
2201
2258
|
const operator = this.expect("operator", "a comparison operator");
|
|
@@ -2311,6 +2368,24 @@ function parsePolicy(source) {
|
|
|
2311
2368
|
|
|
2312
2369
|
// src/predicate-v2/compile.ts
|
|
2313
2370
|
var POLICY_MAX_LINK_DEPTH = 3;
|
|
2371
|
+
function scopeToNamespace(pred, namespace) {
|
|
2372
|
+
switch (pred.kind) {
|
|
2373
|
+
case "capability":
|
|
2374
|
+
case "role":
|
|
2375
|
+
return pred.namespace ? pred : { ...pred, namespace };
|
|
2376
|
+
case "and":
|
|
2377
|
+
case "or":
|
|
2378
|
+
return { ...pred, operands: pred.operands.map((operand) => scopeToNamespace(operand, namespace)) };
|
|
2379
|
+
case "not":
|
|
2380
|
+
return { ...pred, operand: scopeToNamespace(pred.operand, namespace) };
|
|
2381
|
+
case "exists":
|
|
2382
|
+
return { ...pred, where: scopeToNamespace(pred.where, namespace) };
|
|
2383
|
+
case "aggPredicate":
|
|
2384
|
+
return pred.where ? { ...pred, where: scopeToNamespace(pred.where, namespace) } : pred;
|
|
2385
|
+
default:
|
|
2386
|
+
return pred;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2314
2389
|
var CMP_BY_OP = {
|
|
2315
2390
|
"=": "eq",
|
|
2316
2391
|
"!=": "neq",
|
|
@@ -2344,7 +2419,7 @@ function resolveLink(info, ref) {
|
|
|
2344
2419
|
function columnCandidates(info) {
|
|
2345
2420
|
return [...info.columns.map((c) => c.name), ...info.links.map((l) => l.name)];
|
|
2346
2421
|
}
|
|
2347
|
-
var PolicyCompiler = class {
|
|
2422
|
+
var PolicyCompiler = class _PolicyCompiler {
|
|
2348
2423
|
constructor(ctx) {
|
|
2349
2424
|
this.ctx = ctx;
|
|
2350
2425
|
}
|
|
@@ -2357,13 +2432,15 @@ var PolicyCompiler = class {
|
|
|
2357
2432
|
return { kind: node.kind, operands: node.operands.map((operand) => this.compile(operand, table, depth)) };
|
|
2358
2433
|
case "not":
|
|
2359
2434
|
assertNoNegatedExternalClaim(node.operand);
|
|
2360
|
-
return { kind: "not", operand: this.compile(node.operand, table, depth) };
|
|
2435
|
+
return { kind: "not", operand: new _PolicyCompiler({ ...this.ctx, negated: true }).compile(node.operand, table, depth) };
|
|
2361
2436
|
case "capability":
|
|
2362
2437
|
return { kind: "capability", key: node.key };
|
|
2363
2438
|
case "role":
|
|
2364
2439
|
return { kind: "role", key: node.key };
|
|
2365
2440
|
case "exists":
|
|
2366
2441
|
return this.compileExists(node.path, node.where, table, depth);
|
|
2442
|
+
case "visible":
|
|
2443
|
+
return this.compileVisible(node.path, table, depth);
|
|
2367
2444
|
case "compare":
|
|
2368
2445
|
return this.compileCompare(node, table, depth);
|
|
2369
2446
|
}
|
|
@@ -2377,6 +2454,36 @@ var PolicyCompiler = class {
|
|
|
2377
2454
|
}
|
|
2378
2455
|
return pred;
|
|
2379
2456
|
}
|
|
2457
|
+
/** `visible(path)` → `exists(path, <final target's read policy>)`. */
|
|
2458
|
+
compileVisible(path, table, depth) {
|
|
2459
|
+
const { links, finalTable } = this.resolveLinkPath(path, table, depth, { toOneOnly: false });
|
|
2460
|
+
const readPolicy = this.ctx.resolver.readPolicy;
|
|
2461
|
+
if (!readPolicy) {
|
|
2462
|
+
throw new PolicyCompileError(
|
|
2463
|
+
`visible(${path.join(".")}) needs the target's read policy, which this compile context does not provide`,
|
|
2464
|
+
"visible-unresolvable"
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
if (this.ctx.visiting.includes(finalTable)) {
|
|
2468
|
+
throw new PolicyCompileError(
|
|
2469
|
+
`visible(${path.join(".")}) on '${table}' reaches '${finalTable}' whose read policy is already being expanded (${[...this.ctx.visiting, finalTable].join(" \u2192 ")})`,
|
|
2470
|
+
"visible-cycle"
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2473
|
+
const expression = readPolicy(finalTable);
|
|
2474
|
+
let pred = expression === void 0 ? { kind: "const", value: true } : compilePolicyAt({
|
|
2475
|
+
expression,
|
|
2476
|
+
table: finalTable,
|
|
2477
|
+
ctx: { ...this.ctx, visiting: [...this.ctx.visiting, finalTable] },
|
|
2478
|
+
depth: depth + links.length
|
|
2479
|
+
});
|
|
2480
|
+
const owner = this.ctx.resolver.policyNamespace?.(finalTable);
|
|
2481
|
+
if (owner) pred = scopeToNamespace(pred, owner);
|
|
2482
|
+
for (let i = links.length - 1; i >= 0; i -= 1) {
|
|
2483
|
+
pred = { kind: "exists", link: links[i].name, where: pred };
|
|
2484
|
+
}
|
|
2485
|
+
return pred;
|
|
2486
|
+
}
|
|
2380
2487
|
resolveLinkPath(path, table, depth, { toOneOnly }) {
|
|
2381
2488
|
const links = [];
|
|
2382
2489
|
let current = table;
|
|
@@ -2437,6 +2544,7 @@ var PolicyCompiler = class {
|
|
|
2437
2544
|
);
|
|
2438
2545
|
}
|
|
2439
2546
|
if (valueSide.kind === "claim" && isExternalPolicyClaimKey(valueSide.key)) {
|
|
2547
|
+
if (this.ctx.negated) throw negatedExternalClaim(valueSide.key);
|
|
2440
2548
|
assertExternalClaimComparison({ op, key: valueSide.key, column: info.columns.find((c) => c.name === column) });
|
|
2441
2549
|
}
|
|
2442
2550
|
let cmp = this.buildCmp(op, column, valueSide);
|
|
@@ -2474,22 +2582,22 @@ function assertExternalClaimComparison({ op, key, column }) {
|
|
|
2474
2582
|
);
|
|
2475
2583
|
}
|
|
2476
2584
|
}
|
|
2585
|
+
function negatedExternalClaim(key) {
|
|
2586
|
+
return new PolicyCompileError(
|
|
2587
|
+
`claim('${key}') cannot appear under not(): a negated borrower comparison admits other borrowers' rows`,
|
|
2588
|
+
"unsupported-comparison"
|
|
2589
|
+
);
|
|
2590
|
+
}
|
|
2477
2591
|
function assertNoNegatedExternalClaim(operand) {
|
|
2478
2592
|
walkPolicy(operand, (node) => {
|
|
2479
2593
|
if (node.kind !== "compare") return;
|
|
2480
2594
|
const claim = [node.left, node.right].find((side) => side.kind === "claim" && isExternalPolicyClaimKey(side.key));
|
|
2481
|
-
if (claim?.kind === "claim")
|
|
2482
|
-
throw new PolicyCompileError(
|
|
2483
|
-
`claim('${claim.key}') cannot appear under not(): a negated borrower comparison admits other borrowers' rows`,
|
|
2484
|
-
"unsupported-comparison"
|
|
2485
|
-
);
|
|
2486
|
-
}
|
|
2595
|
+
if (claim?.kind === "claim") throw negatedExternalClaim(claim.key);
|
|
2487
2596
|
});
|
|
2488
2597
|
}
|
|
2489
|
-
function
|
|
2598
|
+
function compilePredicateAt({ expression, table, ctx, depth }) {
|
|
2490
2599
|
const parsed = typeof expression === "string" ? parsePolicy(expression) : expression;
|
|
2491
|
-
|
|
2492
|
-
return compiler.compile(parsed.ast, table, 0);
|
|
2600
|
+
return new PolicyCompiler(ctx).compile(parsed.ast, table, depth);
|
|
2493
2601
|
}
|
|
2494
2602
|
function assertClaimsOnlyWhen(parsed) {
|
|
2495
2603
|
walkPolicy(parsed.ast, (node) => {
|
|
@@ -2506,25 +2614,34 @@ function assertClaimsOnlyWhen(parsed) {
|
|
|
2506
2614
|
"when-not-claims-only"
|
|
2507
2615
|
);
|
|
2508
2616
|
}
|
|
2509
|
-
if (node.kind === "exists") {
|
|
2617
|
+
if (node.kind === "exists" || node.kind === "visible") {
|
|
2510
2618
|
throw new PolicyCompileError(
|
|
2511
|
-
|
|
2619
|
+
`variant 'when' guards are claims-only \u2014 ${node.kind}() is not allowed`,
|
|
2512
2620
|
"when-not-claims-only"
|
|
2513
2621
|
);
|
|
2514
2622
|
}
|
|
2515
2623
|
});
|
|
2516
2624
|
}
|
|
2517
|
-
function
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2625
|
+
function initialVisiting({ table, access }) {
|
|
2626
|
+
return (access ?? "read") === "read" ? [table] : [];
|
|
2627
|
+
}
|
|
2628
|
+
function compilePolicy({ expression, table, resolver, maxLinkDepth, access }) {
|
|
2629
|
+
return compilePolicyAt({
|
|
2630
|
+
expression,
|
|
2631
|
+
table,
|
|
2632
|
+
depth: 0,
|
|
2633
|
+
ctx: { resolver, maxLinkDepth: maxLinkDepth ?? POLICY_MAX_LINK_DEPTH, visiting: initialVisiting({ table, access }) }
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
function compilePolicyAt({ expression, table, ctx, depth }) {
|
|
2637
|
+
if (typeof expression === "string") return compilePredicateAt({ expression, table, ctx, depth });
|
|
2638
|
+
const defaultPred = compilePredicateAt({ expression: expression.default, table, ctx, depth });
|
|
2522
2639
|
if (expression.variants.length === 0) return defaultPred;
|
|
2523
2640
|
const branches = expression.variants.map((variant) => {
|
|
2524
2641
|
const parsedWhen = parsePolicy(variant.when);
|
|
2525
2642
|
assertClaimsOnlyWhen(parsedWhen);
|
|
2526
|
-
const when =
|
|
2527
|
-
const grant = variant.allow === true ? { kind: "const", value: true } :
|
|
2643
|
+
const when = compilePredicateAt({ expression: parsedWhen, table, ctx, depth });
|
|
2644
|
+
const grant = variant.allow === true ? { kind: "const", value: true } : compilePredicateAt({ expression: variant.policy, table, ctx, depth });
|
|
2528
2645
|
return { kind: "and", operands: [when, grant] };
|
|
2529
2646
|
});
|
|
2530
2647
|
return { kind: "or", operands: [defaultPred, ...branches] };
|
|
@@ -2562,33 +2679,6 @@ function andPred(...preds) {
|
|
|
2562
2679
|
if (operands.length === 1) return operands[0];
|
|
2563
2680
|
return { kind: "and", operands };
|
|
2564
2681
|
}
|
|
2565
|
-
function walkPred(pred, visit2) {
|
|
2566
|
-
visit2(pred);
|
|
2567
|
-
switch (pred.kind) {
|
|
2568
|
-
case "and":
|
|
2569
|
-
case "or":
|
|
2570
|
-
for (const operand of pred.operands) walkPred(operand, visit2);
|
|
2571
|
-
return;
|
|
2572
|
-
case "not":
|
|
2573
|
-
walkPred(pred.operand, visit2);
|
|
2574
|
-
return;
|
|
2575
|
-
case "exists":
|
|
2576
|
-
walkPred(pred.where, visit2);
|
|
2577
|
-
return;
|
|
2578
|
-
case "aggPredicate":
|
|
2579
|
-
if (pred.where) walkPred(pred.where, visit2);
|
|
2580
|
-
return;
|
|
2581
|
-
default:
|
|
2582
|
-
return;
|
|
2583
|
-
}
|
|
2584
|
-
}
|
|
2585
|
-
function predContainsExists(pred) {
|
|
2586
|
-
let found = false;
|
|
2587
|
-
walkPred(pred, (node) => {
|
|
2588
|
-
if (node.kind === "exists" || node.kind === "aggPredicate") found = true;
|
|
2589
|
-
});
|
|
2590
|
-
return found;
|
|
2591
|
-
}
|
|
2592
2682
|
|
|
2593
2683
|
// src/compiler-gql/inject.ts
|
|
2594
2684
|
function collectPinnedColumns(writePolicy) {
|
|
@@ -2732,12 +2822,6 @@ var PolicyInjector = class {
|
|
|
2732
2822
|
if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
|
|
2733
2823
|
if (root.op === "insert" || root.op === "insertOne") {
|
|
2734
2824
|
const insertPolicy = this.insert(table);
|
|
2735
|
-
if (predContainsExists(insertPolicy)) {
|
|
2736
|
-
throw new DocumentCompileError(
|
|
2737
|
-
`table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its insert policy traverses a link (exists)`,
|
|
2738
|
-
"insert-check-unrepresentable"
|
|
2739
|
-
);
|
|
2740
|
-
}
|
|
2741
2825
|
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
2742
2826
|
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
2743
2827
|
);
|
|
@@ -2752,22 +2836,14 @@ var PolicyInjector = class {
|
|
|
2752
2836
|
const updatePolicy = this.update(table);
|
|
2753
2837
|
const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
|
|
2754
2838
|
if (root.op === "update" || root.op === "updateByPk") {
|
|
2755
|
-
|
|
2756
|
-
return { ...root, where, ...check, returning };
|
|
2839
|
+
return { ...root, where, insertCheck: updatePolicy, returning };
|
|
2757
2840
|
}
|
|
2758
2841
|
if (root.op === "cowOverride") {
|
|
2759
2842
|
const insertPolicy = this.insert(table);
|
|
2760
|
-
if (predContainsExists(insertPolicy)) {
|
|
2761
|
-
throw new DocumentCompileError(
|
|
2762
|
-
`table '${table}' cannot be copy-on-write overridden: v2.0 insert checks are claims + inserted-values only, but its insert policy traverses a link (exists)`,
|
|
2763
|
-
"insert-check-unrepresentable"
|
|
2764
|
-
);
|
|
2765
|
-
}
|
|
2766
2843
|
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
2767
2844
|
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
2768
2845
|
);
|
|
2769
|
-
|
|
2770
|
-
return { ...root, pinnedColumns: pins, cowCopyCheck: insertPolicy, where, ...check, returning };
|
|
2846
|
+
return { ...root, pinnedColumns: pins, cowCopyCheck: insertPolicy, where, insertCheck: updatePolicy, returning };
|
|
2771
2847
|
}
|
|
2772
2848
|
return { ...root, where, returning };
|
|
2773
2849
|
}
|
|
@@ -3001,7 +3077,7 @@ function scopePolicy(pred, namespace) {
|
|
|
3001
3077
|
switch (pred.kind) {
|
|
3002
3078
|
case "capability":
|
|
3003
3079
|
case "role":
|
|
3004
|
-
return { ...pred, namespace };
|
|
3080
|
+
return pred.namespace ? pred : { ...pred, namespace };
|
|
3005
3081
|
case "and":
|
|
3006
3082
|
case "or":
|
|
3007
3083
|
return { ...pred, operands: pred.operands.map((p) => scopePolicy(p, namespace)) };
|
|
@@ -3091,6 +3167,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3091
3167
|
const platformMeta = usesPlatformUsers ? platformUserMeta() : void 0;
|
|
3092
3168
|
if (platformMeta) tables[PLATFORM_USER_TABLE] = platformMeta;
|
|
3093
3169
|
const syncedTableNames = new Set(canonical.tables.filter((t2) => t2.source !== void 0).map((t2) => t2.table));
|
|
3170
|
+
const unkeyedSyncedTableNames = new Set(relations.filter((table) => table.source !== void 0 && table.primaryKey?.length === 0).map((table) => table.table));
|
|
3094
3171
|
const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
|
|
3095
3172
|
const claimsGatedMemo = /* @__PURE__ */ new Map();
|
|
3096
3173
|
const relationIsClaimsGated = (name2) => {
|
|
@@ -3104,6 +3181,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3104
3181
|
};
|
|
3105
3182
|
for (const table of relations) {
|
|
3106
3183
|
const isView = viewNames.has(table.table);
|
|
3184
|
+
const readOnly = isView || unkeyedSyncedTableNames.has(table.table);
|
|
3107
3185
|
const owner = owners.get(table.table);
|
|
3108
3186
|
const entries = resolveColumnEntries(table, enums, diagnostics);
|
|
3109
3187
|
columnEntries.set(table.table, entries);
|
|
@@ -3151,13 +3229,13 @@ function buildCompileContext(rawCanonical) {
|
|
|
3151
3229
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
3152
3230
|
claimsGated: relationIsClaimsGated(table.table),
|
|
3153
3231
|
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
3154
|
-
primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
3232
|
+
primaryKey: isView || unkeyedSyncedTableNames.has(table.table) ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
3155
3233
|
links: [],
|
|
3156
3234
|
filterable: data.map((c) => c.name),
|
|
3157
3235
|
orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
3158
|
-
insertable: resolved ? resolvedWritable(data) :
|
|
3159
|
-
settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey2.includes(name2)) :
|
|
3160
|
-
appendable:
|
|
3236
|
+
insertable: resolved ? resolvedWritable(data) : readOnly ? [] : active.map((c) => c.name),
|
|
3237
|
+
settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey2.includes(name2)) : readOnly ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
3238
|
+
appendable: readOnly ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
3161
3239
|
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
3162
3240
|
...cells ? { cells } : {},
|
|
3163
3241
|
...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey2[0] }) } : {},
|
|
@@ -3181,6 +3259,16 @@ function buildCompileContext(rawCanonical) {
|
|
|
3181
3259
|
});
|
|
3182
3260
|
continue;
|
|
3183
3261
|
}
|
|
3262
|
+
const identityTable = link.kind === "toOne" ? targetName : link.kind === "toMany" ? table.table : void 0;
|
|
3263
|
+
if (identityTable !== void 0 && unkeyedSyncedTableNames.has(identityTable)) {
|
|
3264
|
+
diagnostics.push({
|
|
3265
|
+
severity: "error",
|
|
3266
|
+
code: "unkeyed-link-identity",
|
|
3267
|
+
table: table.table,
|
|
3268
|
+
message: `link '${link.name}' cannot use unkeyed table '${identityTable}' as a row identity`
|
|
3269
|
+
});
|
|
3270
|
+
continue;
|
|
3271
|
+
}
|
|
3184
3272
|
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
|
|
3185
3273
|
const onEntries = link.kind === "toMany" ? targetEntries : entries;
|
|
3186
3274
|
const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
|
|
@@ -3210,7 +3298,14 @@ function buildCompileContext(rawCanonical) {
|
|
|
3210
3298
|
const updatePolicies = /* @__PURE__ */ new Map();
|
|
3211
3299
|
const pins = /* @__PURE__ */ new Map();
|
|
3212
3300
|
const splitWritePolicyTables = /* @__PURE__ */ new Set();
|
|
3213
|
-
const
|
|
3301
|
+
const readPolicyExpressions = new Map(relations.map((relation) => [relation.table, relation.policies.read]));
|
|
3302
|
+
const resolver = {
|
|
3303
|
+
tables,
|
|
3304
|
+
readPolicy: (table) => readPolicyExpressions.get(table),
|
|
3305
|
+
// In a composed namespace, an imported table's read policy (inlined by
|
|
3306
|
+
// visible()) is checked against the namespace that owns it.
|
|
3307
|
+
...normalized.imports?.length ? { policyNamespace: (table) => owners.get(table)?.namespace } : {}
|
|
3308
|
+
};
|
|
3214
3309
|
for (const table of relations) {
|
|
3215
3310
|
const isView = viewNames.has(table.table);
|
|
3216
3311
|
const compilePolicyFor = (access, store) => {
|
|
@@ -3219,7 +3314,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3219
3314
|
return;
|
|
3220
3315
|
}
|
|
3221
3316
|
try {
|
|
3222
|
-
const pred = compilePolicy({ expression, table: table.table, resolver });
|
|
3317
|
+
const pred = compilePolicy({ expression, table: table.table, resolver, access });
|
|
3223
3318
|
store.set(table.table, normalized.imports?.length ? scopePolicy(pred, owners.get(table.table).namespace) : pred);
|
|
3224
3319
|
} catch (error) {
|
|
3225
3320
|
if (error instanceof PolicyCompileError || error instanceof PolicyParseError) {
|
|
@@ -3239,7 +3334,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3239
3334
|
compilePolicyFor("write", writePolicies);
|
|
3240
3335
|
compilePolicyFor("insert", insertPolicies);
|
|
3241
3336
|
compilePolicyFor("update", updatePolicies);
|
|
3242
|
-
if (isView && table.resolved === void 0) {
|
|
3337
|
+
if (isView && table.resolved === void 0 || unkeyedSyncedTableNames.has(table.table)) {
|
|
3243
3338
|
writePolicies.set(table.table, CONST_FALSE);
|
|
3244
3339
|
insertPolicies.set(table.table, CONST_FALSE);
|
|
3245
3340
|
updatePolicies.set(table.table, CONST_FALSE);
|
|
@@ -3314,7 +3409,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3314
3409
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
3315
3410
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
3316
3411
|
for (const table of relations) {
|
|
3317
|
-
if (table.internal === true) continue;
|
|
3412
|
+
if (table.internal === true || unkeyedSyncedTableNames.has(table.table)) continue;
|
|
3318
3413
|
const roots = queryRootNames(table.table);
|
|
3319
3414
|
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
3320
3415
|
if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
|
|
@@ -3694,8 +3789,10 @@ var OperationLowerer = class {
|
|
|
3694
3789
|
constructor(ctx, restricted) {
|
|
3695
3790
|
this.ctx = ctx;
|
|
3696
3791
|
this.variables = new Map(restricted.variables.map((v) => [v.name, v]));
|
|
3792
|
+
this.maxRows = restricted.readBudget ?? MAX_LIMIT;
|
|
3697
3793
|
}
|
|
3698
3794
|
variables;
|
|
3795
|
+
maxRows;
|
|
3699
3796
|
meta(table) {
|
|
3700
3797
|
return this.ctx.tables[table];
|
|
3701
3798
|
}
|
|
@@ -3899,13 +3996,13 @@ var OperationLowerer = class {
|
|
|
3899
3996
|
});
|
|
3900
3997
|
}
|
|
3901
3998
|
lowerLimit(node) {
|
|
3902
|
-
const base2 = { default: DEFAULT_LIMIT, max:
|
|
3999
|
+
const base2 = { default: DEFAULT_LIMIT, max: this.maxRows };
|
|
3903
4000
|
if (!node) return base2;
|
|
3904
4001
|
if (node.kind === Kind.VARIABLE) return { ...base2, variable: node.name.value };
|
|
3905
4002
|
if (node.kind === Kind.INT) {
|
|
3906
4003
|
const value = Number(node.value);
|
|
3907
4004
|
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
3908
|
-
return { ...base2, value: Math.min(value,
|
|
4005
|
+
return { ...base2, value: Math.min(value, this.maxRows) };
|
|
3909
4006
|
}
|
|
3910
4007
|
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
3911
4008
|
}
|
|
@@ -4704,7 +4801,13 @@ var OperationLowerer = class {
|
|
|
4704
4801
|
};
|
|
4705
4802
|
function lowerOperation({ restricted, ctx }) {
|
|
4706
4803
|
const lowerer = new OperationLowerer(ctx, restricted);
|
|
4707
|
-
|
|
4804
|
+
const roots = lowerer.lowerRoots(restricted.definition, restricted.kind);
|
|
4805
|
+
if (restricted.readBudget === void 0) return roots;
|
|
4806
|
+
const root = roots[0];
|
|
4807
|
+
if (restricted.kind !== "query" || restricted.definition.selectionSet.selections.length !== 1 || roots.length !== 1 || root?.kind !== "select" || root.selection.relations.length > 0 || root.selection.aggregates.length > 0 || root.selection.cells !== void 0 || root.prefetch?.length) {
|
|
4808
|
+
fail2("@readBudget requires one flat select root without related selections, aggregates, cells, or prefetch", "invalid-read-budget");
|
|
4809
|
+
}
|
|
4810
|
+
return roots;
|
|
4708
4811
|
}
|
|
4709
4812
|
function isCellColumn(meta, name2) {
|
|
4710
4813
|
if (!meta.cells) return false;
|
|
@@ -4763,6 +4866,19 @@ function parseDocuments({
|
|
|
4763
4866
|
}
|
|
4764
4867
|
return { documents: parsed, diagnostics };
|
|
4765
4868
|
}
|
|
4869
|
+
function parseReadBudget(operation) {
|
|
4870
|
+
const directives = operation.directives?.filter((directive) => directive.name.value === "readBudget") ?? [];
|
|
4871
|
+
if (directives.length === 0) return void 0;
|
|
4872
|
+
const value = directives[0]?.arguments?.find((argument) => argument.name.value === "maxRows")?.value;
|
|
4873
|
+
if (operation.operation !== "query" || directives.length !== 1 || value?.kind !== Kind.INT) {
|
|
4874
|
+
throw new DocumentCompileError("@readBudget requires a literal maxRows on a query", "invalid-read-budget");
|
|
4875
|
+
}
|
|
4876
|
+
const maxRows = Number(value.value);
|
|
4877
|
+
if (!Number.isSafeInteger(maxRows) || maxRows < 1 || maxRows > MAX_PERSISTED_READ_ROWS) {
|
|
4878
|
+
throw new DocumentCompileError(`@readBudget(maxRows:) must be between 1 and ${MAX_PERSISTED_READ_ROWS}`, "invalid-read-budget");
|
|
4879
|
+
}
|
|
4880
|
+
return maxRows;
|
|
4881
|
+
}
|
|
4766
4882
|
function unwrapType(node, variable) {
|
|
4767
4883
|
let nullable = true;
|
|
4768
4884
|
let list2 = false;
|
|
@@ -4923,6 +5039,7 @@ function restrictOperation({
|
|
|
4923
5039
|
name: name2,
|
|
4924
5040
|
kind: operation.operation === "mutation" ? "mutation" : "query",
|
|
4925
5041
|
requires: parseRequires(operation),
|
|
5042
|
+
readBudget: parseReadBudget(operation),
|
|
4926
5043
|
variables,
|
|
4927
5044
|
definition: operation
|
|
4928
5045
|
};
|
|
@@ -5580,6 +5697,118 @@ function bindNamedParameters({ sql: sql4, names }) {
|
|
|
5580
5697
|
}
|
|
5581
5698
|
return result;
|
|
5582
5699
|
}
|
|
5700
|
+
var parser2;
|
|
5701
|
+
var functions = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
5702
|
+
var scalarTypes = /* @__PURE__ */ new Set([
|
|
5703
|
+
"bool",
|
|
5704
|
+
"boolean",
|
|
5705
|
+
"int2",
|
|
5706
|
+
"int4",
|
|
5707
|
+
"int8",
|
|
5708
|
+
"integer",
|
|
5709
|
+
"bigint",
|
|
5710
|
+
"smallint",
|
|
5711
|
+
"numeric",
|
|
5712
|
+
"decimal",
|
|
5713
|
+
"float4",
|
|
5714
|
+
"float8",
|
|
5715
|
+
"real",
|
|
5716
|
+
"text",
|
|
5717
|
+
"varchar",
|
|
5718
|
+
"bpchar",
|
|
5719
|
+
"uuid",
|
|
5720
|
+
"date",
|
|
5721
|
+
"timestamp",
|
|
5722
|
+
"timestamptz",
|
|
5723
|
+
"time",
|
|
5724
|
+
"timetz",
|
|
5725
|
+
"interval",
|
|
5726
|
+
"json",
|
|
5727
|
+
"jsonb",
|
|
5728
|
+
"bytea"
|
|
5729
|
+
]);
|
|
5730
|
+
var sqlValues = /* @__PURE__ */ new Set([
|
|
5731
|
+
"SVFOP_CURRENT_DATE",
|
|
5732
|
+
"SVFOP_CURRENT_TIME",
|
|
5733
|
+
"SVFOP_CURRENT_TIME_N",
|
|
5734
|
+
"SVFOP_CURRENT_TIMESTAMP",
|
|
5735
|
+
"SVFOP_CURRENT_TIMESTAMP_N",
|
|
5736
|
+
"SVFOP_LOCALTIME",
|
|
5737
|
+
"SVFOP_LOCALTIME_N",
|
|
5738
|
+
"SVFOP_LOCALTIMESTAMP",
|
|
5739
|
+
"SVFOP_LOCALTIMESTAMP_N",
|
|
5740
|
+
"SVFOP_CURRENT_ROLE",
|
|
5741
|
+
"SVFOP_CURRENT_USER",
|
|
5742
|
+
"SVFOP_USER",
|
|
5743
|
+
"SVFOP_SESSION_USER",
|
|
5744
|
+
"SVFOP_CURRENT_CATALOG",
|
|
5745
|
+
"SVFOP_CURRENT_SCHEMA"
|
|
5746
|
+
]);
|
|
5747
|
+
async function validateManagedViewQueries({ canonical }) {
|
|
5748
|
+
for (const definition of [canonical, ...canonical.imports ?? []]) {
|
|
5749
|
+
for (const view of definition.views ?? []) {
|
|
5750
|
+
const fail4 = (message) => {
|
|
5751
|
+
throw new OrmDefinitionError(`managed view '${definition.namespace}/${view.view}': ${message}`);
|
|
5752
|
+
};
|
|
5753
|
+
parser2 ??= new PgParser({ version: 17 });
|
|
5754
|
+
const parsed = await parser2.parse(view.query);
|
|
5755
|
+
if (parsed.error || !parsed.tree) fail4("invalid SELECT query");
|
|
5756
|
+
const statements = list(obj(parsed.tree).stmts);
|
|
5757
|
+
if (statements.length !== 1 || !obj(obj(statements[0]).stmt).SelectStmt) fail4("one SELECT query is required");
|
|
5758
|
+
validateTree({ tree: parsed.tree, schema: ormSchemaName(definition.namespace), enums: definition.enums.map((entry) => entry.name), fail: fail4 });
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
function pgCatalogName(parts) {
|
|
5763
|
+
if (parts.length === 1) return parts[0];
|
|
5764
|
+
if (parts.length === 2 && parts[0] === "pg_catalog") return parts[1];
|
|
5765
|
+
return void 0;
|
|
5766
|
+
}
|
|
5767
|
+
function reviewedFunctionName({ parts, format }) {
|
|
5768
|
+
return format === "COERCE_SQL_SYNTAX" && parts.length === 2 && parts[0] === "pg_catalog" && parts[1] === "position" ? "strpos" : pgCatalogName(parts);
|
|
5769
|
+
}
|
|
5770
|
+
function validateTree({ tree, ...scope }) {
|
|
5771
|
+
const pending = [tree];
|
|
5772
|
+
while (pending.length) {
|
|
5773
|
+
const value = pending.pop();
|
|
5774
|
+
if (Array.isArray(value)) {
|
|
5775
|
+
pending.push(...value);
|
|
5776
|
+
continue;
|
|
5777
|
+
}
|
|
5778
|
+
if (!value || typeof value !== "object") continue;
|
|
5779
|
+
const node = obj(value);
|
|
5780
|
+
for (const [kind, child] of Object.entries(node)) {
|
|
5781
|
+
validateNode({ kind, child, scope });
|
|
5782
|
+
pending.push(child);
|
|
5783
|
+
}
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
function validateNode({ kind, child, scope }) {
|
|
5787
|
+
const { fail: fail4 } = scope;
|
|
5788
|
+
if (kind.endsWith("Stmt") && kind !== "SelectStmt") fail4("only SELECT statements are permitted");
|
|
5789
|
+
if (kind === "FuncCall") {
|
|
5790
|
+
const call = obj(child);
|
|
5791
|
+
const parts = strings(call.funcname);
|
|
5792
|
+
const name2 = reviewedFunctionName({ parts, format: call.funcformat });
|
|
5793
|
+
if (!name2 || !functions.has(name2)) fail4(`only explicitly allowlisted pg_catalog functions are permitted (found '${parts.join(".")}')`);
|
|
5794
|
+
}
|
|
5795
|
+
if (kind === "typeName" || kind === "TypeName") validateType({ node: obj(child), ...scope });
|
|
5796
|
+
if (kind === "A_Expr" && pgCatalogName(strings(obj(child).name)) === void 0) fail4("only pg_catalog operators are permitted");
|
|
5797
|
+
if (kind === "SortBy" && list(obj(child).useOp).length && pgCatalogName(strings(obj(child).useOp)) === void 0) fail4("only pg_catalog sort operators are permitted");
|
|
5798
|
+
if (kind === "SQLValueFunction" && !sqlValues.has(String(obj(child).op))) fail4("unsupported SQL value expression");
|
|
5799
|
+
if (kind === "ParamRef") fail4("view queries cannot contain parameters");
|
|
5800
|
+
if (kind === "A_Indirection" && list(obj(child).indirection).some((entry) => !obj(entry).A_Indices)) {
|
|
5801
|
+
fail4("composite field notation is not permitted in managed views");
|
|
5802
|
+
}
|
|
5803
|
+
}
|
|
5804
|
+
function validateType({ node, schema, enums, fail: fail4 }) {
|
|
5805
|
+
const parts = strings(node.names);
|
|
5806
|
+
const builtin = pgCatalogName(parts);
|
|
5807
|
+
const managedEnum = parts.length === 1 && enums.includes(parts[0]) || parts.length === 2 && parts[0] === schema && enums.includes(parts[1]);
|
|
5808
|
+
if (node.setof || node.pct_type || !managedEnum && (!builtin || !scalarTypes.has(builtin))) {
|
|
5809
|
+
fail4(`casts may only use supported pg_catalog scalar types or declared enums (found '${parts.join(".")}')`);
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5583
5812
|
|
|
5584
5813
|
// src/sql/operations.ts
|
|
5585
5814
|
function isSqlOperationDocument(path, contents) {
|
|
@@ -5595,7 +5824,13 @@ function isSqlOperationDocument(path, contents) {
|
|
|
5595
5824
|
async function compileOperationDocuments(input) {
|
|
5596
5825
|
const sqlDocuments = input.documents.filter((d) => isSqlOperationDocument(d.path, d.contents));
|
|
5597
5826
|
const result = compileNamespace({ ...input, documents: input.documents.filter((d) => !isSqlOperationDocument(d.path, d.contents)) });
|
|
5598
|
-
if (!result.ok
|
|
5827
|
+
if (!result.ok) return result;
|
|
5828
|
+
const viewDiagnostics = await managedViewDiagnostics(input.canonical);
|
|
5829
|
+
if (viewDiagnostics.length) return { ok: false, diagnostics: [...result.diagnostics, ...viewDiagnostics] };
|
|
5830
|
+
if (!sqlDocuments.length) return result;
|
|
5831
|
+
return compileSqlDocuments({ input, result, sqlDocuments });
|
|
5832
|
+
}
|
|
5833
|
+
async function compileSqlDocuments({ input, result, sqlDocuments }) {
|
|
5599
5834
|
for (const document of sqlDocuments) {
|
|
5600
5835
|
try {
|
|
5601
5836
|
const definition = defineOperation(JSON.parse(document.contents));
|
|
@@ -5689,6 +5924,18 @@ async function compileOperationDocuments(input) {
|
|
|
5689
5924
|
result.manifest.opsHash = result.artifact.opsHash;
|
|
5690
5925
|
return { ...result, artifact: normalizeIr(result.artifact) };
|
|
5691
5926
|
}
|
|
5927
|
+
async function managedViewDiagnostics(canonical) {
|
|
5928
|
+
try {
|
|
5929
|
+
await validateManagedViewQueries({ canonical });
|
|
5930
|
+
return [];
|
|
5931
|
+
} catch (error) {
|
|
5932
|
+
return [{
|
|
5933
|
+
severity: "error",
|
|
5934
|
+
code: "invalid-view-query",
|
|
5935
|
+
message: error instanceof Error ? error.message : "invalid managed view query"
|
|
5936
|
+
}];
|
|
5937
|
+
}
|
|
5938
|
+
}
|
|
5692
5939
|
|
|
5693
5940
|
// src/sql/codegen.ts
|
|
5694
5941
|
var typeOf = (type) => ["integer", "double"].includes(type) ? "number" : type === "boolean" ? "boolean" : "string";
|
|
@@ -7784,7 +8031,7 @@ function snapshotTableNames(snapshot, schema) {
|
|
|
7784
8031
|
// src/engine/materialized-refresh.ts
|
|
7785
8032
|
function materializedRefreshFunctions(canonical) {
|
|
7786
8033
|
const schema = ormSchemaName(canonical.namespace);
|
|
7787
|
-
const
|
|
8034
|
+
const functions2 = /* @__PURE__ */ new Map();
|
|
7788
8035
|
for (const view of canonical.views ?? []) {
|
|
7789
8036
|
const materialized = view.resolved?.materialized;
|
|
7790
8037
|
if (!materialized) continue;
|
|
@@ -7793,7 +8040,7 @@ function materializedRefreshFunctions(canonical) {
|
|
|
7793
8040
|
if (!table || pk.length === 0) {
|
|
7794
8041
|
throw new OrmDefinitionError(`materialized resolved table '${view.view}' needs its table and primary key`);
|
|
7795
8042
|
}
|
|
7796
|
-
|
|
8043
|
+
functions2.set(view.view, refreshFunctionSql({
|
|
7797
8044
|
schema,
|
|
7798
8045
|
name: view.view,
|
|
7799
8046
|
table: materialized.table,
|
|
@@ -7802,7 +8049,7 @@ function materializedRefreshFunctions(canonical) {
|
|
|
7802
8049
|
pk
|
|
7803
8050
|
}));
|
|
7804
8051
|
}
|
|
7805
|
-
return
|
|
8052
|
+
return functions2;
|
|
7806
8053
|
}
|
|
7807
8054
|
function refreshFunctionSql({ schema, name: name2, table, compute, columns, pk }) {
|
|
7808
8055
|
const fn = `${quoteIdentifier(schema)}.${quoteIdentifier(materializedRefreshFunctionName(name2))}`;
|
|
@@ -8446,6 +8693,7 @@ async function planWithViewRecreation({
|
|
|
8446
8693
|
previousSnapshot,
|
|
8447
8694
|
viewRecreation
|
|
8448
8695
|
}) {
|
|
8696
|
+
await validateManagedViewQueries({ canonical });
|
|
8449
8697
|
const schema = ormSchemaName(canonical.namespace);
|
|
8450
8698
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
8451
8699
|
assertValidViewDependencies(canonical);
|
|
@@ -9736,6 +9984,9 @@ function generateSdl(canonical, options = {}) {
|
|
|
9736
9984
|
blocks.push(
|
|
9737
9985
|
'"""Gates an operation behind a platform capability or custom role \u2014 checked by the data plane before execution."""\ndirective @requires(capability: String, role: String) on QUERY | MUTATION'
|
|
9738
9986
|
);
|
|
9987
|
+
if (!options.omitReadBudgetDirective) blocks.push(
|
|
9988
|
+
'"""Sets a document-authored ceiling of 1\u201350001 rows for one flat select root. Runtime variables cannot raise it."""\ndirective @readBudget(maxRows: Int!) on QUERY'
|
|
9989
|
+
);
|
|
9739
9990
|
blocks.push("directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT");
|
|
9740
9991
|
blocks.push(
|
|
9741
9992
|
'"""Abort the entire mutation unless this root affects exactly the document-authored number of rows. Mutation roots only; cannot be conditional."""\ndirective @assertAffectedRows(exactly: Int!) on FIELD'
|
|
@@ -11940,7 +12191,7 @@ function emitSyncedTable(table) {
|
|
|
11940
12191
|
lines.push(" },", " {");
|
|
11941
12192
|
lines.push(` source: ${quote(table.source ?? "")},`);
|
|
11942
12193
|
if (table.description !== void 0) lines.push(` description: ${quote(table.description)},`);
|
|
11943
|
-
if (singlePrimaryKey === void 0 && table.primaryKey
|
|
12194
|
+
if (singlePrimaryKey === void 0 && table.primaryKey !== void 0) {
|
|
11944
12195
|
lines.push(` primaryKey: ${jsonLiteral(table.primaryKey, 4)},`);
|
|
11945
12196
|
}
|
|
11946
12197
|
if (table.links.length > 0) {
|