@sequenceholdings/orm 2.4.0 → 2.6.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/README.md +23 -0
- package/dist/authoring.cjs +53 -7
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +10 -6
- package/dist/authoring.d.ts +10 -6
- 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 +1 -1
- package/dist/canonical.d.ts +1 -1
- package/dist/canonical.js +48 -2
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +260 -31
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +260 -31
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +11 -6
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +38 -31
- package/dist/client.d.ts +38 -31
- package/dist/client.js +11 -7
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +513 -23
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +39 -3
- package/dist/compiler.d.ts +39 -3
- package/dist/compiler.js +510 -24
- 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 +461 -140
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -4
- package/dist/index.d.ts +34 -4
- package/dist/index.js +460 -141
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +55 -4
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +1 -1
- package/dist/ir.d.ts +1 -1
- package/dist/ir.js +55 -4
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +55 -4
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +1 -1
- package/dist/predicate-v2.d.ts +1 -1
- package/dist/predicate-v2.js +55 -4
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +60 -4
- 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 +60 -4
- package/dist/sdl.js.map +1 -1
- package/package.json +1 -1
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` });
|
|
@@ -1409,6 +1458,9 @@ function normalizeCanonical(definition) {
|
|
|
1409
1458
|
return normalized;
|
|
1410
1459
|
}
|
|
1411
1460
|
|
|
1461
|
+
// src/client/sql.ts
|
|
1462
|
+
var BUILTIN_SQL_READ_HASH = "sha256:a50c477c952a5e0fe68ed158a4530e4aab573c09bcbb1a8ac454dc88a39a6258";
|
|
1463
|
+
|
|
1412
1464
|
// src/ir/index.ts
|
|
1413
1465
|
var IR_VERSION = 5;
|
|
1414
1466
|
var LEGACY_IR_VERSION = 1;
|
|
@@ -1497,7 +1549,7 @@ var limitIrSchema = zod.z.object({
|
|
|
1497
1549
|
variable: zod.z.string().optional(),
|
|
1498
1550
|
value: zod.z.number().int().positive().optional(),
|
|
1499
1551
|
default: zod.z.number().int().positive(),
|
|
1500
|
-
max: zod.z.number().int().positive()
|
|
1552
|
+
max: zod.z.number().int().positive().max(MAX_PERSISTED_READ_ROWS)
|
|
1501
1553
|
});
|
|
1502
1554
|
var selectionIrSchema = zod.z.lazy(
|
|
1503
1555
|
() => zod.z.object({
|
|
@@ -1742,7 +1794,8 @@ var compiledOperationSchema = zod.z.object({
|
|
|
1742
1794
|
}).superRefine((operation, ctx) => {
|
|
1743
1795
|
if (operation.language === "sql") {
|
|
1744
1796
|
const validPlan = operation.sqlQuery ? !operation.sql && operation.irVersion === RUNTIME_SQL_IR_VERSION : !!operation.sql && operation.irVersion === SQL_IR_VERSION;
|
|
1745
|
-
|
|
1797
|
+
const builtin = operation.name === "BuiltinSqlRead" && operation.opHash === BUILTIN_SQL_READ_HASH && operation.requires === null;
|
|
1798
|
+
if (!validPlan || operation.kind !== "query" || operation.roots.length !== 0 || !builtin && !operation.requires?.capability) {
|
|
1746
1799
|
ctx.addIssue({ code: "custom", message: "SQL operations require a query-only v4 plan or v5 runtime contract, capability, and no GraphQL roots" });
|
|
1747
1800
|
}
|
|
1748
1801
|
} else if (operation.sql || operation.sqlQuery || operation.roots.length === 0) {
|
|
@@ -3121,6 +3174,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3121
3174
|
const platformMeta = usesPlatformUsers ? platformUserMeta() : void 0;
|
|
3122
3175
|
if (platformMeta) tables[PLATFORM_USER_TABLE] = platformMeta;
|
|
3123
3176
|
const syncedTableNames = new Set(canonical.tables.filter((t2) => t2.source !== void 0).map((t2) => t2.table));
|
|
3177
|
+
const unkeyedSyncedTableNames = new Set(relations.filter((table) => table.source !== void 0 && table.primaryKey?.length === 0).map((table) => table.table));
|
|
3124
3178
|
const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
|
|
3125
3179
|
const claimsGatedMemo = /* @__PURE__ */ new Map();
|
|
3126
3180
|
const relationIsClaimsGated = (name2) => {
|
|
@@ -3134,6 +3188,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3134
3188
|
};
|
|
3135
3189
|
for (const table of relations) {
|
|
3136
3190
|
const isView = viewNames.has(table.table);
|
|
3191
|
+
const readOnly = isView || unkeyedSyncedTableNames.has(table.table);
|
|
3137
3192
|
const owner = owners.get(table.table);
|
|
3138
3193
|
const entries = resolveColumnEntries(table, enums, diagnostics);
|
|
3139
3194
|
columnEntries.set(table.table, entries);
|
|
@@ -3181,13 +3236,13 @@ function buildCompileContext(rawCanonical) {
|
|
|
3181
3236
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
3182
3237
|
claimsGated: relationIsClaimsGated(table.table),
|
|
3183
3238
|
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
3184
|
-
primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
3239
|
+
primaryKey: isView || unkeyedSyncedTableNames.has(table.table) ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
3185
3240
|
links: [],
|
|
3186
3241
|
filterable: data.map((c) => c.name),
|
|
3187
3242
|
orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
3188
|
-
insertable: resolved ? resolvedWritable(data) :
|
|
3189
|
-
settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey2.includes(name2)) :
|
|
3190
|
-
appendable:
|
|
3243
|
+
insertable: resolved ? resolvedWritable(data) : readOnly ? [] : active.map((c) => c.name),
|
|
3244
|
+
settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey2.includes(name2)) : readOnly ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
3245
|
+
appendable: readOnly ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
3191
3246
|
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
3192
3247
|
...cells ? { cells } : {},
|
|
3193
3248
|
...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey2[0] }) } : {},
|
|
@@ -3211,6 +3266,16 @@ function buildCompileContext(rawCanonical) {
|
|
|
3211
3266
|
});
|
|
3212
3267
|
continue;
|
|
3213
3268
|
}
|
|
3269
|
+
const identityTable = link.kind === "toOne" ? targetName : link.kind === "toMany" ? table.table : void 0;
|
|
3270
|
+
if (identityTable !== void 0 && unkeyedSyncedTableNames.has(identityTable)) {
|
|
3271
|
+
diagnostics.push({
|
|
3272
|
+
severity: "error",
|
|
3273
|
+
code: "unkeyed-link-identity",
|
|
3274
|
+
table: table.table,
|
|
3275
|
+
message: `link '${link.name}' cannot use unkeyed table '${identityTable}' as a row identity`
|
|
3276
|
+
});
|
|
3277
|
+
continue;
|
|
3278
|
+
}
|
|
3214
3279
|
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
|
|
3215
3280
|
const onEntries = link.kind === "toMany" ? targetEntries : entries;
|
|
3216
3281
|
const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
|
|
@@ -3276,7 +3341,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3276
3341
|
compilePolicyFor("write", writePolicies);
|
|
3277
3342
|
compilePolicyFor("insert", insertPolicies);
|
|
3278
3343
|
compilePolicyFor("update", updatePolicies);
|
|
3279
|
-
if (isView && table.resolved === void 0) {
|
|
3344
|
+
if (isView && table.resolved === void 0 || unkeyedSyncedTableNames.has(table.table)) {
|
|
3280
3345
|
writePolicies.set(table.table, CONST_FALSE);
|
|
3281
3346
|
insertPolicies.set(table.table, CONST_FALSE);
|
|
3282
3347
|
updatePolicies.set(table.table, CONST_FALSE);
|
|
@@ -3351,7 +3416,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3351
3416
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
3352
3417
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
3353
3418
|
for (const table of relations) {
|
|
3354
|
-
if (table.internal === true) continue;
|
|
3419
|
+
if (table.internal === true || unkeyedSyncedTableNames.has(table.table)) continue;
|
|
3355
3420
|
const roots = queryRootNames(table.table);
|
|
3356
3421
|
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
3357
3422
|
if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
|
|
@@ -3731,8 +3796,10 @@ var OperationLowerer = class {
|
|
|
3731
3796
|
constructor(ctx, restricted) {
|
|
3732
3797
|
this.ctx = ctx;
|
|
3733
3798
|
this.variables = new Map(restricted.variables.map((v) => [v.name, v]));
|
|
3799
|
+
this.maxRows = restricted.readBudget ?? MAX_LIMIT;
|
|
3734
3800
|
}
|
|
3735
3801
|
variables;
|
|
3802
|
+
maxRows;
|
|
3736
3803
|
meta(table) {
|
|
3737
3804
|
return this.ctx.tables[table];
|
|
3738
3805
|
}
|
|
@@ -3936,13 +4003,13 @@ var OperationLowerer = class {
|
|
|
3936
4003
|
});
|
|
3937
4004
|
}
|
|
3938
4005
|
lowerLimit(node) {
|
|
3939
|
-
const base2 = { default: DEFAULT_LIMIT, max:
|
|
4006
|
+
const base2 = { default: DEFAULT_LIMIT, max: this.maxRows };
|
|
3940
4007
|
if (!node) return base2;
|
|
3941
4008
|
if (node.kind === graphql.Kind.VARIABLE) return { ...base2, variable: node.name.value };
|
|
3942
4009
|
if (node.kind === graphql.Kind.INT) {
|
|
3943
4010
|
const value = Number(node.value);
|
|
3944
4011
|
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
3945
|
-
return { ...base2, value: Math.min(value,
|
|
4012
|
+
return { ...base2, value: Math.min(value, this.maxRows) };
|
|
3946
4013
|
}
|
|
3947
4014
|
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
3948
4015
|
}
|
|
@@ -4741,7 +4808,13 @@ var OperationLowerer = class {
|
|
|
4741
4808
|
};
|
|
4742
4809
|
function lowerOperation({ restricted, ctx }) {
|
|
4743
4810
|
const lowerer = new OperationLowerer(ctx, restricted);
|
|
4744
|
-
|
|
4811
|
+
const roots = lowerer.lowerRoots(restricted.definition, restricted.kind);
|
|
4812
|
+
if (restricted.readBudget === void 0) return roots;
|
|
4813
|
+
const root = roots[0];
|
|
4814
|
+
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) {
|
|
4815
|
+
fail2("@readBudget requires one flat select root without related selections, aggregates, cells, or prefetch", "invalid-read-budget");
|
|
4816
|
+
}
|
|
4817
|
+
return roots;
|
|
4745
4818
|
}
|
|
4746
4819
|
function isCellColumn(meta, name2) {
|
|
4747
4820
|
if (!meta.cells) return false;
|
|
@@ -4800,6 +4873,19 @@ function parseDocuments({
|
|
|
4800
4873
|
}
|
|
4801
4874
|
return { documents: parsed, diagnostics };
|
|
4802
4875
|
}
|
|
4876
|
+
function parseReadBudget(operation) {
|
|
4877
|
+
const directives = operation.directives?.filter((directive) => directive.name.value === "readBudget") ?? [];
|
|
4878
|
+
if (directives.length === 0) return void 0;
|
|
4879
|
+
const value = directives[0]?.arguments?.find((argument) => argument.name.value === "maxRows")?.value;
|
|
4880
|
+
if (operation.operation !== "query" || directives.length !== 1 || value?.kind !== graphql.Kind.INT) {
|
|
4881
|
+
throw new DocumentCompileError("@readBudget requires a literal maxRows on a query", "invalid-read-budget");
|
|
4882
|
+
}
|
|
4883
|
+
const maxRows = Number(value.value);
|
|
4884
|
+
if (!Number.isSafeInteger(maxRows) || maxRows < 1 || maxRows > MAX_PERSISTED_READ_ROWS) {
|
|
4885
|
+
throw new DocumentCompileError(`@readBudget(maxRows:) must be between 1 and ${MAX_PERSISTED_READ_ROWS}`, "invalid-read-budget");
|
|
4886
|
+
}
|
|
4887
|
+
return maxRows;
|
|
4888
|
+
}
|
|
4803
4889
|
function unwrapType(node, variable) {
|
|
4804
4890
|
let nullable = true;
|
|
4805
4891
|
let list2 = false;
|
|
@@ -4960,6 +5046,7 @@ function restrictOperation({
|
|
|
4960
5046
|
name: name2,
|
|
4961
5047
|
kind: operation.operation === "mutation" ? "mutation" : "query",
|
|
4962
5048
|
requires: parseRequires(operation),
|
|
5049
|
+
readBudget: parseReadBudget(operation),
|
|
4963
5050
|
variables,
|
|
4964
5051
|
definition: operation
|
|
4965
5052
|
};
|
|
@@ -5090,9 +5177,9 @@ function compilePredIrToSql(pred, options = {}) {
|
|
|
5090
5177
|
case "cmp":
|
|
5091
5178
|
return compileCmp(pred.op, pred.column, pred.value, options);
|
|
5092
5179
|
case "capability":
|
|
5093
|
-
return `(${capabilityExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
5180
|
+
return `(${options.statementClaimChecks ? "SELECT " : ""}${capabilityExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
5094
5181
|
case "role":
|
|
5095
|
-
return `(${roleExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
5182
|
+
return `(${options.statementClaimChecks ? "SELECT " : ""}${roleExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
5096
5183
|
case "exists":
|
|
5097
5184
|
if (options.exists) return options.exists(pred);
|
|
5098
5185
|
throw new OrmDefinitionError("synced view read policy cannot use 'exists' without a relationship resolver");
|
|
@@ -5200,7 +5287,8 @@ function createSqlCatalog({ canonical, publishedObjects, executionTarget }) {
|
|
|
5200
5287
|
const { ctx, diagnostics } = buildCompileContext(canonical);
|
|
5201
5288
|
if (diagnostics.some((d) => d.severity === "error")) throw new Error(diagnostics.map((d) => d.message).join("; "));
|
|
5202
5289
|
const identity = sqlHash(`ontology-sql-v1\0${computeSchemaHash(canonical)}\0${executionTarget}`);
|
|
5203
|
-
const
|
|
5290
|
+
const logicalNames = new Map(Object.entries(ctx.tables).map(([key, meta]) => [meta, key]));
|
|
5291
|
+
const refOf = (meta) => `${meta.namespace ?? canonical.namespace}/${logicalNames.get(meta)}`;
|
|
5204
5292
|
const definitions = [canonical, ...canonical.imports ?? []];
|
|
5205
5293
|
const internal = new Set(definitions.flatMap((owner) => [
|
|
5206
5294
|
...owner.tables.filter((t2) => t2.internal).map((t2) => `${owner.namespace}/${t2.table}`),
|
|
@@ -5276,15 +5364,22 @@ function only(node, keys) {
|
|
|
5276
5364
|
var parser;
|
|
5277
5365
|
var SQL_TYPES = ["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"];
|
|
5278
5366
|
var pgType = (type) => type === "double" ? "double precision" : SQL_TYPES.some((t2) => t2 === type) ? type : fail3("unsupported SQL type");
|
|
5279
|
-
async function
|
|
5280
|
-
if (
|
|
5367
|
+
async function parseSelect(sql4) {
|
|
5368
|
+
if (Buffer.byteLength(sql4) > 65536) fail3("SQL limit exceeded");
|
|
5281
5369
|
parser ??= new pgParser.PgParser({ version: 17 });
|
|
5282
5370
|
const parsed = await parser.parse(sql4);
|
|
5283
5371
|
if (parsed.error || !parsed.tree) fail3("invalid SQL");
|
|
5284
5372
|
const statements = list(obj(parsed.tree).stmts);
|
|
5285
5373
|
if (statements.length !== 1) fail3("one SQL SELECT is required");
|
|
5374
|
+
const statement = obj(obj(statements[0]).stmt);
|
|
5375
|
+
if (!statement.SelectStmt) fail3("one SQL SELECT is required");
|
|
5376
|
+
return statement;
|
|
5377
|
+
}
|
|
5378
|
+
async function compileOntologySql({ catalog, sql: sql4, parameterTypes = [], maxRows = 1e3 }) {
|
|
5379
|
+
if (!Number.isInteger(maxRows) || maxRows < 1 || maxRows > 1e4 || Buffer.byteLength(sql4) > 65536) fail3("SQL limit exceeded");
|
|
5380
|
+
const statement = await parseSelect(sql4);
|
|
5286
5381
|
const emitter = new SqlEmitter({ catalog, parameterTypes });
|
|
5287
|
-
const result = emitter.select(
|
|
5382
|
+
const result = emitter.select(statement, { relations: [], ctes: /* @__PURE__ */ new Map() });
|
|
5288
5383
|
if (emitter.used.size !== parameterTypes.length) fail3("SQL parameters must be used exactly as declared");
|
|
5289
5384
|
if (!emitter.read.size) fail3("SQL operation must read an ontology object");
|
|
5290
5385
|
return {
|
|
@@ -5617,6 +5712,118 @@ function bindNamedParameters({ sql: sql4, names }) {
|
|
|
5617
5712
|
}
|
|
5618
5713
|
return result;
|
|
5619
5714
|
}
|
|
5715
|
+
var parser2;
|
|
5716
|
+
var functions = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
5717
|
+
var scalarTypes = /* @__PURE__ */ new Set([
|
|
5718
|
+
"bool",
|
|
5719
|
+
"boolean",
|
|
5720
|
+
"int2",
|
|
5721
|
+
"int4",
|
|
5722
|
+
"int8",
|
|
5723
|
+
"integer",
|
|
5724
|
+
"bigint",
|
|
5725
|
+
"smallint",
|
|
5726
|
+
"numeric",
|
|
5727
|
+
"decimal",
|
|
5728
|
+
"float4",
|
|
5729
|
+
"float8",
|
|
5730
|
+
"real",
|
|
5731
|
+
"text",
|
|
5732
|
+
"varchar",
|
|
5733
|
+
"bpchar",
|
|
5734
|
+
"uuid",
|
|
5735
|
+
"date",
|
|
5736
|
+
"timestamp",
|
|
5737
|
+
"timestamptz",
|
|
5738
|
+
"time",
|
|
5739
|
+
"timetz",
|
|
5740
|
+
"interval",
|
|
5741
|
+
"json",
|
|
5742
|
+
"jsonb",
|
|
5743
|
+
"bytea"
|
|
5744
|
+
]);
|
|
5745
|
+
var sqlValues = /* @__PURE__ */ new Set([
|
|
5746
|
+
"SVFOP_CURRENT_DATE",
|
|
5747
|
+
"SVFOP_CURRENT_TIME",
|
|
5748
|
+
"SVFOP_CURRENT_TIME_N",
|
|
5749
|
+
"SVFOP_CURRENT_TIMESTAMP",
|
|
5750
|
+
"SVFOP_CURRENT_TIMESTAMP_N",
|
|
5751
|
+
"SVFOP_LOCALTIME",
|
|
5752
|
+
"SVFOP_LOCALTIME_N",
|
|
5753
|
+
"SVFOP_LOCALTIMESTAMP",
|
|
5754
|
+
"SVFOP_LOCALTIMESTAMP_N",
|
|
5755
|
+
"SVFOP_CURRENT_ROLE",
|
|
5756
|
+
"SVFOP_CURRENT_USER",
|
|
5757
|
+
"SVFOP_USER",
|
|
5758
|
+
"SVFOP_SESSION_USER",
|
|
5759
|
+
"SVFOP_CURRENT_CATALOG",
|
|
5760
|
+
"SVFOP_CURRENT_SCHEMA"
|
|
5761
|
+
]);
|
|
5762
|
+
async function validateManagedViewQueries({ canonical }) {
|
|
5763
|
+
for (const definition of [canonical, ...canonical.imports ?? []]) {
|
|
5764
|
+
for (const view of definition.views ?? []) {
|
|
5765
|
+
const fail4 = (message) => {
|
|
5766
|
+
throw new OrmDefinitionError(`managed view '${definition.namespace}/${view.view}': ${message}`);
|
|
5767
|
+
};
|
|
5768
|
+
parser2 ??= new pgParser.PgParser({ version: 17 });
|
|
5769
|
+
const parsed = await parser2.parse(view.query);
|
|
5770
|
+
if (parsed.error || !parsed.tree) fail4("invalid SELECT query");
|
|
5771
|
+
const statements = list(obj(parsed.tree).stmts);
|
|
5772
|
+
if (statements.length !== 1 || !obj(obj(statements[0]).stmt).SelectStmt) fail4("one SELECT query is required");
|
|
5773
|
+
validateTree({ tree: parsed.tree, schema: ormSchemaName(definition.namespace), enums: definition.enums.map((entry) => entry.name), fail: fail4 });
|
|
5774
|
+
}
|
|
5775
|
+
}
|
|
5776
|
+
}
|
|
5777
|
+
function pgCatalogName(parts) {
|
|
5778
|
+
if (parts.length === 1) return parts[0];
|
|
5779
|
+
if (parts.length === 2 && parts[0] === "pg_catalog") return parts[1];
|
|
5780
|
+
return void 0;
|
|
5781
|
+
}
|
|
5782
|
+
function reviewedFunctionName({ parts, format }) {
|
|
5783
|
+
return format === "COERCE_SQL_SYNTAX" && parts.length === 2 && parts[0] === "pg_catalog" && parts[1] === "position" ? "strpos" : pgCatalogName(parts);
|
|
5784
|
+
}
|
|
5785
|
+
function validateTree({ tree, ...scope }) {
|
|
5786
|
+
const pending = [tree];
|
|
5787
|
+
while (pending.length) {
|
|
5788
|
+
const value = pending.pop();
|
|
5789
|
+
if (Array.isArray(value)) {
|
|
5790
|
+
pending.push(...value);
|
|
5791
|
+
continue;
|
|
5792
|
+
}
|
|
5793
|
+
if (!value || typeof value !== "object") continue;
|
|
5794
|
+
const node = obj(value);
|
|
5795
|
+
for (const [kind, child] of Object.entries(node)) {
|
|
5796
|
+
validateNode({ kind, child, scope });
|
|
5797
|
+
pending.push(child);
|
|
5798
|
+
}
|
|
5799
|
+
}
|
|
5800
|
+
}
|
|
5801
|
+
function validateNode({ kind, child, scope }) {
|
|
5802
|
+
const { fail: fail4 } = scope;
|
|
5803
|
+
if (kind.endsWith("Stmt") && kind !== "SelectStmt") fail4("only SELECT statements are permitted");
|
|
5804
|
+
if (kind === "FuncCall") {
|
|
5805
|
+
const call = obj(child);
|
|
5806
|
+
const parts = strings(call.funcname);
|
|
5807
|
+
const name2 = reviewedFunctionName({ parts, format: call.funcformat });
|
|
5808
|
+
if (!name2 || !functions.has(name2)) fail4(`only explicitly allowlisted pg_catalog functions are permitted (found '${parts.join(".")}')`);
|
|
5809
|
+
}
|
|
5810
|
+
if (kind === "typeName" || kind === "TypeName") validateType({ node: obj(child), ...scope });
|
|
5811
|
+
if (kind === "A_Expr" && pgCatalogName(strings(obj(child).name)) === void 0) fail4("only pg_catalog operators are permitted");
|
|
5812
|
+
if (kind === "SortBy" && list(obj(child).useOp).length && pgCatalogName(strings(obj(child).useOp)) === void 0) fail4("only pg_catalog sort operators are permitted");
|
|
5813
|
+
if (kind === "SQLValueFunction" && !sqlValues.has(String(obj(child).op))) fail4("unsupported SQL value expression");
|
|
5814
|
+
if (kind === "ParamRef") fail4("view queries cannot contain parameters");
|
|
5815
|
+
if (kind === "A_Indirection" && list(obj(child).indirection).some((entry) => !obj(entry).A_Indices)) {
|
|
5816
|
+
fail4("composite field notation is not permitted in managed views");
|
|
5817
|
+
}
|
|
5818
|
+
}
|
|
5819
|
+
function validateType({ node, schema, enums, fail: fail4 }) {
|
|
5820
|
+
const parts = strings(node.names);
|
|
5821
|
+
const builtin = pgCatalogName(parts);
|
|
5822
|
+
const managedEnum = parts.length === 1 && enums.includes(parts[0]) || parts.length === 2 && parts[0] === schema && enums.includes(parts[1]);
|
|
5823
|
+
if (node.setof || node.pct_type || !managedEnum && (!builtin || !scalarTypes.has(builtin))) {
|
|
5824
|
+
fail4(`casts may only use supported pg_catalog scalar types or declared enums (found '${parts.join(".")}')`);
|
|
5825
|
+
}
|
|
5826
|
+
}
|
|
5620
5827
|
|
|
5621
5828
|
// src/sql/operations.ts
|
|
5622
5829
|
function isSqlOperationDocument(path, contents) {
|
|
@@ -5632,7 +5839,13 @@ function isSqlOperationDocument(path, contents) {
|
|
|
5632
5839
|
async function compileOperationDocuments(input) {
|
|
5633
5840
|
const sqlDocuments = input.documents.filter((d) => isSqlOperationDocument(d.path, d.contents));
|
|
5634
5841
|
const result = compileNamespace({ ...input, documents: input.documents.filter((d) => !isSqlOperationDocument(d.path, d.contents)) });
|
|
5635
|
-
if (!result.ok
|
|
5842
|
+
if (!result.ok) return result;
|
|
5843
|
+
const viewDiagnostics = await managedViewDiagnostics(input.canonical);
|
|
5844
|
+
if (viewDiagnostics.length) return { ok: false, diagnostics: [...result.diagnostics, ...viewDiagnostics] };
|
|
5845
|
+
if (!sqlDocuments.length) return result;
|
|
5846
|
+
return compileSqlDocuments({ input, result, sqlDocuments });
|
|
5847
|
+
}
|
|
5848
|
+
async function compileSqlDocuments({ input, result, sqlDocuments }) {
|
|
5636
5849
|
for (const document of sqlDocuments) {
|
|
5637
5850
|
try {
|
|
5638
5851
|
const definition = defineOperation(JSON.parse(document.contents));
|
|
@@ -5726,6 +5939,18 @@ async function compileOperationDocuments(input) {
|
|
|
5726
5939
|
result.manifest.opsHash = result.artifact.opsHash;
|
|
5727
5940
|
return { ...result, artifact: normalizeIr(result.artifact) };
|
|
5728
5941
|
}
|
|
5942
|
+
async function managedViewDiagnostics(canonical) {
|
|
5943
|
+
try {
|
|
5944
|
+
await validateManagedViewQueries({ canonical });
|
|
5945
|
+
return [];
|
|
5946
|
+
} catch (error) {
|
|
5947
|
+
return [{
|
|
5948
|
+
severity: "error",
|
|
5949
|
+
code: "invalid-view-query",
|
|
5950
|
+
message: error instanceof Error ? error.message : "invalid managed view query"
|
|
5951
|
+
}];
|
|
5952
|
+
}
|
|
5953
|
+
}
|
|
5729
5954
|
|
|
5730
5955
|
// src/sql/codegen.ts
|
|
5731
5956
|
var typeOf = (type) => ["integer", "double"].includes(type) ? "number" : type === "boolean" ? "boolean" : "string";
|
|
@@ -7573,19 +7798,19 @@ function compileComparison(op, left, right, columnTypes) {
|
|
|
7573
7798
|
}
|
|
7574
7799
|
return `${compileOperand(left, right, columnTypes)} ${SQL_COMPARE_OPS[op]} ${compileOperand(right, left, columnTypes)}`;
|
|
7575
7800
|
}
|
|
7576
|
-
function compilePredicate(predicate, columnTypes) {
|
|
7801
|
+
function compilePredicate(predicate, columnTypes, options = {}) {
|
|
7577
7802
|
switch (predicate.kind) {
|
|
7578
7803
|
case "and":
|
|
7579
7804
|
case "or": {
|
|
7580
|
-
const joined = predicate.operands.map((operand) => compilePredicate(operand, columnTypes)).join(` ${predicate.kind.toUpperCase()} `);
|
|
7805
|
+
const joined = predicate.operands.map((operand) => compilePredicate(operand, columnTypes, options)).join(` ${predicate.kind.toUpperCase()} `);
|
|
7581
7806
|
return `(${joined})`;
|
|
7582
7807
|
}
|
|
7583
7808
|
case "not":
|
|
7584
|
-
return `(NOT ${compilePredicate(predicate.operand, columnTypes)})`;
|
|
7809
|
+
return `(NOT ${compilePredicate(predicate.operand, columnTypes, options)})`;
|
|
7585
7810
|
case "compare":
|
|
7586
7811
|
return `(${compileComparison(predicate.op, predicate.left, predicate.right, columnTypes)})`;
|
|
7587
7812
|
case "capability":
|
|
7588
|
-
return `(${capabilityExpression(predicate.key)})`;
|
|
7813
|
+
return `(${options.statementClaimChecks ? "SELECT " : ""}${capabilityExpression(predicate.key)})`;
|
|
7589
7814
|
}
|
|
7590
7815
|
}
|
|
7591
7816
|
|
|
@@ -7821,7 +8046,7 @@ function snapshotTableNames(snapshot, schema) {
|
|
|
7821
8046
|
// src/engine/materialized-refresh.ts
|
|
7822
8047
|
function materializedRefreshFunctions(canonical) {
|
|
7823
8048
|
const schema = ormSchemaName(canonical.namespace);
|
|
7824
|
-
const
|
|
8049
|
+
const functions2 = /* @__PURE__ */ new Map();
|
|
7825
8050
|
for (const view of canonical.views ?? []) {
|
|
7826
8051
|
const materialized = view.resolved?.materialized;
|
|
7827
8052
|
if (!materialized) continue;
|
|
@@ -7830,7 +8055,7 @@ function materializedRefreshFunctions(canonical) {
|
|
|
7830
8055
|
if (!table || pk.length === 0) {
|
|
7831
8056
|
throw new OrmDefinitionError(`materialized resolved table '${view.view}' needs its table and primary key`);
|
|
7832
8057
|
}
|
|
7833
|
-
|
|
8058
|
+
functions2.set(view.view, refreshFunctionSql({
|
|
7834
8059
|
schema,
|
|
7835
8060
|
name: view.view,
|
|
7836
8061
|
table: materialized.table,
|
|
@@ -7839,7 +8064,7 @@ function materializedRefreshFunctions(canonical) {
|
|
|
7839
8064
|
pk
|
|
7840
8065
|
}));
|
|
7841
8066
|
}
|
|
7842
|
-
return
|
|
8067
|
+
return functions2;
|
|
7843
8068
|
}
|
|
7844
8069
|
function refreshFunctionSql({ schema, name: name2, table, compute, columns, pk }) {
|
|
7845
8070
|
const fn = `${quoteIdentifier(schema)}.${quoteIdentifier(materializedRefreshFunctionName(name2))}`;
|
|
@@ -8483,6 +8708,7 @@ async function planWithViewRecreation({
|
|
|
8483
8708
|
previousSnapshot,
|
|
8484
8709
|
viewRecreation
|
|
8485
8710
|
}) {
|
|
8711
|
+
await validateManagedViewQueries({ canonical });
|
|
8486
8712
|
const schema = ormSchemaName(canonical.namespace);
|
|
8487
8713
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
8488
8714
|
assertValidViewDependencies(canonical);
|
|
@@ -9773,6 +9999,9 @@ function generateSdl(canonical, options = {}) {
|
|
|
9773
9999
|
blocks.push(
|
|
9774
10000
|
'"""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'
|
|
9775
10001
|
);
|
|
10002
|
+
if (!options.omitReadBudgetDirective) blocks.push(
|
|
10003
|
+
'"""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'
|
|
10004
|
+
);
|
|
9776
10005
|
blocks.push("directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT");
|
|
9777
10006
|
blocks.push(
|
|
9778
10007
|
'"""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'
|
|
@@ -11977,7 +12206,7 @@ function emitSyncedTable(table) {
|
|
|
11977
12206
|
lines.push(" },", " {");
|
|
11978
12207
|
lines.push(` source: ${quote(table.source ?? "")},`);
|
|
11979
12208
|
if (table.description !== void 0) lines.push(` description: ${quote(table.description)},`);
|
|
11980
|
-
if (singlePrimaryKey === void 0 && table.primaryKey
|
|
12209
|
+
if (singlePrimaryKey === void 0 && table.primaryKey !== void 0) {
|
|
11981
12210
|
lines.push(` primaryKey: ${jsonLiteral(table.primaryKey, 4)},`);
|
|
11982
12211
|
}
|
|
11983
12212
|
if (table.links.length > 0) {
|