@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.cjs
CHANGED
|
@@ -59,18 +59,22 @@ var init_rehearsal_poll = __esm({
|
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
// src/constants.ts
|
|
62
|
-
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
62
|
+
var NAMESPACE_PATTERN = /^(?!(?:events|native_sources)$)[a-z][a-z0-9_]{0,40}$/;
|
|
63
63
|
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
64
64
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
65
65
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
66
66
|
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
67
|
+
var MAX_PERSISTED_READ_ROWS = 50001;
|
|
67
68
|
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
68
69
|
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
|
|
69
70
|
var SYNCED_SOURCE_PATTERN = QUALIFIED_TABLE_PATTERN;
|
|
70
71
|
var ORM_SCHEMA_PREFIX = "orm_";
|
|
71
72
|
var CONSUMER_ROLE_SUFFIX = "_consumer";
|
|
72
|
-
var ormSchemaName = (namespace) =>
|
|
73
|
-
|
|
73
|
+
var ormSchemaName = (namespace) => {
|
|
74
|
+
if (!NAMESPACE_PATTERN.test(namespace)) throw new Error(`invalid or reserved ORM namespace '${namespace}'`);
|
|
75
|
+
return `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
76
|
+
};
|
|
77
|
+
var consumerRoleName = (namespace) => `${ormSchemaName(namespace)}${CONSUMER_ROLE_SUFFIX}`;
|
|
74
78
|
var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
75
79
|
var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
|
|
76
80
|
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
@@ -538,6 +542,7 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
538
542
|
function validateSource({ scope, source, table }) {
|
|
539
543
|
const { fail: fail4 } = scope;
|
|
540
544
|
if (source.kind === "etl" && table.source === void 0) fail4(`etl source '${source.name}' must reference a synced table`);
|
|
545
|
+
if (source.kind === "etl" && table.primaryKey?.length === 0) fail4(`etl source '${source.name}' cannot use an unkeyed synced table for resolved row identity`);
|
|
541
546
|
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
542
547
|
fail4(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
543
548
|
}
|
|
@@ -986,7 +991,7 @@ var canonicalTableSchema = zod.z.object({
|
|
|
986
991
|
/** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
|
|
987
992
|
source: zod.z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
|
|
988
993
|
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
989
|
-
primaryKey: zod.z.array(columnIdentifier).
|
|
994
|
+
primaryKey: zod.z.array(columnIdentifier).max(8).optional(),
|
|
990
995
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
991
996
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
992
997
|
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
@@ -1022,6 +1027,29 @@ var canonicalTableSchema = zod.z.object({
|
|
|
1022
1027
|
* data plane. Omitted otherwise.
|
|
1023
1028
|
*/
|
|
1024
1029
|
materializes: identifier2.optional()
|
|
1030
|
+
}).superRefine((table, ctx) => {
|
|
1031
|
+
if (table.primaryKey?.length !== 0) return;
|
|
1032
|
+
const fail4 = (message, path = ["primaryKey"]) => ctx.addIssue({ code: "custom", path, message: `unkeyed table '${table.table}' ${message}` });
|
|
1033
|
+
if (table.source === void 0 || table.columns.length === 0) {
|
|
1034
|
+
fail4("requires a typed synced source");
|
|
1035
|
+
}
|
|
1036
|
+
if ([table.policies.write, table.policies.insert, table.policies.update].some((policy) => policy !== void 0)) {
|
|
1037
|
+
fail4("is read-only and cannot declare write policies", ["policies"]);
|
|
1038
|
+
}
|
|
1039
|
+
const ownedCollections = [table.indexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
|
|
1040
|
+
const ownedMetadata = [table.cellMetadata, table.internal, table.materializes, table.renamedFrom];
|
|
1041
|
+
if (ownedCollections.some((items) => items !== void 0 && items.length > 0) || ownedMetadata.some((value) => value !== void 0)) {
|
|
1042
|
+
fail4("cannot declare owned storage, constraints or identity operations");
|
|
1043
|
+
}
|
|
1044
|
+
for (const [index3, column] of table.columns.entries()) {
|
|
1045
|
+
const ownedColumnMetadata = [column.default, column.defaultFn, column.onUpdateFn, column.renamedFrom, column.cast, column.cell];
|
|
1046
|
+
if (column.unique || ownedColumnMetadata.some((value) => value !== void 0)) {
|
|
1047
|
+
fail4("cannot declare column uniqueness, mutation defaults or migration metadata", ["columns", index3]);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
for (const [index3, link] of table.links.entries()) {
|
|
1051
|
+
if (link.enforce || link.kind === "toMany") fail4("cannot provide an identity for this link", ["links", index3]);
|
|
1052
|
+
}
|
|
1025
1053
|
});
|
|
1026
1054
|
var MAX_VIEW_QUERY = 1e5;
|
|
1027
1055
|
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
@@ -1078,6 +1106,26 @@ var canonicalViewSchema = zod.z.object({
|
|
|
1078
1106
|
}
|
|
1079
1107
|
}
|
|
1080
1108
|
});
|
|
1109
|
+
function validateUnkeyedLinkIdentities({ definition, ctx }) {
|
|
1110
|
+
const tables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1111
|
+
const relations = [
|
|
1112
|
+
...definition.tables.map((table, index3) => ({ kind: "tables", index: index3, links: table.links })),
|
|
1113
|
+
...(definition.views ?? []).map((view, index3) => ({ kind: "views", index: index3, links: view.links }))
|
|
1114
|
+
];
|
|
1115
|
+
for (const relation of relations) {
|
|
1116
|
+
for (const [linkIndex, link] of relation.links.entries()) {
|
|
1117
|
+
if (link.kind !== "toOne" || link.namespace !== void 0 && link.namespace !== definition.namespace) continue;
|
|
1118
|
+
const target = link.target === void 0 ? void 0 : tables.get(link.target);
|
|
1119
|
+
if (target?.source !== void 0 && target.primaryKey?.length === 0) {
|
|
1120
|
+
ctx.addIssue({
|
|
1121
|
+
code: "custom",
|
|
1122
|
+
path: [relation.kind, relation.index, "links", linkIndex],
|
|
1123
|
+
message: `link '${link.name}' cannot use unkeyed table '${target.table}' as a to-one identity`
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1081
1129
|
var CELL_PART_TYPES = {
|
|
1082
1130
|
at: "timestamptz",
|
|
1083
1131
|
by: "text",
|
|
@@ -1141,7 +1189,7 @@ var exportGateSchema = zod.z.union([zod.z.literal(true), zod.z.object({ capabili
|
|
|
1141
1189
|
var relationExportSchema = zod.z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
|
|
1142
1190
|
var localDefinitionSchema = zod.z.object({
|
|
1143
1191
|
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
1144
|
-
namespace: identifier2.max(41),
|
|
1192
|
+
namespace: identifier2.max(41).regex(NAMESPACE_PATTERN, "invalid or reserved ORM namespace"),
|
|
1145
1193
|
description: zod.z.string().max(2e3).optional(),
|
|
1146
1194
|
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
1147
1195
|
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -1175,6 +1223,7 @@ var localDefinitionSchema = zod.z.object({
|
|
|
1175
1223
|
}
|
|
1176
1224
|
}
|
|
1177
1225
|
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1226
|
+
validateUnkeyedLinkIdentities({ definition, ctx });
|
|
1178
1227
|
const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
|
|
1179
1228
|
for (const name2 of Object.keys(definition.exports ?? {})) {
|
|
1180
1229
|
if (!relations.has(name2)) ctx.addIssue({ code: "custom", path: ["exports", name2], message: `export '${name2}' is not a local object` });
|
|
@@ -1497,7 +1546,7 @@ var limitIrSchema = zod.z.object({
|
|
|
1497
1546
|
variable: zod.z.string().optional(),
|
|
1498
1547
|
value: zod.z.number().int().positive().optional(),
|
|
1499
1548
|
default: zod.z.number().int().positive(),
|
|
1500
|
-
max: zod.z.number().int().positive()
|
|
1549
|
+
max: zod.z.number().int().positive().max(MAX_PERSISTED_READ_ROWS)
|
|
1501
1550
|
});
|
|
1502
1551
|
var selectionIrSchema = zod.z.lazy(
|
|
1503
1552
|
() => zod.z.object({
|
|
@@ -2180,6 +2229,7 @@ var Parser2 = class {
|
|
|
2180
2229
|
return node;
|
|
2181
2230
|
}
|
|
2182
2231
|
if (this.peekKeyword("exists")) return this.parseExists();
|
|
2232
|
+
if (this.peekKeyword("visible") && this.tokens[this.index + 1]?.kind === "lparen") return this.parseVisible();
|
|
2183
2233
|
return this.parseComparison();
|
|
2184
2234
|
}
|
|
2185
2235
|
parseBooleanCall(fn) {
|
|
@@ -2199,6 +2249,13 @@ var Parser2 = class {
|
|
|
2199
2249
|
this.expect("rparen", "')' closing exists(...)");
|
|
2200
2250
|
return { kind: "exists", path, where };
|
|
2201
2251
|
}
|
|
2252
|
+
parseVisible() {
|
|
2253
|
+
this.advance();
|
|
2254
|
+
this.expect("lparen", "'(' after visible");
|
|
2255
|
+
const path = this.parsePath("a link path in visible(...)");
|
|
2256
|
+
this.expect("rparen", "')' closing visible(...)");
|
|
2257
|
+
return { kind: "visible", path };
|
|
2258
|
+
}
|
|
2202
2259
|
parseComparison() {
|
|
2203
2260
|
const left = this.parseOperand();
|
|
2204
2261
|
const operator = this.expect("operator", "a comparison operator");
|
|
@@ -2314,6 +2371,24 @@ function parsePolicy(source) {
|
|
|
2314
2371
|
|
|
2315
2372
|
// src/predicate-v2/compile.ts
|
|
2316
2373
|
var POLICY_MAX_LINK_DEPTH = 3;
|
|
2374
|
+
function scopeToNamespace(pred, namespace) {
|
|
2375
|
+
switch (pred.kind) {
|
|
2376
|
+
case "capability":
|
|
2377
|
+
case "role":
|
|
2378
|
+
return pred.namespace ? pred : { ...pred, namespace };
|
|
2379
|
+
case "and":
|
|
2380
|
+
case "or":
|
|
2381
|
+
return { ...pred, operands: pred.operands.map((operand) => scopeToNamespace(operand, namespace)) };
|
|
2382
|
+
case "not":
|
|
2383
|
+
return { ...pred, operand: scopeToNamespace(pred.operand, namespace) };
|
|
2384
|
+
case "exists":
|
|
2385
|
+
return { ...pred, where: scopeToNamespace(pred.where, namespace) };
|
|
2386
|
+
case "aggPredicate":
|
|
2387
|
+
return pred.where ? { ...pred, where: scopeToNamespace(pred.where, namespace) } : pred;
|
|
2388
|
+
default:
|
|
2389
|
+
return pred;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2317
2392
|
var CMP_BY_OP = {
|
|
2318
2393
|
"=": "eq",
|
|
2319
2394
|
"!=": "neq",
|
|
@@ -2347,7 +2422,7 @@ function resolveLink(info, ref) {
|
|
|
2347
2422
|
function columnCandidates(info) {
|
|
2348
2423
|
return [...info.columns.map((c) => c.name), ...info.links.map((l) => l.name)];
|
|
2349
2424
|
}
|
|
2350
|
-
var PolicyCompiler = class {
|
|
2425
|
+
var PolicyCompiler = class _PolicyCompiler {
|
|
2351
2426
|
constructor(ctx) {
|
|
2352
2427
|
this.ctx = ctx;
|
|
2353
2428
|
}
|
|
@@ -2360,13 +2435,15 @@ var PolicyCompiler = class {
|
|
|
2360
2435
|
return { kind: node.kind, operands: node.operands.map((operand) => this.compile(operand, table, depth)) };
|
|
2361
2436
|
case "not":
|
|
2362
2437
|
assertNoNegatedExternalClaim(node.operand);
|
|
2363
|
-
return { kind: "not", operand: this.compile(node.operand, table, depth) };
|
|
2438
|
+
return { kind: "not", operand: new _PolicyCompiler({ ...this.ctx, negated: true }).compile(node.operand, table, depth) };
|
|
2364
2439
|
case "capability":
|
|
2365
2440
|
return { kind: "capability", key: node.key };
|
|
2366
2441
|
case "role":
|
|
2367
2442
|
return { kind: "role", key: node.key };
|
|
2368
2443
|
case "exists":
|
|
2369
2444
|
return this.compileExists(node.path, node.where, table, depth);
|
|
2445
|
+
case "visible":
|
|
2446
|
+
return this.compileVisible(node.path, table, depth);
|
|
2370
2447
|
case "compare":
|
|
2371
2448
|
return this.compileCompare(node, table, depth);
|
|
2372
2449
|
}
|
|
@@ -2380,6 +2457,36 @@ var PolicyCompiler = class {
|
|
|
2380
2457
|
}
|
|
2381
2458
|
return pred;
|
|
2382
2459
|
}
|
|
2460
|
+
/** `visible(path)` → `exists(path, <final target's read policy>)`. */
|
|
2461
|
+
compileVisible(path, table, depth) {
|
|
2462
|
+
const { links, finalTable } = this.resolveLinkPath(path, table, depth, { toOneOnly: false });
|
|
2463
|
+
const readPolicy = this.ctx.resolver.readPolicy;
|
|
2464
|
+
if (!readPolicy) {
|
|
2465
|
+
throw new PolicyCompileError(
|
|
2466
|
+
`visible(${path.join(".")}) needs the target's read policy, which this compile context does not provide`,
|
|
2467
|
+
"visible-unresolvable"
|
|
2468
|
+
);
|
|
2469
|
+
}
|
|
2470
|
+
if (this.ctx.visiting.includes(finalTable)) {
|
|
2471
|
+
throw new PolicyCompileError(
|
|
2472
|
+
`visible(${path.join(".")}) on '${table}' reaches '${finalTable}' whose read policy is already being expanded (${[...this.ctx.visiting, finalTable].join(" \u2192 ")})`,
|
|
2473
|
+
"visible-cycle"
|
|
2474
|
+
);
|
|
2475
|
+
}
|
|
2476
|
+
const expression = readPolicy(finalTable);
|
|
2477
|
+
let pred = expression === void 0 ? { kind: "const", value: true } : compilePolicyAt({
|
|
2478
|
+
expression,
|
|
2479
|
+
table: finalTable,
|
|
2480
|
+
ctx: { ...this.ctx, visiting: [...this.ctx.visiting, finalTable] },
|
|
2481
|
+
depth: depth + links.length
|
|
2482
|
+
});
|
|
2483
|
+
const owner = this.ctx.resolver.policyNamespace?.(finalTable);
|
|
2484
|
+
if (owner) pred = scopeToNamespace(pred, owner);
|
|
2485
|
+
for (let i = links.length - 1; i >= 0; i -= 1) {
|
|
2486
|
+
pred = { kind: "exists", link: links[i].name, where: pred };
|
|
2487
|
+
}
|
|
2488
|
+
return pred;
|
|
2489
|
+
}
|
|
2383
2490
|
resolveLinkPath(path, table, depth, { toOneOnly }) {
|
|
2384
2491
|
const links = [];
|
|
2385
2492
|
let current = table;
|
|
@@ -2440,6 +2547,7 @@ var PolicyCompiler = class {
|
|
|
2440
2547
|
);
|
|
2441
2548
|
}
|
|
2442
2549
|
if (valueSide.kind === "claim" && isExternalPolicyClaimKey(valueSide.key)) {
|
|
2550
|
+
if (this.ctx.negated) throw negatedExternalClaim(valueSide.key);
|
|
2443
2551
|
assertExternalClaimComparison({ op, key: valueSide.key, column: info.columns.find((c) => c.name === column) });
|
|
2444
2552
|
}
|
|
2445
2553
|
let cmp = this.buildCmp(op, column, valueSide);
|
|
@@ -2477,22 +2585,22 @@ function assertExternalClaimComparison({ op, key, column }) {
|
|
|
2477
2585
|
);
|
|
2478
2586
|
}
|
|
2479
2587
|
}
|
|
2588
|
+
function negatedExternalClaim(key) {
|
|
2589
|
+
return new PolicyCompileError(
|
|
2590
|
+
`claim('${key}') cannot appear under not(): a negated borrower comparison admits other borrowers' rows`,
|
|
2591
|
+
"unsupported-comparison"
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2480
2594
|
function assertNoNegatedExternalClaim(operand) {
|
|
2481
2595
|
walkPolicy(operand, (node) => {
|
|
2482
2596
|
if (node.kind !== "compare") return;
|
|
2483
2597
|
const claim = [node.left, node.right].find((side) => side.kind === "claim" && isExternalPolicyClaimKey(side.key));
|
|
2484
|
-
if (claim?.kind === "claim")
|
|
2485
|
-
throw new PolicyCompileError(
|
|
2486
|
-
`claim('${claim.key}') cannot appear under not(): a negated borrower comparison admits other borrowers' rows`,
|
|
2487
|
-
"unsupported-comparison"
|
|
2488
|
-
);
|
|
2489
|
-
}
|
|
2598
|
+
if (claim?.kind === "claim") throw negatedExternalClaim(claim.key);
|
|
2490
2599
|
});
|
|
2491
2600
|
}
|
|
2492
|
-
function
|
|
2601
|
+
function compilePredicateAt({ expression, table, ctx, depth }) {
|
|
2493
2602
|
const parsed = typeof expression === "string" ? parsePolicy(expression) : expression;
|
|
2494
|
-
|
|
2495
|
-
return compiler.compile(parsed.ast, table, 0);
|
|
2603
|
+
return new PolicyCompiler(ctx).compile(parsed.ast, table, depth);
|
|
2496
2604
|
}
|
|
2497
2605
|
function assertClaimsOnlyWhen(parsed) {
|
|
2498
2606
|
walkPolicy(parsed.ast, (node) => {
|
|
@@ -2509,25 +2617,34 @@ function assertClaimsOnlyWhen(parsed) {
|
|
|
2509
2617
|
"when-not-claims-only"
|
|
2510
2618
|
);
|
|
2511
2619
|
}
|
|
2512
|
-
if (node.kind === "exists") {
|
|
2620
|
+
if (node.kind === "exists" || node.kind === "visible") {
|
|
2513
2621
|
throw new PolicyCompileError(
|
|
2514
|
-
|
|
2622
|
+
`variant 'when' guards are claims-only \u2014 ${node.kind}() is not allowed`,
|
|
2515
2623
|
"when-not-claims-only"
|
|
2516
2624
|
);
|
|
2517
2625
|
}
|
|
2518
2626
|
});
|
|
2519
2627
|
}
|
|
2520
|
-
function
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2628
|
+
function initialVisiting({ table, access }) {
|
|
2629
|
+
return (access ?? "read") === "read" ? [table] : [];
|
|
2630
|
+
}
|
|
2631
|
+
function compilePolicy({ expression, table, resolver, maxLinkDepth, access }) {
|
|
2632
|
+
return compilePolicyAt({
|
|
2633
|
+
expression,
|
|
2634
|
+
table,
|
|
2635
|
+
depth: 0,
|
|
2636
|
+
ctx: { resolver, maxLinkDepth: maxLinkDepth ?? POLICY_MAX_LINK_DEPTH, visiting: initialVisiting({ table, access }) }
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
function compilePolicyAt({ expression, table, ctx, depth }) {
|
|
2640
|
+
if (typeof expression === "string") return compilePredicateAt({ expression, table, ctx, depth });
|
|
2641
|
+
const defaultPred = compilePredicateAt({ expression: expression.default, table, ctx, depth });
|
|
2525
2642
|
if (expression.variants.length === 0) return defaultPred;
|
|
2526
2643
|
const branches = expression.variants.map((variant) => {
|
|
2527
2644
|
const parsedWhen = parsePolicy(variant.when);
|
|
2528
2645
|
assertClaimsOnlyWhen(parsedWhen);
|
|
2529
|
-
const when =
|
|
2530
|
-
const grant = variant.allow === true ? { kind: "const", value: true } :
|
|
2646
|
+
const when = compilePredicateAt({ expression: parsedWhen, table, ctx, depth });
|
|
2647
|
+
const grant = variant.allow === true ? { kind: "const", value: true } : compilePredicateAt({ expression: variant.policy, table, ctx, depth });
|
|
2531
2648
|
return { kind: "and", operands: [when, grant] };
|
|
2532
2649
|
});
|
|
2533
2650
|
return { kind: "or", operands: [defaultPred, ...branches] };
|
|
@@ -2565,33 +2682,6 @@ function andPred(...preds) {
|
|
|
2565
2682
|
if (operands.length === 1) return operands[0];
|
|
2566
2683
|
return { kind: "and", operands };
|
|
2567
2684
|
}
|
|
2568
|
-
function walkPred(pred, visit2) {
|
|
2569
|
-
visit2(pred);
|
|
2570
|
-
switch (pred.kind) {
|
|
2571
|
-
case "and":
|
|
2572
|
-
case "or":
|
|
2573
|
-
for (const operand of pred.operands) walkPred(operand, visit2);
|
|
2574
|
-
return;
|
|
2575
|
-
case "not":
|
|
2576
|
-
walkPred(pred.operand, visit2);
|
|
2577
|
-
return;
|
|
2578
|
-
case "exists":
|
|
2579
|
-
walkPred(pred.where, visit2);
|
|
2580
|
-
return;
|
|
2581
|
-
case "aggPredicate":
|
|
2582
|
-
if (pred.where) walkPred(pred.where, visit2);
|
|
2583
|
-
return;
|
|
2584
|
-
default:
|
|
2585
|
-
return;
|
|
2586
|
-
}
|
|
2587
|
-
}
|
|
2588
|
-
function predContainsExists(pred) {
|
|
2589
|
-
let found = false;
|
|
2590
|
-
walkPred(pred, (node) => {
|
|
2591
|
-
if (node.kind === "exists" || node.kind === "aggPredicate") found = true;
|
|
2592
|
-
});
|
|
2593
|
-
return found;
|
|
2594
|
-
}
|
|
2595
2685
|
|
|
2596
2686
|
// src/compiler-gql/inject.ts
|
|
2597
2687
|
function collectPinnedColumns(writePolicy) {
|
|
@@ -2735,12 +2825,6 @@ var PolicyInjector = class {
|
|
|
2735
2825
|
if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
|
|
2736
2826
|
if (root.op === "insert" || root.op === "insertOne") {
|
|
2737
2827
|
const insertPolicy = this.insert(table);
|
|
2738
|
-
if (predContainsExists(insertPolicy)) {
|
|
2739
|
-
throw new DocumentCompileError(
|
|
2740
|
-
`table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its insert policy traverses a link (exists)`,
|
|
2741
|
-
"insert-check-unrepresentable"
|
|
2742
|
-
);
|
|
2743
|
-
}
|
|
2744
2828
|
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
2745
2829
|
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
2746
2830
|
);
|
|
@@ -2755,22 +2839,14 @@ var PolicyInjector = class {
|
|
|
2755
2839
|
const updatePolicy = this.update(table);
|
|
2756
2840
|
const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
|
|
2757
2841
|
if (root.op === "update" || root.op === "updateByPk") {
|
|
2758
|
-
|
|
2759
|
-
return { ...root, where, ...check, returning };
|
|
2842
|
+
return { ...root, where, insertCheck: updatePolicy, returning };
|
|
2760
2843
|
}
|
|
2761
2844
|
if (root.op === "cowOverride") {
|
|
2762
2845
|
const insertPolicy = this.insert(table);
|
|
2763
|
-
if (predContainsExists(insertPolicy)) {
|
|
2764
|
-
throw new DocumentCompileError(
|
|
2765
|
-
`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)`,
|
|
2766
|
-
"insert-check-unrepresentable"
|
|
2767
|
-
);
|
|
2768
|
-
}
|
|
2769
2846
|
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
2770
2847
|
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
2771
2848
|
);
|
|
2772
|
-
|
|
2773
|
-
return { ...root, pinnedColumns: pins, cowCopyCheck: insertPolicy, where, ...check, returning };
|
|
2849
|
+
return { ...root, pinnedColumns: pins, cowCopyCheck: insertPolicy, where, insertCheck: updatePolicy, returning };
|
|
2774
2850
|
}
|
|
2775
2851
|
return { ...root, where, returning };
|
|
2776
2852
|
}
|
|
@@ -3004,7 +3080,7 @@ function scopePolicy(pred, namespace) {
|
|
|
3004
3080
|
switch (pred.kind) {
|
|
3005
3081
|
case "capability":
|
|
3006
3082
|
case "role":
|
|
3007
|
-
return { ...pred, namespace };
|
|
3083
|
+
return pred.namespace ? pred : { ...pred, namespace };
|
|
3008
3084
|
case "and":
|
|
3009
3085
|
case "or":
|
|
3010
3086
|
return { ...pred, operands: pred.operands.map((p) => scopePolicy(p, namespace)) };
|
|
@@ -3094,6 +3170,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3094
3170
|
const platformMeta = usesPlatformUsers ? platformUserMeta() : void 0;
|
|
3095
3171
|
if (platformMeta) tables[PLATFORM_USER_TABLE] = platformMeta;
|
|
3096
3172
|
const syncedTableNames = new Set(canonical.tables.filter((t2) => t2.source !== void 0).map((t2) => t2.table));
|
|
3173
|
+
const unkeyedSyncedTableNames = new Set(relations.filter((table) => table.source !== void 0 && table.primaryKey?.length === 0).map((table) => table.table));
|
|
3097
3174
|
const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
|
|
3098
3175
|
const claimsGatedMemo = /* @__PURE__ */ new Map();
|
|
3099
3176
|
const relationIsClaimsGated = (name2) => {
|
|
@@ -3107,6 +3184,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3107
3184
|
};
|
|
3108
3185
|
for (const table of relations) {
|
|
3109
3186
|
const isView = viewNames.has(table.table);
|
|
3187
|
+
const readOnly = isView || unkeyedSyncedTableNames.has(table.table);
|
|
3110
3188
|
const owner = owners.get(table.table);
|
|
3111
3189
|
const entries = resolveColumnEntries(table, enums, diagnostics);
|
|
3112
3190
|
columnEntries.set(table.table, entries);
|
|
@@ -3154,13 +3232,13 @@ function buildCompileContext(rawCanonical) {
|
|
|
3154
3232
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
3155
3233
|
claimsGated: relationIsClaimsGated(table.table),
|
|
3156
3234
|
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
3157
|
-
primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
3235
|
+
primaryKey: isView || unkeyedSyncedTableNames.has(table.table) ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
3158
3236
|
links: [],
|
|
3159
3237
|
filterable: data.map((c) => c.name),
|
|
3160
3238
|
orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
3161
|
-
insertable: resolved ? resolvedWritable(data) :
|
|
3162
|
-
settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey2.includes(name2)) :
|
|
3163
|
-
appendable:
|
|
3239
|
+
insertable: resolved ? resolvedWritable(data) : readOnly ? [] : active.map((c) => c.name),
|
|
3240
|
+
settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey2.includes(name2)) : readOnly ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
3241
|
+
appendable: readOnly ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
3164
3242
|
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
3165
3243
|
...cells ? { cells } : {},
|
|
3166
3244
|
...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey2[0] }) } : {},
|
|
@@ -3184,6 +3262,16 @@ function buildCompileContext(rawCanonical) {
|
|
|
3184
3262
|
});
|
|
3185
3263
|
continue;
|
|
3186
3264
|
}
|
|
3265
|
+
const identityTable = link.kind === "toOne" ? targetName : link.kind === "toMany" ? table.table : void 0;
|
|
3266
|
+
if (identityTable !== void 0 && unkeyedSyncedTableNames.has(identityTable)) {
|
|
3267
|
+
diagnostics.push({
|
|
3268
|
+
severity: "error",
|
|
3269
|
+
code: "unkeyed-link-identity",
|
|
3270
|
+
table: table.table,
|
|
3271
|
+
message: `link '${link.name}' cannot use unkeyed table '${identityTable}' as a row identity`
|
|
3272
|
+
});
|
|
3273
|
+
continue;
|
|
3274
|
+
}
|
|
3187
3275
|
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
|
|
3188
3276
|
const onEntries = link.kind === "toMany" ? targetEntries : entries;
|
|
3189
3277
|
const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
|
|
@@ -3213,7 +3301,14 @@ function buildCompileContext(rawCanonical) {
|
|
|
3213
3301
|
const updatePolicies = /* @__PURE__ */ new Map();
|
|
3214
3302
|
const pins = /* @__PURE__ */ new Map();
|
|
3215
3303
|
const splitWritePolicyTables = /* @__PURE__ */ new Set();
|
|
3216
|
-
const
|
|
3304
|
+
const readPolicyExpressions = new Map(relations.map((relation) => [relation.table, relation.policies.read]));
|
|
3305
|
+
const resolver = {
|
|
3306
|
+
tables,
|
|
3307
|
+
readPolicy: (table) => readPolicyExpressions.get(table),
|
|
3308
|
+
// In a composed namespace, an imported table's read policy (inlined by
|
|
3309
|
+
// visible()) is checked against the namespace that owns it.
|
|
3310
|
+
...normalized.imports?.length ? { policyNamespace: (table) => owners.get(table)?.namespace } : {}
|
|
3311
|
+
};
|
|
3217
3312
|
for (const table of relations) {
|
|
3218
3313
|
const isView = viewNames.has(table.table);
|
|
3219
3314
|
const compilePolicyFor = (access, store) => {
|
|
@@ -3222,7 +3317,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3222
3317
|
return;
|
|
3223
3318
|
}
|
|
3224
3319
|
try {
|
|
3225
|
-
const pred = compilePolicy({ expression, table: table.table, resolver });
|
|
3320
|
+
const pred = compilePolicy({ expression, table: table.table, resolver, access });
|
|
3226
3321
|
store.set(table.table, normalized.imports?.length ? scopePolicy(pred, owners.get(table.table).namespace) : pred);
|
|
3227
3322
|
} catch (error) {
|
|
3228
3323
|
if (error instanceof PolicyCompileError || error instanceof PolicyParseError) {
|
|
@@ -3242,7 +3337,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3242
3337
|
compilePolicyFor("write", writePolicies);
|
|
3243
3338
|
compilePolicyFor("insert", insertPolicies);
|
|
3244
3339
|
compilePolicyFor("update", updatePolicies);
|
|
3245
|
-
if (isView && table.resolved === void 0) {
|
|
3340
|
+
if (isView && table.resolved === void 0 || unkeyedSyncedTableNames.has(table.table)) {
|
|
3246
3341
|
writePolicies.set(table.table, CONST_FALSE);
|
|
3247
3342
|
insertPolicies.set(table.table, CONST_FALSE);
|
|
3248
3343
|
updatePolicies.set(table.table, CONST_FALSE);
|
|
@@ -3317,7 +3412,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3317
3412
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
3318
3413
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
3319
3414
|
for (const table of relations) {
|
|
3320
|
-
if (table.internal === true) continue;
|
|
3415
|
+
if (table.internal === true || unkeyedSyncedTableNames.has(table.table)) continue;
|
|
3321
3416
|
const roots = queryRootNames(table.table);
|
|
3322
3417
|
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
3323
3418
|
if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
|
|
@@ -3697,8 +3792,10 @@ var OperationLowerer = class {
|
|
|
3697
3792
|
constructor(ctx, restricted) {
|
|
3698
3793
|
this.ctx = ctx;
|
|
3699
3794
|
this.variables = new Map(restricted.variables.map((v) => [v.name, v]));
|
|
3795
|
+
this.maxRows = restricted.readBudget ?? MAX_LIMIT;
|
|
3700
3796
|
}
|
|
3701
3797
|
variables;
|
|
3798
|
+
maxRows;
|
|
3702
3799
|
meta(table) {
|
|
3703
3800
|
return this.ctx.tables[table];
|
|
3704
3801
|
}
|
|
@@ -3902,13 +3999,13 @@ var OperationLowerer = class {
|
|
|
3902
3999
|
});
|
|
3903
4000
|
}
|
|
3904
4001
|
lowerLimit(node) {
|
|
3905
|
-
const base2 = { default: DEFAULT_LIMIT, max:
|
|
4002
|
+
const base2 = { default: DEFAULT_LIMIT, max: this.maxRows };
|
|
3906
4003
|
if (!node) return base2;
|
|
3907
4004
|
if (node.kind === graphql.Kind.VARIABLE) return { ...base2, variable: node.name.value };
|
|
3908
4005
|
if (node.kind === graphql.Kind.INT) {
|
|
3909
4006
|
const value = Number(node.value);
|
|
3910
4007
|
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
3911
|
-
return { ...base2, value: Math.min(value,
|
|
4008
|
+
return { ...base2, value: Math.min(value, this.maxRows) };
|
|
3912
4009
|
}
|
|
3913
4010
|
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
3914
4011
|
}
|
|
@@ -4707,7 +4804,13 @@ var OperationLowerer = class {
|
|
|
4707
4804
|
};
|
|
4708
4805
|
function lowerOperation({ restricted, ctx }) {
|
|
4709
4806
|
const lowerer = new OperationLowerer(ctx, restricted);
|
|
4710
|
-
|
|
4807
|
+
const roots = lowerer.lowerRoots(restricted.definition, restricted.kind);
|
|
4808
|
+
if (restricted.readBudget === void 0) return roots;
|
|
4809
|
+
const root = roots[0];
|
|
4810
|
+
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) {
|
|
4811
|
+
fail2("@readBudget requires one flat select root without related selections, aggregates, cells, or prefetch", "invalid-read-budget");
|
|
4812
|
+
}
|
|
4813
|
+
return roots;
|
|
4711
4814
|
}
|
|
4712
4815
|
function isCellColumn(meta, name2) {
|
|
4713
4816
|
if (!meta.cells) return false;
|
|
@@ -4766,6 +4869,19 @@ function parseDocuments({
|
|
|
4766
4869
|
}
|
|
4767
4870
|
return { documents: parsed, diagnostics };
|
|
4768
4871
|
}
|
|
4872
|
+
function parseReadBudget(operation) {
|
|
4873
|
+
const directives = operation.directives?.filter((directive) => directive.name.value === "readBudget") ?? [];
|
|
4874
|
+
if (directives.length === 0) return void 0;
|
|
4875
|
+
const value = directives[0]?.arguments?.find((argument) => argument.name.value === "maxRows")?.value;
|
|
4876
|
+
if (operation.operation !== "query" || directives.length !== 1 || value?.kind !== graphql.Kind.INT) {
|
|
4877
|
+
throw new DocumentCompileError("@readBudget requires a literal maxRows on a query", "invalid-read-budget");
|
|
4878
|
+
}
|
|
4879
|
+
const maxRows = Number(value.value);
|
|
4880
|
+
if (!Number.isSafeInteger(maxRows) || maxRows < 1 || maxRows > MAX_PERSISTED_READ_ROWS) {
|
|
4881
|
+
throw new DocumentCompileError(`@readBudget(maxRows:) must be between 1 and ${MAX_PERSISTED_READ_ROWS}`, "invalid-read-budget");
|
|
4882
|
+
}
|
|
4883
|
+
return maxRows;
|
|
4884
|
+
}
|
|
4769
4885
|
function unwrapType(node, variable) {
|
|
4770
4886
|
let nullable = true;
|
|
4771
4887
|
let list2 = false;
|
|
@@ -4926,6 +5042,7 @@ function restrictOperation({
|
|
|
4926
5042
|
name: name2,
|
|
4927
5043
|
kind: operation.operation === "mutation" ? "mutation" : "query",
|
|
4928
5044
|
requires: parseRequires(operation),
|
|
5045
|
+
readBudget: parseReadBudget(operation),
|
|
4929
5046
|
variables,
|
|
4930
5047
|
definition: operation
|
|
4931
5048
|
};
|
|
@@ -5583,6 +5700,118 @@ function bindNamedParameters({ sql: sql4, names }) {
|
|
|
5583
5700
|
}
|
|
5584
5701
|
return result;
|
|
5585
5702
|
}
|
|
5703
|
+
var parser2;
|
|
5704
|
+
var functions = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
5705
|
+
var scalarTypes = /* @__PURE__ */ new Set([
|
|
5706
|
+
"bool",
|
|
5707
|
+
"boolean",
|
|
5708
|
+
"int2",
|
|
5709
|
+
"int4",
|
|
5710
|
+
"int8",
|
|
5711
|
+
"integer",
|
|
5712
|
+
"bigint",
|
|
5713
|
+
"smallint",
|
|
5714
|
+
"numeric",
|
|
5715
|
+
"decimal",
|
|
5716
|
+
"float4",
|
|
5717
|
+
"float8",
|
|
5718
|
+
"real",
|
|
5719
|
+
"text",
|
|
5720
|
+
"varchar",
|
|
5721
|
+
"bpchar",
|
|
5722
|
+
"uuid",
|
|
5723
|
+
"date",
|
|
5724
|
+
"timestamp",
|
|
5725
|
+
"timestamptz",
|
|
5726
|
+
"time",
|
|
5727
|
+
"timetz",
|
|
5728
|
+
"interval",
|
|
5729
|
+
"json",
|
|
5730
|
+
"jsonb",
|
|
5731
|
+
"bytea"
|
|
5732
|
+
]);
|
|
5733
|
+
var sqlValues = /* @__PURE__ */ new Set([
|
|
5734
|
+
"SVFOP_CURRENT_DATE",
|
|
5735
|
+
"SVFOP_CURRENT_TIME",
|
|
5736
|
+
"SVFOP_CURRENT_TIME_N",
|
|
5737
|
+
"SVFOP_CURRENT_TIMESTAMP",
|
|
5738
|
+
"SVFOP_CURRENT_TIMESTAMP_N",
|
|
5739
|
+
"SVFOP_LOCALTIME",
|
|
5740
|
+
"SVFOP_LOCALTIME_N",
|
|
5741
|
+
"SVFOP_LOCALTIMESTAMP",
|
|
5742
|
+
"SVFOP_LOCALTIMESTAMP_N",
|
|
5743
|
+
"SVFOP_CURRENT_ROLE",
|
|
5744
|
+
"SVFOP_CURRENT_USER",
|
|
5745
|
+
"SVFOP_USER",
|
|
5746
|
+
"SVFOP_SESSION_USER",
|
|
5747
|
+
"SVFOP_CURRENT_CATALOG",
|
|
5748
|
+
"SVFOP_CURRENT_SCHEMA"
|
|
5749
|
+
]);
|
|
5750
|
+
async function validateManagedViewQueries({ canonical }) {
|
|
5751
|
+
for (const definition of [canonical, ...canonical.imports ?? []]) {
|
|
5752
|
+
for (const view of definition.views ?? []) {
|
|
5753
|
+
const fail4 = (message) => {
|
|
5754
|
+
throw new OrmDefinitionError(`managed view '${definition.namespace}/${view.view}': ${message}`);
|
|
5755
|
+
};
|
|
5756
|
+
parser2 ??= new pgParser.PgParser({ version: 17 });
|
|
5757
|
+
const parsed = await parser2.parse(view.query);
|
|
5758
|
+
if (parsed.error || !parsed.tree) fail4("invalid SELECT query");
|
|
5759
|
+
const statements = list(obj(parsed.tree).stmts);
|
|
5760
|
+
if (statements.length !== 1 || !obj(obj(statements[0]).stmt).SelectStmt) fail4("one SELECT query is required");
|
|
5761
|
+
validateTree({ tree: parsed.tree, schema: ormSchemaName(definition.namespace), enums: definition.enums.map((entry) => entry.name), fail: fail4 });
|
|
5762
|
+
}
|
|
5763
|
+
}
|
|
5764
|
+
}
|
|
5765
|
+
function pgCatalogName(parts) {
|
|
5766
|
+
if (parts.length === 1) return parts[0];
|
|
5767
|
+
if (parts.length === 2 && parts[0] === "pg_catalog") return parts[1];
|
|
5768
|
+
return void 0;
|
|
5769
|
+
}
|
|
5770
|
+
function reviewedFunctionName({ parts, format }) {
|
|
5771
|
+
return format === "COERCE_SQL_SYNTAX" && parts.length === 2 && parts[0] === "pg_catalog" && parts[1] === "position" ? "strpos" : pgCatalogName(parts);
|
|
5772
|
+
}
|
|
5773
|
+
function validateTree({ tree, ...scope }) {
|
|
5774
|
+
const pending = [tree];
|
|
5775
|
+
while (pending.length) {
|
|
5776
|
+
const value = pending.pop();
|
|
5777
|
+
if (Array.isArray(value)) {
|
|
5778
|
+
pending.push(...value);
|
|
5779
|
+
continue;
|
|
5780
|
+
}
|
|
5781
|
+
if (!value || typeof value !== "object") continue;
|
|
5782
|
+
const node = obj(value);
|
|
5783
|
+
for (const [kind, child] of Object.entries(node)) {
|
|
5784
|
+
validateNode({ kind, child, scope });
|
|
5785
|
+
pending.push(child);
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
}
|
|
5789
|
+
function validateNode({ kind, child, scope }) {
|
|
5790
|
+
const { fail: fail4 } = scope;
|
|
5791
|
+
if (kind.endsWith("Stmt") && kind !== "SelectStmt") fail4("only SELECT statements are permitted");
|
|
5792
|
+
if (kind === "FuncCall") {
|
|
5793
|
+
const call = obj(child);
|
|
5794
|
+
const parts = strings(call.funcname);
|
|
5795
|
+
const name2 = reviewedFunctionName({ parts, format: call.funcformat });
|
|
5796
|
+
if (!name2 || !functions.has(name2)) fail4(`only explicitly allowlisted pg_catalog functions are permitted (found '${parts.join(".")}')`);
|
|
5797
|
+
}
|
|
5798
|
+
if (kind === "typeName" || kind === "TypeName") validateType({ node: obj(child), ...scope });
|
|
5799
|
+
if (kind === "A_Expr" && pgCatalogName(strings(obj(child).name)) === void 0) fail4("only pg_catalog operators are permitted");
|
|
5800
|
+
if (kind === "SortBy" && list(obj(child).useOp).length && pgCatalogName(strings(obj(child).useOp)) === void 0) fail4("only pg_catalog sort operators are permitted");
|
|
5801
|
+
if (kind === "SQLValueFunction" && !sqlValues.has(String(obj(child).op))) fail4("unsupported SQL value expression");
|
|
5802
|
+
if (kind === "ParamRef") fail4("view queries cannot contain parameters");
|
|
5803
|
+
if (kind === "A_Indirection" && list(obj(child).indirection).some((entry) => !obj(entry).A_Indices)) {
|
|
5804
|
+
fail4("composite field notation is not permitted in managed views");
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
5807
|
+
function validateType({ node, schema, enums, fail: fail4 }) {
|
|
5808
|
+
const parts = strings(node.names);
|
|
5809
|
+
const builtin = pgCatalogName(parts);
|
|
5810
|
+
const managedEnum = parts.length === 1 && enums.includes(parts[0]) || parts.length === 2 && parts[0] === schema && enums.includes(parts[1]);
|
|
5811
|
+
if (node.setof || node.pct_type || !managedEnum && (!builtin || !scalarTypes.has(builtin))) {
|
|
5812
|
+
fail4(`casts may only use supported pg_catalog scalar types or declared enums (found '${parts.join(".")}')`);
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5586
5815
|
|
|
5587
5816
|
// src/sql/operations.ts
|
|
5588
5817
|
function isSqlOperationDocument(path, contents) {
|
|
@@ -5598,7 +5827,13 @@ function isSqlOperationDocument(path, contents) {
|
|
|
5598
5827
|
async function compileOperationDocuments(input) {
|
|
5599
5828
|
const sqlDocuments = input.documents.filter((d) => isSqlOperationDocument(d.path, d.contents));
|
|
5600
5829
|
const result = compileNamespace({ ...input, documents: input.documents.filter((d) => !isSqlOperationDocument(d.path, d.contents)) });
|
|
5601
|
-
if (!result.ok
|
|
5830
|
+
if (!result.ok) return result;
|
|
5831
|
+
const viewDiagnostics = await managedViewDiagnostics(input.canonical);
|
|
5832
|
+
if (viewDiagnostics.length) return { ok: false, diagnostics: [...result.diagnostics, ...viewDiagnostics] };
|
|
5833
|
+
if (!sqlDocuments.length) return result;
|
|
5834
|
+
return compileSqlDocuments({ input, result, sqlDocuments });
|
|
5835
|
+
}
|
|
5836
|
+
async function compileSqlDocuments({ input, result, sqlDocuments }) {
|
|
5602
5837
|
for (const document of sqlDocuments) {
|
|
5603
5838
|
try {
|
|
5604
5839
|
const definition = defineOperation(JSON.parse(document.contents));
|
|
@@ -5692,6 +5927,18 @@ async function compileOperationDocuments(input) {
|
|
|
5692
5927
|
result.manifest.opsHash = result.artifact.opsHash;
|
|
5693
5928
|
return { ...result, artifact: normalizeIr(result.artifact) };
|
|
5694
5929
|
}
|
|
5930
|
+
async function managedViewDiagnostics(canonical) {
|
|
5931
|
+
try {
|
|
5932
|
+
await validateManagedViewQueries({ canonical });
|
|
5933
|
+
return [];
|
|
5934
|
+
} catch (error) {
|
|
5935
|
+
return [{
|
|
5936
|
+
severity: "error",
|
|
5937
|
+
code: "invalid-view-query",
|
|
5938
|
+
message: error instanceof Error ? error.message : "invalid managed view query"
|
|
5939
|
+
}];
|
|
5940
|
+
}
|
|
5941
|
+
}
|
|
5695
5942
|
|
|
5696
5943
|
// src/sql/codegen.ts
|
|
5697
5944
|
var typeOf = (type) => ["integer", "double"].includes(type) ? "number" : type === "boolean" ? "boolean" : "string";
|
|
@@ -7787,7 +8034,7 @@ function snapshotTableNames(snapshot, schema) {
|
|
|
7787
8034
|
// src/engine/materialized-refresh.ts
|
|
7788
8035
|
function materializedRefreshFunctions(canonical) {
|
|
7789
8036
|
const schema = ormSchemaName(canonical.namespace);
|
|
7790
|
-
const
|
|
8037
|
+
const functions2 = /* @__PURE__ */ new Map();
|
|
7791
8038
|
for (const view of canonical.views ?? []) {
|
|
7792
8039
|
const materialized = view.resolved?.materialized;
|
|
7793
8040
|
if (!materialized) continue;
|
|
@@ -7796,7 +8043,7 @@ function materializedRefreshFunctions(canonical) {
|
|
|
7796
8043
|
if (!table || pk.length === 0) {
|
|
7797
8044
|
throw new OrmDefinitionError(`materialized resolved table '${view.view}' needs its table and primary key`);
|
|
7798
8045
|
}
|
|
7799
|
-
|
|
8046
|
+
functions2.set(view.view, refreshFunctionSql({
|
|
7800
8047
|
schema,
|
|
7801
8048
|
name: view.view,
|
|
7802
8049
|
table: materialized.table,
|
|
@@ -7805,7 +8052,7 @@ function materializedRefreshFunctions(canonical) {
|
|
|
7805
8052
|
pk
|
|
7806
8053
|
}));
|
|
7807
8054
|
}
|
|
7808
|
-
return
|
|
8055
|
+
return functions2;
|
|
7809
8056
|
}
|
|
7810
8057
|
function refreshFunctionSql({ schema, name: name2, table, compute, columns, pk }) {
|
|
7811
8058
|
const fn = `${quoteIdentifier(schema)}.${quoteIdentifier(materializedRefreshFunctionName(name2))}`;
|
|
@@ -8449,6 +8696,7 @@ async function planWithViewRecreation({
|
|
|
8449
8696
|
previousSnapshot,
|
|
8450
8697
|
viewRecreation
|
|
8451
8698
|
}) {
|
|
8699
|
+
await validateManagedViewQueries({ canonical });
|
|
8452
8700
|
const schema = ormSchemaName(canonical.namespace);
|
|
8453
8701
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
8454
8702
|
assertValidViewDependencies(canonical);
|
|
@@ -9739,6 +9987,9 @@ function generateSdl(canonical, options = {}) {
|
|
|
9739
9987
|
blocks.push(
|
|
9740
9988
|
'"""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'
|
|
9741
9989
|
);
|
|
9990
|
+
if (!options.omitReadBudgetDirective) blocks.push(
|
|
9991
|
+
'"""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'
|
|
9992
|
+
);
|
|
9742
9993
|
blocks.push("directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT");
|
|
9743
9994
|
blocks.push(
|
|
9744
9995
|
'"""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'
|
|
@@ -11943,7 +12194,7 @@ function emitSyncedTable(table) {
|
|
|
11943
12194
|
lines.push(" },", " {");
|
|
11944
12195
|
lines.push(` source: ${quote(table.source ?? "")},`);
|
|
11945
12196
|
if (table.description !== void 0) lines.push(` description: ${quote(table.description)},`);
|
|
11946
|
-
if (singlePrimaryKey === void 0 && table.primaryKey
|
|
12197
|
+
if (singlePrimaryKey === void 0 && table.primaryKey !== void 0) {
|
|
11947
12198
|
lines.push(` primaryKey: ${jsonLiteral(table.primaryKey, 4)},`);
|
|
11948
12199
|
}
|
|
11949
12200
|
if (table.links.length > 0) {
|