@sequenceholdings/orm 2.10.0 → 2.11.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 +71 -0
- package/dist/authoring.cjs +796 -7
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +11 -2
- package/dist/authoring.d.ts +11 -2
- package/dist/authoring.js +796 -7
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +777 -6
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +43 -43
- package/dist/canonical.d.ts +43 -43
- package/dist/canonical.js +777 -6
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +7561 -7502
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +7561 -7502
- package/dist/cli.js.map +1 -1
- package/dist/compiler.cjs +801 -33
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +1 -1
- package/dist/compiler.d.ts +1 -1
- package/dist/compiler.js +801 -33
- package/dist/compiler.js.map +1 -1
- package/dist/index.cjs +3716 -3445
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -30
- package/dist/index.d.ts +95 -30
- package/dist/index.js +3711 -3444
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +778 -7
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +58 -58
- package/dist/ir.d.ts +58 -58
- package/dist/ir.js +778 -7
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +793 -23
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.js +793 -23
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +786 -17
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.js +786 -17
- package/dist/sdl.js.map +1 -1
- package/package.json +1 -1
- package/dist/{definition-B99BsOAO.d.cts → definition-CevES7lM.d.cts} +2 -2
- package/dist/{definition-B99BsOAO.d.ts → definition-CevES7lM.d.ts} +2 -2
package/dist/authoring.cjs
CHANGED
|
@@ -158,9 +158,13 @@ var NAMESPACE_PATTERN = /^(?!(?:events|native_sources)$)[a-z][a-z0-9_]{0,40}$/;
|
|
|
158
158
|
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
159
159
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
160
160
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
161
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
162
|
+
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
161
163
|
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
|
|
162
164
|
var SYNCED_SOURCE_PATTERN = QUALIFIED_TABLE_PATTERN;
|
|
163
165
|
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
166
|
+
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
167
|
+
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
164
168
|
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
165
169
|
var CELL_PART_SUFFIX = {
|
|
166
170
|
at: "__at",
|
|
@@ -185,6 +189,13 @@ var OrmDefinitionError = class extends Error {
|
|
|
185
189
|
}
|
|
186
190
|
name = "OrmDefinitionError";
|
|
187
191
|
};
|
|
192
|
+
var PredicateParseError = class extends Error {
|
|
193
|
+
constructor(message, position) {
|
|
194
|
+
super(`${message} (at position ${position})`);
|
|
195
|
+
this.position = position;
|
|
196
|
+
}
|
|
197
|
+
name = "PredicateParseError";
|
|
198
|
+
};
|
|
188
199
|
|
|
189
200
|
// src/authoring/registry.ts
|
|
190
201
|
var CONTEXT_STACK_KEY = Symbol.for("sequence.orm.authoring.context-stack.v2");
|
|
@@ -403,12 +414,28 @@ function defineSyncedTable(name2, columnsOrConfig, maybeConfig) {
|
|
|
403
414
|
);
|
|
404
415
|
}
|
|
405
416
|
}
|
|
417
|
+
for (const index of config.indexes ?? []) {
|
|
418
|
+
const label = index.name ?? index.columns.join(", ");
|
|
419
|
+
if (entries.length === 0) {
|
|
420
|
+
throw new OrmDefinitionError(
|
|
421
|
+
`synced table "${name2}": index (${label}) requires declared columns \u2014 only a typed synced table can declare indexes`,
|
|
422
|
+
file
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
if (index.unique === true) {
|
|
426
|
+
throw new OrmDefinitionError(
|
|
427
|
+
`synced table "${name2}": index (${label}) cannot be unique \u2014 the primary key already identifies a row, and a unique index can fail the sync`,
|
|
428
|
+
file
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
406
432
|
const tableConfig = {};
|
|
407
433
|
if (config.description !== void 0) tableConfig.description = config.description;
|
|
408
434
|
if (config.primaryKey !== void 0) tableConfig.primaryKey = config.primaryKey;
|
|
409
435
|
if (config.links !== void 0) tableConfig.links = config.links;
|
|
410
436
|
if (config.policies !== void 0) tableConfig.policies = { read: config.policies.read };
|
|
411
437
|
if (config.internal === true) tableConfig.internal = true;
|
|
438
|
+
if (config.indexes !== void 0 && config.indexes.length > 0) tableConfig.indexes = config.indexes;
|
|
412
439
|
const table = {
|
|
413
440
|
kind: TABLE_BRAND,
|
|
414
441
|
tableName: name2,
|
|
@@ -1110,6 +1137,732 @@ var Parser = class {
|
|
|
1110
1137
|
function compileCheckExpression(source, columns) {
|
|
1111
1138
|
return new Parser(tokenize(source), columns).parse();
|
|
1112
1139
|
}
|
|
1140
|
+
|
|
1141
|
+
// src/predicate/ast.ts
|
|
1142
|
+
var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
1143
|
+
function walkPredicate(predicate, visit) {
|
|
1144
|
+
visit(predicate);
|
|
1145
|
+
if (predicate.kind === "and" || predicate.kind === "or") {
|
|
1146
|
+
for (const operand of predicate.operands) walkPredicate(operand, visit);
|
|
1147
|
+
}
|
|
1148
|
+
if (predicate.kind === "not") walkPredicate(predicate.operand, visit);
|
|
1149
|
+
}
|
|
1150
|
+
function predicateOperands(predicate) {
|
|
1151
|
+
const operands = [];
|
|
1152
|
+
walkPredicate(predicate, (node) => {
|
|
1153
|
+
if (node.kind === "compare") operands.push(node.left, node.right);
|
|
1154
|
+
});
|
|
1155
|
+
return operands;
|
|
1156
|
+
}
|
|
1157
|
+
function referencedColumns(predicate) {
|
|
1158
|
+
const names = predicateOperands(predicate).flatMap(
|
|
1159
|
+
(operand) => operand.kind === "column" ? [operand.name] : []
|
|
1160
|
+
);
|
|
1161
|
+
return [...new Set(names)];
|
|
1162
|
+
}
|
|
1163
|
+
var identifierSchema = zod.z.string().max(MAX_IDENTIFIER_LENGTH).regex(IDENTIFIER_PATTERN, "must be a lowercase identifier (a-z, 0-9, _)");
|
|
1164
|
+
var namespaceSchema = zod.z.string().regex(NAMESPACE_PATTERN, "must be lowercase, start with a letter, and be at most 41 characters");
|
|
1165
|
+
var tableReferenceSchema = zod.z.string().refine(
|
|
1166
|
+
(value) => IDENTIFIER_PATTERN.test(value) || QUALIFIED_TABLE_PATTERN.test(value),
|
|
1167
|
+
"must be a table name or a schema-qualified table reference"
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
// src/schema/columns.ts
|
|
1171
|
+
var COLUMN_TYPES = [
|
|
1172
|
+
"uuid",
|
|
1173
|
+
"text",
|
|
1174
|
+
"integer",
|
|
1175
|
+
"bigint",
|
|
1176
|
+
"numeric",
|
|
1177
|
+
"double",
|
|
1178
|
+
"boolean",
|
|
1179
|
+
"timestamptz",
|
|
1180
|
+
"date",
|
|
1181
|
+
"jsonb"
|
|
1182
|
+
];
|
|
1183
|
+
var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
|
|
1184
|
+
var FUNCTION_DEFAULTS = {
|
|
1185
|
+
"gen_random_uuid()": ["uuid"],
|
|
1186
|
+
"now()": ["timestamptz", "date"]
|
|
1187
|
+
};
|
|
1188
|
+
var CAST_PATTERN = /^([a-z][a-z0-9_]*)::([a-z]+)$/;
|
|
1189
|
+
function defaultIssue(value, type) {
|
|
1190
|
+
if (typeof value === "boolean") return type === "boolean" ? null : `boolean default is invalid for type '${type}'`;
|
|
1191
|
+
if (typeof value === "number") {
|
|
1192
|
+
if (!["integer", "bigint", "numeric", "double"].includes(type)) {
|
|
1193
|
+
return `numeric default is invalid for type '${type}'`;
|
|
1194
|
+
}
|
|
1195
|
+
if (["integer", "bigint"].includes(type)) {
|
|
1196
|
+
if (!Number.isInteger(value)) return `default for type '${type}' must be an integer`;
|
|
1197
|
+
if (!Number.isSafeInteger(value)) {
|
|
1198
|
+
return `default for type '${type}' exceeds the safe integer range \u2014 YAML numbers lose precision past 2^53`;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (type === "numeric" && Math.abs(value) > Number.MAX_SAFE_INTEGER) {
|
|
1202
|
+
return "numeric default exceeds the safe float range \u2014 write it as a string (e.g. '9007199254740993') to keep exact precision";
|
|
1203
|
+
}
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
const allowedTypes = FUNCTION_DEFAULTS[value];
|
|
1207
|
+
if (allowedTypes) return allowedTypes.includes(type) ? null : `default '${value}' is invalid for type '${type}'`;
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
var columnSchema = zod.z.strictObject({
|
|
1211
|
+
type: columnTypeSchema,
|
|
1212
|
+
primary_key: zod.z.boolean().optional(),
|
|
1213
|
+
nullable: zod.z.boolean().optional(),
|
|
1214
|
+
unique: zod.z.boolean().optional(),
|
|
1215
|
+
default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean()]).optional(),
|
|
1216
|
+
description: zod.z.string().optional(),
|
|
1217
|
+
renamed_from: identifierSchema.optional(),
|
|
1218
|
+
cast: zod.z.string().optional(),
|
|
1219
|
+
deprecated: zod.z.boolean().optional()
|
|
1220
|
+
}).superRefine((column, ctx) => {
|
|
1221
|
+
if (column.default !== void 0) {
|
|
1222
|
+
const issue = defaultIssue(column.default, column.type);
|
|
1223
|
+
if (issue) ctx.addIssue({ code: "custom", message: issue, path: ["default"] });
|
|
1224
|
+
}
|
|
1225
|
+
if (column.primary_key && column.nullable) {
|
|
1226
|
+
ctx.addIssue({ code: "custom", message: "a primary key column cannot be nullable", path: ["nullable"] });
|
|
1227
|
+
}
|
|
1228
|
+
if (column.primary_key && column.deprecated) {
|
|
1229
|
+
ctx.addIssue({ code: "custom", message: "a primary key column cannot be deprecated", path: ["deprecated"] });
|
|
1230
|
+
}
|
|
1231
|
+
if (column.cast && !CAST_PATTERN.test(column.cast)) {
|
|
1232
|
+
ctx.addIssue({ code: "custom", message: "cast must have the shape '<column>::<type>'", path: ["cast"] });
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
function castParts(cast) {
|
|
1236
|
+
const match = CAST_PATTERN.exec(cast);
|
|
1237
|
+
if (!match) return null;
|
|
1238
|
+
return { column: match[1], type: match[2] };
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// src/predicate/parse.ts
|
|
1242
|
+
var KEYWORDS = /* @__PURE__ */ new Set(["and", "or", "not", "true", "false", "null", "claim", "capability"]);
|
|
1243
|
+
var OPERATOR_PATTERN = /^(!=|<=|>=|<>|=|<|>)/;
|
|
1244
|
+
var IDENTIFIER_START = /[a-z_]/;
|
|
1245
|
+
var IDENTIFIER_CHAR = /[a-z0-9_.-]/;
|
|
1246
|
+
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
1247
|
+
function tokenize2(source) {
|
|
1248
|
+
const tokens = [];
|
|
1249
|
+
let position = 0;
|
|
1250
|
+
while (position < source.length) {
|
|
1251
|
+
const char = source[position];
|
|
1252
|
+
if (/\s/.test(char)) {
|
|
1253
|
+
position += 1;
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
if (char === "(") {
|
|
1257
|
+
tokens.push({ kind: "lparen", value: char, position });
|
|
1258
|
+
position += 1;
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
if (char === ")") {
|
|
1262
|
+
tokens.push({ kind: "rparen", value: char, position });
|
|
1263
|
+
position += 1;
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
if (char === ",") {
|
|
1267
|
+
tokens.push({ kind: "comma", value: char, position });
|
|
1268
|
+
position += 1;
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
if (char === "'") {
|
|
1272
|
+
const { value, end } = readString(source, position);
|
|
1273
|
+
tokens.push({ kind: "string", value, position });
|
|
1274
|
+
position = end;
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
const operator = OPERATOR_PATTERN.exec(source.slice(position));
|
|
1278
|
+
if (operator) {
|
|
1279
|
+
const value = operator[0] === "<>" ? "!=" : operator[0];
|
|
1280
|
+
tokens.push({ kind: "operator", value, position });
|
|
1281
|
+
position += operator[0].length;
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
const number = NUMBER_PATTERN.exec(source.slice(position));
|
|
1285
|
+
if (number && !IDENTIFIER_START.test(char)) {
|
|
1286
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
1287
|
+
position += number[0].length;
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
if (IDENTIFIER_START.test(char)) {
|
|
1291
|
+
let end = position;
|
|
1292
|
+
while (end < source.length && IDENTIFIER_CHAR.test(source[end])) end += 1;
|
|
1293
|
+
tokens.push({ kind: "identifier", value: source.slice(position, end), position });
|
|
1294
|
+
position = end;
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
throw new PredicateParseError(`unexpected character '${char}'`, position);
|
|
1298
|
+
}
|
|
1299
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
1300
|
+
return tokens;
|
|
1301
|
+
}
|
|
1302
|
+
function readString(source, start) {
|
|
1303
|
+
let value = "";
|
|
1304
|
+
let position = start + 1;
|
|
1305
|
+
while (position < source.length) {
|
|
1306
|
+
const char = source[position];
|
|
1307
|
+
if (char !== "'") {
|
|
1308
|
+
value += char;
|
|
1309
|
+
position += 1;
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
if (source[position + 1] === "'") {
|
|
1313
|
+
value += "'";
|
|
1314
|
+
position += 2;
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
return { value, end: position + 1 };
|
|
1318
|
+
}
|
|
1319
|
+
throw new PredicateParseError("unterminated string literal", start);
|
|
1320
|
+
}
|
|
1321
|
+
var Parser2 = class {
|
|
1322
|
+
constructor(tokens) {
|
|
1323
|
+
this.tokens = tokens;
|
|
1324
|
+
}
|
|
1325
|
+
index = 0;
|
|
1326
|
+
parse() {
|
|
1327
|
+
const predicate = this.parseOr();
|
|
1328
|
+
this.expect("end", "end of expression");
|
|
1329
|
+
return predicate;
|
|
1330
|
+
}
|
|
1331
|
+
parseOr() {
|
|
1332
|
+
const operands = [this.parseAnd()];
|
|
1333
|
+
while (this.matchKeyword("or")) operands.push(this.parseAnd());
|
|
1334
|
+
return operands.length === 1 ? operands[0] : { kind: "or", operands };
|
|
1335
|
+
}
|
|
1336
|
+
parseAnd() {
|
|
1337
|
+
const operands = [this.parseNot()];
|
|
1338
|
+
while (this.matchKeyword("and")) operands.push(this.parseNot());
|
|
1339
|
+
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
1340
|
+
}
|
|
1341
|
+
parseNot() {
|
|
1342
|
+
if (this.matchKeyword("not")) return { kind: "not", operand: this.parseNot() };
|
|
1343
|
+
return this.parsePrimary();
|
|
1344
|
+
}
|
|
1345
|
+
parsePrimary() {
|
|
1346
|
+
if (this.peek().kind === "lparen") {
|
|
1347
|
+
this.advance();
|
|
1348
|
+
const inner = this.parseOr();
|
|
1349
|
+
this.expect("rparen", "')'");
|
|
1350
|
+
return inner;
|
|
1351
|
+
}
|
|
1352
|
+
if (this.peekKeyword("capability")) {
|
|
1353
|
+
const capability = this.parseCapability();
|
|
1354
|
+
const next = this.peek();
|
|
1355
|
+
if (next.kind === "operator") {
|
|
1356
|
+
throw new PredicateParseError("capability() is boolean-valued and cannot appear in a comparison", next.position);
|
|
1357
|
+
}
|
|
1358
|
+
return capability;
|
|
1359
|
+
}
|
|
1360
|
+
return this.parseComparison();
|
|
1361
|
+
}
|
|
1362
|
+
parseCapability() {
|
|
1363
|
+
this.advance();
|
|
1364
|
+
const key = this.parseCallArgument("capability");
|
|
1365
|
+
if (!CAPABILITY_KEY_PATTERN.test(key)) {
|
|
1366
|
+
throw new PredicateParseError(`invalid capability key '${key}'`, this.peek().position);
|
|
1367
|
+
}
|
|
1368
|
+
return { kind: "capability", key };
|
|
1369
|
+
}
|
|
1370
|
+
parseComparison() {
|
|
1371
|
+
const left = this.parseOperand();
|
|
1372
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
1373
|
+
if (!COMPARE_OPS.includes(operator.value)) {
|
|
1374
|
+
throw new PredicateParseError(`unsupported operator '${operator.value}'`, operator.position);
|
|
1375
|
+
}
|
|
1376
|
+
const right = this.parseOperand();
|
|
1377
|
+
return { kind: "compare", op: operator.value, left, right };
|
|
1378
|
+
}
|
|
1379
|
+
parseOperand() {
|
|
1380
|
+
const token = this.peek();
|
|
1381
|
+
if (token.kind === "string") {
|
|
1382
|
+
this.advance();
|
|
1383
|
+
return { kind: "literal", value: token.value };
|
|
1384
|
+
}
|
|
1385
|
+
if (token.kind === "number") {
|
|
1386
|
+
this.advance();
|
|
1387
|
+
const value = Number(token.value);
|
|
1388
|
+
if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
1389
|
+
throw new PredicateParseError(
|
|
1390
|
+
`numeric literal '${token.value}' exceeds safe integer precision \u2014 quote it as a string`,
|
|
1391
|
+
token.position
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
return { kind: "literal", value };
|
|
1395
|
+
}
|
|
1396
|
+
if (token.kind !== "identifier") {
|
|
1397
|
+
throw new PredicateParseError(`expected an operand, got '${token.value || "end"}'`, token.position);
|
|
1398
|
+
}
|
|
1399
|
+
this.advance();
|
|
1400
|
+
if (token.value === "true" || token.value === "false") {
|
|
1401
|
+
return { kind: "literal", value: token.value === "true" };
|
|
1402
|
+
}
|
|
1403
|
+
if (token.value === "null") return { kind: "literal", value: null };
|
|
1404
|
+
if (token.value === "claim") {
|
|
1405
|
+
const key = this.parseCallArgument("claim");
|
|
1406
|
+
if (!CLAIM_KEY_PATTERN.test(key)) {
|
|
1407
|
+
throw new PredicateParseError(`invalid claim key '${key}'`, token.position);
|
|
1408
|
+
}
|
|
1409
|
+
return { kind: "claim", key };
|
|
1410
|
+
}
|
|
1411
|
+
if (token.value === "capability") {
|
|
1412
|
+
throw new PredicateParseError("capability() is boolean-valued and cannot appear in a comparison", token.position);
|
|
1413
|
+
}
|
|
1414
|
+
if (KEYWORDS.has(token.value) || !IDENTIFIER_PATTERN.test(token.value)) {
|
|
1415
|
+
throw new PredicateParseError(`invalid column reference '${token.value}'`, token.position);
|
|
1416
|
+
}
|
|
1417
|
+
return { kind: "column", name: token.value };
|
|
1418
|
+
}
|
|
1419
|
+
parseCallArgument(fn) {
|
|
1420
|
+
this.expect("lparen", `'(' after ${fn}`);
|
|
1421
|
+
const argument = this.expect("string", `a quoted key in ${fn}(...)`);
|
|
1422
|
+
this.expect("rparen", `')' after ${fn} key`);
|
|
1423
|
+
return argument.value;
|
|
1424
|
+
}
|
|
1425
|
+
peek() {
|
|
1426
|
+
return this.tokens[this.index];
|
|
1427
|
+
}
|
|
1428
|
+
peekKeyword(keyword) {
|
|
1429
|
+
const token = this.peek();
|
|
1430
|
+
return token.kind === "identifier" && token.value === keyword;
|
|
1431
|
+
}
|
|
1432
|
+
matchKeyword(keyword) {
|
|
1433
|
+
if (!this.peekKeyword(keyword)) return false;
|
|
1434
|
+
this.advance();
|
|
1435
|
+
return true;
|
|
1436
|
+
}
|
|
1437
|
+
advance() {
|
|
1438
|
+
const token = this.peek();
|
|
1439
|
+
if (token.kind !== "end") this.index += 1;
|
|
1440
|
+
return token;
|
|
1441
|
+
}
|
|
1442
|
+
expect(kind, description) {
|
|
1443
|
+
const token = this.peek();
|
|
1444
|
+
if (token.kind !== kind) {
|
|
1445
|
+
throw new PredicateParseError(`expected ${description}, got '${token.value || "end"}'`, token.position);
|
|
1446
|
+
}
|
|
1447
|
+
return this.advance();
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
function parsePredicate(source) {
|
|
1451
|
+
const trimmed = source.trim();
|
|
1452
|
+
if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
|
|
1453
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// src/schema/predicates.ts
|
|
1457
|
+
var parsedPredicateSchema = zod.z.string().transform((source, ctx) => {
|
|
1458
|
+
try {
|
|
1459
|
+
return parsePredicate(source);
|
|
1460
|
+
} catch (error) {
|
|
1461
|
+
ctx.addIssue({ code: "custom", message: error instanceof Error ? error.message : String(error) });
|
|
1462
|
+
return zod.z.NEVER;
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
// src/schema/actions.ts
|
|
1467
|
+
var EXECUTES_VERB_PATTERN = /^\s*(insert|update|delete)\b/i;
|
|
1468
|
+
var DOLLAR_QUOTE_PATTERN = /\$[a-zA-Z0-9_]*\$/;
|
|
1469
|
+
var RETURNING_PATTERN = /\breturning\b/i;
|
|
1470
|
+
var ESCAPE_STRING_PATTERN = /(?<![a-zA-Z0-9_])[Ee]'/;
|
|
1471
|
+
var MASKED_CLAIM_CALL_PATTERN = /\b(?:claim|capability)\(\s*''\s*\)/g;
|
|
1472
|
+
var BARE_CLAIM_CALL_PATTERN = /\b(?:claim|capability)\s*\(/;
|
|
1473
|
+
var EXISTS_ENTRY_PATTERN = /^exists\(\s*'([^']+)'\s*,\s*([a-z][a-z0-9_]*)\s*\)$/;
|
|
1474
|
+
var NAMED_PARAM_PATTERN = /(?<!:):([a-z][a-z0-9_]*)/g;
|
|
1475
|
+
var QUOTED_STRING_PATTERN = /'(?:[^']|'')*'/g;
|
|
1476
|
+
var SQL_COMMENT_PATTERN = /--|\/\*/;
|
|
1477
|
+
var actionParamSchema = zod.z.strictObject({
|
|
1478
|
+
type: columnTypeSchema,
|
|
1479
|
+
optional: zod.z.boolean().optional(),
|
|
1480
|
+
description: zod.z.string().optional()
|
|
1481
|
+
});
|
|
1482
|
+
var validatesEntrySchema = zod.z.string().transform((source, ctx) => {
|
|
1483
|
+
const match = EXISTS_ENTRY_PATTERN.exec(source.trim());
|
|
1484
|
+
if (!match) {
|
|
1485
|
+
ctx.addIssue({ code: "custom", message: "validates entries must have the shape exists('<table>', <param>)" });
|
|
1486
|
+
return zod.z.NEVER;
|
|
1487
|
+
}
|
|
1488
|
+
const table = tableReferenceSchema.safeParse(match[1]);
|
|
1489
|
+
if (!table.success) {
|
|
1490
|
+
ctx.addIssue({ code: "custom", message: `invalid table reference '${match[1]}'` });
|
|
1491
|
+
return zod.z.NEVER;
|
|
1492
|
+
}
|
|
1493
|
+
return { source: source.trim(), table: table.data, param: match[2] };
|
|
1494
|
+
});
|
|
1495
|
+
var AUDIT_ACTIONS = ["create", "update", "delete"];
|
|
1496
|
+
var auditSchema = zod.z.strictObject({
|
|
1497
|
+
action: zod.z.enum(AUDIT_ACTIONS),
|
|
1498
|
+
resource: identifierSchema.optional()
|
|
1499
|
+
});
|
|
1500
|
+
var actionSchema = zod.z.strictObject({
|
|
1501
|
+
description: zod.z.string().optional(),
|
|
1502
|
+
requires: parsedPredicateSchema.optional(),
|
|
1503
|
+
params: zod.z.record(identifierSchema, actionParamSchema).optional(),
|
|
1504
|
+
validates: zod.z.array(validatesEntrySchema).optional(),
|
|
1505
|
+
// One statement, or an ordered list run atomically in the action function.
|
|
1506
|
+
executes: zod.z.union([zod.z.string().min(1), zod.z.array(zod.z.string().min(1)).min(1)]),
|
|
1507
|
+
// Declares the columns the action's RETURNING yields. Present → the
|
|
1508
|
+
// function returns those rows (typed in the client); absent → the action
|
|
1509
|
+
// reports rows_affected only. The column names/types must match the
|
|
1510
|
+
// RETURNING list, which Postgres enforces at CREATE FUNCTION.
|
|
1511
|
+
returns: zod.z.record(identifierSchema, columnTypeSchema).optional(),
|
|
1512
|
+
audit: auditSchema.optional()
|
|
1513
|
+
}).superRefine((action, ctx) => {
|
|
1514
|
+
const params = Object.keys(action.params ?? {});
|
|
1515
|
+
if (action.requires && referencedColumns(action.requires.ast).length > 0) {
|
|
1516
|
+
ctx.addIssue({
|
|
1517
|
+
code: "custom",
|
|
1518
|
+
message: "requires may reference claims and capabilities only, not columns",
|
|
1519
|
+
path: ["requires"]
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
for (const issue of executesIssues(action.executes, new Set(params), action.returns !== void 0)) {
|
|
1523
|
+
ctx.addIssue({ code: "custom", message: issue, path: ["executes"] });
|
|
1524
|
+
}
|
|
1525
|
+
if (action.returns && Object.keys(action.returns).length === 0) {
|
|
1526
|
+
ctx.addIssue({ code: "custom", message: "returns must declare at least one column", path: ["returns"] });
|
|
1527
|
+
}
|
|
1528
|
+
for (const [index, entry] of (action.validates ?? []).entries()) {
|
|
1529
|
+
if (!params.includes(entry.param)) {
|
|
1530
|
+
ctx.addIssue({
|
|
1531
|
+
code: "custom",
|
|
1532
|
+
message: `validates references undeclared param '${entry.param}'`,
|
|
1533
|
+
path: ["validates", index]
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
function extractNamedParams(sql4) {
|
|
1539
|
+
const withoutStrings = sql4.replace(QUOTED_STRING_PATTERN, "''");
|
|
1540
|
+
const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
|
|
1541
|
+
return [...new Set(names)];
|
|
1542
|
+
}
|
|
1543
|
+
function actionStatementSources(executes) {
|
|
1544
|
+
return (Array.isArray(executes) ? executes : [executes]).map((statement) => statement.trim());
|
|
1545
|
+
}
|
|
1546
|
+
function executesIssues(executes, declaredParams, hasReturns) {
|
|
1547
|
+
const issues = [];
|
|
1548
|
+
const statements = actionStatementSources(executes);
|
|
1549
|
+
statements.forEach((statement, index) => {
|
|
1550
|
+
const withoutStrings = statement.replace(QUOTED_STRING_PATTERN, "''");
|
|
1551
|
+
const at = statements.length > 1 ? ` (statement ${index + 1})` : "";
|
|
1552
|
+
if (!EXECUTES_VERB_PATTERN.test(statement)) issues.push(`executes must be INSERT, UPDATE, or DELETE${at}`);
|
|
1553
|
+
if (withoutStrings.replace(/;\s*$/, "").includes(";")) issues.push(`each executes entry must be a single statement${at}`);
|
|
1554
|
+
if (SQL_COMMENT_PATTERN.test(withoutStrings)) issues.push(`executes must not contain SQL comments${at}`);
|
|
1555
|
+
if (DOLLAR_QUOTE_PATTERN.test(withoutStrings)) issues.push(`executes must not use dollar-quoted strings${at}`);
|
|
1556
|
+
if (ESCAPE_STRING_PATTERN.test(withoutStrings)) issues.push(`executes must not use escape strings (E'\u2026')${at}`);
|
|
1557
|
+
if (withoutStrings.includes('"')) issues.push(`executes must not use double-quoted identifiers${at}`);
|
|
1558
|
+
if (BARE_CLAIM_CALL_PATTERN.test(withoutStrings.replace(MASKED_CLAIM_CALL_PATTERN, ""))) {
|
|
1559
|
+
issues.push(`executes has a malformed claim()/capability() call \u2014 use e.g. claim('sub')${at}`);
|
|
1560
|
+
}
|
|
1561
|
+
if (RETURNING_PATTERN.test(withoutStrings) && !(hasReturns && index === statements.length - 1)) {
|
|
1562
|
+
issues.push(
|
|
1563
|
+
hasReturns ? `only the last executes statement may use RETURNING${at}` : `executes must not use RETURNING unless the action declares returns${at}`
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
const last = statements.at(-1);
|
|
1568
|
+
if (hasReturns && last && !RETURNING_PATTERN.test(last.replace(QUOTED_STRING_PATTERN, "''"))) {
|
|
1569
|
+
issues.push("the last executes statement must use RETURNING to populate the declared returns columns");
|
|
1570
|
+
}
|
|
1571
|
+
const undeclared = extractNamedParams(statements.join("\n")).filter((name2) => !declaredParams.has(name2));
|
|
1572
|
+
if (undeclared.length > 0) issues.push(`executes references undeclared params: ${undeclared.join(", ")}`);
|
|
1573
|
+
return issues;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// src/schema/table.ts
|
|
1577
|
+
var linkSchema = zod.z.strictObject({
|
|
1578
|
+
to: tableReferenceSchema,
|
|
1579
|
+
on: identifierSchema,
|
|
1580
|
+
enforce: zod.z.boolean().optional(),
|
|
1581
|
+
description: zod.z.string().optional()
|
|
1582
|
+
});
|
|
1583
|
+
var indexSchema = zod.z.union([
|
|
1584
|
+
zod.z.array(identifierSchema).min(1),
|
|
1585
|
+
zod.z.strictObject({
|
|
1586
|
+
columns: zod.z.array(identifierSchema).min(1),
|
|
1587
|
+
unique: zod.z.boolean().optional(),
|
|
1588
|
+
name: identifierSchema.optional()
|
|
1589
|
+
})
|
|
1590
|
+
]).transform((index) => Array.isArray(index) ? { columns: index } : index);
|
|
1591
|
+
var policiesSchema = zod.z.strictObject({
|
|
1592
|
+
read: parsedPredicateSchema.optional(),
|
|
1593
|
+
write: parsedPredicateSchema.optional(),
|
|
1594
|
+
insert: parsedPredicateSchema.optional(),
|
|
1595
|
+
update: parsedPredicateSchema.optional()
|
|
1596
|
+
});
|
|
1597
|
+
zod.z.strictObject({
|
|
1598
|
+
namespace: namespaceSchema,
|
|
1599
|
+
table: identifierSchema,
|
|
1600
|
+
description: zod.z.string().optional(),
|
|
1601
|
+
columns: zod.z.record(identifierSchema, columnSchema).optional(),
|
|
1602
|
+
source: zod.z.string().regex(SYNCED_SOURCE_PATTERN, "source must be a qualified lowercase schema.table identifier").optional(),
|
|
1603
|
+
links: zod.z.record(identifierSchema, linkSchema).optional(),
|
|
1604
|
+
indexes: zod.z.array(indexSchema).optional(),
|
|
1605
|
+
policies: policiesSchema.optional(),
|
|
1606
|
+
actions: zod.z.record(identifierSchema, actionSchema).optional()
|
|
1607
|
+
}).superRefine((table, ctx) => {
|
|
1608
|
+
if (RESERVED_TABLE_NAMES.has(table.table)) {
|
|
1609
|
+
ctx.addIssue({ code: "custom", message: `'${table.table}' is a reserved table name`, path: ["table"] });
|
|
1610
|
+
}
|
|
1611
|
+
for (const name2 of Object.keys(table.columns ?? {})) {
|
|
1612
|
+
if (RESERVED_COLUMN_NAMES.has(name2)) {
|
|
1613
|
+
ctx.addIssue({
|
|
1614
|
+
code: "custom",
|
|
1615
|
+
message: `column '${name2}' collides with the Data API's reserved query parameters (select, order, limit, offset) \u2014 it could never be filtered`,
|
|
1616
|
+
path: ["columns", name2]
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
if (table.source) validateSyncedTable(table, ctx);
|
|
1621
|
+
else validateColumnTable(table, ctx);
|
|
1622
|
+
validateLinks(table, ctx);
|
|
1623
|
+
validatePolicyColumns(table, ctx);
|
|
1624
|
+
validateDerivedNameLengths(table, ctx);
|
|
1625
|
+
});
|
|
1626
|
+
var indexName = (table, definition) => definition.name ?? `ix_${table}_${definition.columns.join("_")}`;
|
|
1627
|
+
var policyName = (table, suffix) => `${table}_${suffix}`;
|
|
1628
|
+
var foreignKeyName = (table, link) => `fk_${table}_${link}`;
|
|
1629
|
+
function validateSyncedTable(table, ctx) {
|
|
1630
|
+
const forbidden = [
|
|
1631
|
+
["actions", "synced tables are read-only; actions are not allowed"],
|
|
1632
|
+
["indexes", "v1 synced tables cannot declare indexes; declare them with defineSyncedTable({ indexes }) in a v2 definition"]
|
|
1633
|
+
];
|
|
1634
|
+
for (const [field, message] of forbidden) {
|
|
1635
|
+
if (table[field] !== void 0) ctx.addIssue({ code: "custom", message, path: [field] });
|
|
1636
|
+
}
|
|
1637
|
+
if (table.policies?.write) {
|
|
1638
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
|
|
1639
|
+
}
|
|
1640
|
+
if (table.policies?.insert) {
|
|
1641
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "insert"] });
|
|
1642
|
+
}
|
|
1643
|
+
if (table.policies?.update) {
|
|
1644
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "update"] });
|
|
1645
|
+
}
|
|
1646
|
+
const columns = table.columns ?? {};
|
|
1647
|
+
const names = Object.keys(columns);
|
|
1648
|
+
if (names.length === 0) return;
|
|
1649
|
+
const primaryKeys = names.filter((name2) => columns[name2]?.primary_key);
|
|
1650
|
+
if (primaryKeys.length !== 1) {
|
|
1651
|
+
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
1652
|
+
}
|
|
1653
|
+
validateRenames(columns, names, ctx);
|
|
1654
|
+
for (const name2 of names) {
|
|
1655
|
+
const column = columns[name2];
|
|
1656
|
+
if (!column) continue;
|
|
1657
|
+
const cast = column.cast ? castParts(column.cast) : null;
|
|
1658
|
+
if (column.cast && cast?.column !== name2) {
|
|
1659
|
+
ctx.addIssue({
|
|
1660
|
+
code: "custom",
|
|
1661
|
+
message: "cast must reference the column it is declared on",
|
|
1662
|
+
path: ["columns", name2, "cast"]
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
if (cast && cast.type !== column.type) {
|
|
1666
|
+
ctx.addIssue({
|
|
1667
|
+
code: "custom",
|
|
1668
|
+
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
1669
|
+
path: ["columns", name2, "cast"]
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
function validateRenames(columns, names, ctx) {
|
|
1675
|
+
const renameSources = /* @__PURE__ */ new Map();
|
|
1676
|
+
for (const name2 of names) {
|
|
1677
|
+
const renamedFrom = columns[name2]?.renamed_from;
|
|
1678
|
+
if (!renamedFrom) continue;
|
|
1679
|
+
if (renamedFrom === name2 || names.includes(renamedFrom)) {
|
|
1680
|
+
ctx.addIssue({
|
|
1681
|
+
code: "custom",
|
|
1682
|
+
message: "renamed_from must name a column that no longer exists",
|
|
1683
|
+
path: ["columns", name2, "renamed_from"]
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
const claimed = renameSources.get(renamedFrom);
|
|
1687
|
+
if (claimed) {
|
|
1688
|
+
ctx.addIssue({
|
|
1689
|
+
code: "custom",
|
|
1690
|
+
message: `renamed_from '${renamedFrom}' is already claimed by '${claimed}'`,
|
|
1691
|
+
path: ["columns", name2, "renamed_from"]
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
renameSources.set(renamedFrom, name2);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
function validateColumnTable(table, ctx) {
|
|
1698
|
+
const columns = table.columns ?? {};
|
|
1699
|
+
const names = Object.keys(columns);
|
|
1700
|
+
if (names.length === 0) {
|
|
1701
|
+
ctx.addIssue({ code: "custom", message: "a table must declare columns or a source", path: ["columns"] });
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
const primaryKeys = names.filter((name2) => columns[name2]?.primary_key);
|
|
1705
|
+
if (primaryKeys.length !== 1) {
|
|
1706
|
+
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
1707
|
+
}
|
|
1708
|
+
validateRenames(columns, names, ctx);
|
|
1709
|
+
for (const name2 of names) {
|
|
1710
|
+
const column = columns[name2];
|
|
1711
|
+
if (!column) continue;
|
|
1712
|
+
const cast = column.cast ? castParts(column.cast) : null;
|
|
1713
|
+
if (column.cast && cast?.column !== name2) {
|
|
1714
|
+
ctx.addIssue({
|
|
1715
|
+
code: "custom",
|
|
1716
|
+
message: "cast must reference the column it is declared on",
|
|
1717
|
+
path: ["columns", name2, "cast"]
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
if (cast && cast.type !== column.type) {
|
|
1721
|
+
ctx.addIssue({
|
|
1722
|
+
code: "custom",
|
|
1723
|
+
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
1724
|
+
path: ["columns", name2, "cast"]
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
for (const [position, index] of (table.indexes ?? []).entries()) {
|
|
1729
|
+
if (new Set(index.columns).size !== index.columns.length) {
|
|
1730
|
+
ctx.addIssue({
|
|
1731
|
+
code: "custom",
|
|
1732
|
+
message: "index names a column more than once",
|
|
1733
|
+
path: ["indexes", position]
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
for (const column of index.columns) {
|
|
1737
|
+
if (!names.includes(column)) {
|
|
1738
|
+
ctx.addIssue({
|
|
1739
|
+
code: "custom",
|
|
1740
|
+
message: `index references unknown column '${column}'`,
|
|
1741
|
+
path: ["indexes", position]
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
function validateLinks(table, ctx) {
|
|
1748
|
+
const columnNames = Object.keys(table.columns ?? {});
|
|
1749
|
+
for (const [name2, link] of Object.entries(table.links ?? {})) {
|
|
1750
|
+
if (columnNames.length > 0 && !columnNames.includes(link.on)) {
|
|
1751
|
+
ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name2, "on"] });
|
|
1752
|
+
}
|
|
1753
|
+
if (link.enforce && link.to.includes(".")) {
|
|
1754
|
+
ctx.addIssue({
|
|
1755
|
+
code: "custom",
|
|
1756
|
+
message: "enforce is only valid for links inside the namespace; cross-boundary links stay soft",
|
|
1757
|
+
path: ["links", name2, "enforce"]
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
if (link.enforce && table.source) {
|
|
1761
|
+
ctx.addIssue({
|
|
1762
|
+
code: "custom",
|
|
1763
|
+
message: "a synced table cannot enforce foreign keys \u2014 its DDL is owned by the sync",
|
|
1764
|
+
path: ["links", name2, "enforce"]
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
function validateDerivedNameLengths(table, ctx) {
|
|
1770
|
+
const worstPolicy = policyName(table.table, "insert");
|
|
1771
|
+
if (!table.source && worstPolicy.length > MAX_IDENTIFIER_LENGTH) {
|
|
1772
|
+
ctx.addIssue({
|
|
1773
|
+
code: "custom",
|
|
1774
|
+
message: `table name is too long \u2014 the derived policy name '${worstPolicy}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit`,
|
|
1775
|
+
path: ["table"]
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
for (const [position, index] of (table.indexes ?? []).entries()) {
|
|
1779
|
+
const name2 = indexName(table.table, index);
|
|
1780
|
+
if (name2.length > MAX_IDENTIFIER_LENGTH) {
|
|
1781
|
+
ctx.addIssue({
|
|
1782
|
+
code: "custom",
|
|
1783
|
+
message: `derived index name '${name2}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 set a shorter explicit name`,
|
|
1784
|
+
path: ["indexes", position]
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
for (const [name2, link] of Object.entries(table.links ?? {})) {
|
|
1789
|
+
const constraint = foreignKeyName(table.table, name2);
|
|
1790
|
+
if (link.enforce && constraint.length > MAX_IDENTIFIER_LENGTH) {
|
|
1791
|
+
ctx.addIssue({
|
|
1792
|
+
code: "custom",
|
|
1793
|
+
message: `derived foreign-key name '${constraint}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the link or table name`,
|
|
1794
|
+
path: ["links", name2]
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
for (const name2 of Object.keys(table.actions ?? {})) {
|
|
1799
|
+
const fnName = `action_${name2}`;
|
|
1800
|
+
if (fnName.length > MAX_IDENTIFIER_LENGTH) {
|
|
1801
|
+
ctx.addIssue({
|
|
1802
|
+
code: "custom",
|
|
1803
|
+
message: `derived action function name '${fnName}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the action name`,
|
|
1804
|
+
path: ["actions", name2]
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
var TYPE_GROUPS = {
|
|
1810
|
+
integer: "numeric",
|
|
1811
|
+
bigint: "numeric",
|
|
1812
|
+
numeric: "numeric",
|
|
1813
|
+
double: "numeric",
|
|
1814
|
+
text: "text",
|
|
1815
|
+
uuid: "uuid",
|
|
1816
|
+
date: "temporal",
|
|
1817
|
+
timestamptz: "temporal",
|
|
1818
|
+
boolean: "boolean",
|
|
1819
|
+
jsonb: "jsonb"
|
|
1820
|
+
};
|
|
1821
|
+
function validatePolicyColumns(table, ctx) {
|
|
1822
|
+
const columns = table.columns ?? {};
|
|
1823
|
+
const columnNames = Object.keys(columns);
|
|
1824
|
+
if (columnNames.length === 0) return;
|
|
1825
|
+
for (const kind of ["read", "write", "insert", "update"]) {
|
|
1826
|
+
const predicate = table.policies?.[kind];
|
|
1827
|
+
if (!predicate) continue;
|
|
1828
|
+
for (const column of referencedColumns(predicate.ast)) {
|
|
1829
|
+
if (!columnNames.includes(column)) {
|
|
1830
|
+
ctx.addIssue({
|
|
1831
|
+
code: "custom",
|
|
1832
|
+
message: `policy references unknown column '${column}'`,
|
|
1833
|
+
path: ["policies", kind]
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
walkPredicate(predicate.ast, (node) => {
|
|
1838
|
+
if (node.kind !== "compare") return;
|
|
1839
|
+
const issue = comparisonTypeIssue(node.left, node.right, columns);
|
|
1840
|
+
if (issue) ctx.addIssue({ code: "custom", message: issue, path: ["policies", kind] });
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
function comparisonTypeIssue(left, right, columns) {
|
|
1845
|
+
const groupOf = (operand) => operand.kind === "column" ? columns[operand.name] ? TYPE_GROUPS[columns[operand.name].type] : null : null;
|
|
1846
|
+
if (left.kind === "column" && right.kind === "column") {
|
|
1847
|
+
const leftGroup = groupOf(left);
|
|
1848
|
+
const rightGroup = groupOf(right);
|
|
1849
|
+
if (leftGroup && rightGroup && leftGroup !== rightGroup) {
|
|
1850
|
+
return `policy compares '${left.name}' (${columns[left.name].type}) with '${right.name}' (${columns[right.name].type}) \u2014 incomparable types`;
|
|
1851
|
+
}
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
const column = left.kind === "column" ? left : right.kind === "column" ? right : null;
|
|
1855
|
+
const literal2 = left.kind === "literal" ? left : right.kind === "literal" ? right : null;
|
|
1856
|
+
if (!column || !literal2 || literal2.value === null) return null;
|
|
1857
|
+
const columnGroup = groupOf(column);
|
|
1858
|
+
if (!columnGroup) return null;
|
|
1859
|
+
if (typeof literal2.value === "string") return null;
|
|
1860
|
+
const literalGroup = typeof literal2.value === "number" ? "numeric" : "boolean";
|
|
1861
|
+
if (literalGroup !== columnGroup) {
|
|
1862
|
+
return `policy compares '${column.name}' (${columns[column.name].type}) with a ${typeof literal2.value} literal \u2014 incomparable types`;
|
|
1863
|
+
}
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
1113
1866
|
var policyVariantSchema = zod.z.object({
|
|
1114
1867
|
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
1115
1868
|
when: zod.z.string().min(1).max(1e3),
|
|
@@ -1426,7 +2179,7 @@ var CANONICAL_FORMAT = 2;
|
|
|
1426
2179
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
1427
2180
|
var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
1428
2181
|
var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
1429
|
-
var
|
|
2182
|
+
var COLUMN_TYPES2 = [
|
|
1430
2183
|
"uuid",
|
|
1431
2184
|
"text",
|
|
1432
2185
|
"integer",
|
|
@@ -1438,7 +2191,7 @@ var COLUMN_TYPES = [
|
|
|
1438
2191
|
"date",
|
|
1439
2192
|
"jsonb"
|
|
1440
2193
|
];
|
|
1441
|
-
var
|
|
2194
|
+
var columnTypeSchema2 = zod.z.enum(COLUMN_TYPES2);
|
|
1442
2195
|
var canonicalEnumSchema = zod.z.object({
|
|
1443
2196
|
name: identifier2,
|
|
1444
2197
|
values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
|
|
@@ -1447,7 +2200,7 @@ var canonicalColumnSchema = zod.z.object({
|
|
|
1447
2200
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
1448
2201
|
name: zod.z.string().min(1).max(63),
|
|
1449
2202
|
columnName: columnIdentifier,
|
|
1450
|
-
type: zod.z.union([
|
|
2203
|
+
type: zod.z.union([columnTypeSchema2, zod.z.object({ enum: identifier2 })]),
|
|
1451
2204
|
nullable: zod.z.boolean().default(true),
|
|
1452
2205
|
unique: zod.z.boolean().default(false),
|
|
1453
2206
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
@@ -1697,7 +2450,7 @@ var canonicalActionSchema = zod.z.object({
|
|
|
1697
2450
|
name: identifier2,
|
|
1698
2451
|
requires: zod.z.string().max(1e3).optional(),
|
|
1699
2452
|
params: zod.z.record(zod.z.string(), zod.z.object({
|
|
1700
|
-
type:
|
|
2453
|
+
type: columnTypeSchema2,
|
|
1701
2454
|
nullable: zod.z.boolean().default(false)
|
|
1702
2455
|
})).default({}),
|
|
1703
2456
|
executes: zod.z.string().min(1).max(1e4),
|
|
@@ -1798,7 +2551,8 @@ var canonicalTableSchema = zod.z.object({
|
|
|
1798
2551
|
if ([table.policies.write, table.policies.insert, table.policies.update].some((policy) => policy !== void 0)) {
|
|
1799
2552
|
fail("is read-only and cannot declare write policies", ["policies"]);
|
|
1800
2553
|
}
|
|
1801
|
-
const
|
|
2554
|
+
const ownedIndexes = table.source === void 0 ? table.indexes : table.indexes.filter((index) => index.unique);
|
|
2555
|
+
const ownedCollections = [ownedIndexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
|
|
1802
2556
|
const ownedMetadata = [table.cellMetadata, table.internal, table.materializes, table.renamedFrom];
|
|
1803
2557
|
if (ownedCollections.some((items) => items !== void 0 && items.length > 0) || ownedMetadata.some((value) => value !== void 0)) {
|
|
1804
2558
|
fail("cannot declare owned storage, constraints or identity operations");
|
|
@@ -1900,6 +2654,23 @@ function trackedCellColumns(table) {
|
|
|
1900
2654
|
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
1901
2655
|
}
|
|
1902
2656
|
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
2657
|
+
function validateSyncedIndexes(table, tableIndex, ctx) {
|
|
2658
|
+
if (table.source === void 0) return;
|
|
2659
|
+
const columns = new Set(table.columns.map((column) => column.columnName));
|
|
2660
|
+
for (const [position, index] of table.indexes.entries()) {
|
|
2661
|
+
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "indexes", position], message: `synced table '${table.table}' ${message}` });
|
|
2662
|
+
if (columns.size === 0) fail("must declare columns to declare indexes");
|
|
2663
|
+
if (index.unique) fail("cannot declare a unique index");
|
|
2664
|
+
if (new Set(index.columns).size !== index.columns.length) fail("index names a column more than once");
|
|
2665
|
+
for (const column of index.columns) {
|
|
2666
|
+
if (columns.size > 0 && !columns.has(column)) fail(`index references unknown column '${column}'`);
|
|
2667
|
+
}
|
|
2668
|
+
const name2 = indexName(table.table, index);
|
|
2669
|
+
if (name2.length > MAX_IDENTIFIER_LENGTH) {
|
|
2670
|
+
fail(`derived index name '${name2}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 set a shorter explicit name`);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
1903
2674
|
function validateCellMetadata(table, index, ctx) {
|
|
1904
2675
|
const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
|
|
1905
2676
|
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
@@ -1966,12 +2737,24 @@ var localDefinitionSchema = zod.z.object({
|
|
|
1966
2737
|
}).default({ uses: [] })
|
|
1967
2738
|
}).superRefine((definition, ctx) => {
|
|
1968
2739
|
const seen = /* @__PURE__ */ new Set();
|
|
2740
|
+
const syncedIndexOwners = /* @__PURE__ */ new Map();
|
|
1969
2741
|
for (const [index, table] of definition.tables.entries()) {
|
|
1970
2742
|
if (seen.has(table.table)) {
|
|
1971
2743
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
1972
2744
|
}
|
|
1973
2745
|
seen.add(table.table);
|
|
1974
2746
|
validateCellMetadata(table, index, ctx);
|
|
2747
|
+
validateSyncedIndexes(table, index, ctx);
|
|
2748
|
+
if (table.source !== void 0) {
|
|
2749
|
+
for (const [position, entry] of table.indexes.entries()) {
|
|
2750
|
+
const name2 = indexName(table.table, entry);
|
|
2751
|
+
const owner = syncedIndexOwners.get(name2);
|
|
2752
|
+
if (owner !== void 0) {
|
|
2753
|
+
ctx.addIssue({ code: "custom", path: ["tables", index, "indexes", position], message: `synced index '${name2}' is also declared on '${owner}'` });
|
|
2754
|
+
}
|
|
2755
|
+
syncedIndexOwners.set(name2, table.table);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
1975
2758
|
if (table.source !== void 0) {
|
|
1976
2759
|
for (const [columnIndex, column] of table.columns.entries()) {
|
|
1977
2760
|
if (column.onUpdateFn !== void 0) {
|
|
@@ -2949,9 +3732,15 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
2949
3732
|
if (table.config.checks?.length) {
|
|
2950
3733
|
throw new OrmDefinitionError(`synced table "${table.tableName}" is system-owned; checks are not allowed`, table.file);
|
|
2951
3734
|
}
|
|
2952
|
-
if (table.config.indexes
|
|
3735
|
+
if (table.config.indexes?.some((index) => index.unique === true)) {
|
|
3736
|
+
throw new OrmDefinitionError(
|
|
3737
|
+
`synced table "${table.tableName}": indexes cannot be unique \u2014 the primary key already identifies a row, and a unique index can fail the sync`,
|
|
3738
|
+
table.file
|
|
3739
|
+
);
|
|
3740
|
+
}
|
|
3741
|
+
if (table.config.indexes?.length && Object.keys(table.columns).length === 0) {
|
|
2953
3742
|
throw new OrmDefinitionError(
|
|
2954
|
-
`synced table "${table.tableName}"
|
|
3743
|
+
`synced table "${table.tableName}": only a typed synced table (declared columns) can declare indexes`,
|
|
2955
3744
|
table.file
|
|
2956
3745
|
);
|
|
2957
3746
|
}
|