@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/compiler.cjs
CHANGED
|
@@ -8,12 +8,17 @@ var pgParser = require('@supabase/pg-parser');
|
|
|
8
8
|
// src/compiler-gql/artifact.ts
|
|
9
9
|
|
|
10
10
|
// src/constants.ts
|
|
11
|
+
var NAMESPACE_PATTERN = /^(?!(?:events|native_sources)$)[a-z][a-z0-9_]{0,40}$/;
|
|
11
12
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
12
13
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
13
14
|
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
15
|
+
var MAX_PERSISTED_READ_ROWS = 50001;
|
|
14
16
|
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
|
|
15
17
|
var ORM_SCHEMA_PREFIX = "orm_";
|
|
16
|
-
var ormSchemaName = (namespace) =>
|
|
18
|
+
var ormSchemaName = (namespace) => {
|
|
19
|
+
if (!NAMESPACE_PATTERN.test(namespace)) throw new Error(`invalid or reserved ORM namespace '${namespace}'`);
|
|
20
|
+
return `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
21
|
+
};
|
|
17
22
|
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
18
23
|
var CELL_PART_SUFFIX = {
|
|
19
24
|
at: "__at",
|
|
@@ -389,6 +394,7 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
389
394
|
function validateSource({ scope, source, table }) {
|
|
390
395
|
const { fail: fail4 } = scope;
|
|
391
396
|
if (source.kind === "etl" && table.source === void 0) fail4(`etl source '${source.name}' must reference a synced table`);
|
|
397
|
+
if (source.kind === "etl" && table.primaryKey?.length === 0) fail4(`etl source '${source.name}' cannot use an unkeyed synced table for resolved row identity`);
|
|
392
398
|
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
393
399
|
fail4(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
394
400
|
}
|
|
@@ -620,6 +626,163 @@ function isSingleViewQuerySql(value) {
|
|
|
620
626
|
}
|
|
621
627
|
return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
|
|
622
628
|
}
|
|
629
|
+
var VIEW_QUERY_FUNCTION_ALLOWLIST = [
|
|
630
|
+
"abs",
|
|
631
|
+
"age",
|
|
632
|
+
"array_agg",
|
|
633
|
+
"array_append",
|
|
634
|
+
"array_cat",
|
|
635
|
+
"array_dims",
|
|
636
|
+
"array_length",
|
|
637
|
+
"array_lower",
|
|
638
|
+
"array_ndims",
|
|
639
|
+
"array_position",
|
|
640
|
+
"array_positions",
|
|
641
|
+
"array_prepend",
|
|
642
|
+
"array_remove",
|
|
643
|
+
"array_replace",
|
|
644
|
+
"array_to_string",
|
|
645
|
+
"array_trim",
|
|
646
|
+
"array_upper",
|
|
647
|
+
"ascii",
|
|
648
|
+
"avg",
|
|
649
|
+
"bit_and",
|
|
650
|
+
"bit_or",
|
|
651
|
+
"bit_xor",
|
|
652
|
+
"bool_and",
|
|
653
|
+
"bool_or",
|
|
654
|
+
"btrim",
|
|
655
|
+
"cardinality",
|
|
656
|
+
"cbrt",
|
|
657
|
+
"ceil",
|
|
658
|
+
"ceiling",
|
|
659
|
+
"char_length",
|
|
660
|
+
"character_length",
|
|
661
|
+
"chr",
|
|
662
|
+
"concat",
|
|
663
|
+
"concat_ws",
|
|
664
|
+
"count",
|
|
665
|
+
"cume_dist",
|
|
666
|
+
"date_bin",
|
|
667
|
+
"date_part",
|
|
668
|
+
"date_trunc",
|
|
669
|
+
"decode",
|
|
670
|
+
"degrees",
|
|
671
|
+
"dense_rank",
|
|
672
|
+
"encode",
|
|
673
|
+
"enum_first",
|
|
674
|
+
"enum_last",
|
|
675
|
+
"enum_range",
|
|
676
|
+
"every",
|
|
677
|
+
"exp",
|
|
678
|
+
"first_value",
|
|
679
|
+
"floor",
|
|
680
|
+
"format",
|
|
681
|
+
"grouping",
|
|
682
|
+
"initcap",
|
|
683
|
+
"json_agg",
|
|
684
|
+
"json_array_length",
|
|
685
|
+
"json_build_array",
|
|
686
|
+
"json_build_object",
|
|
687
|
+
"json_extract_path",
|
|
688
|
+
"json_extract_path_text",
|
|
689
|
+
"json_object_agg",
|
|
690
|
+
"json_object_keys",
|
|
691
|
+
"json_strip_nulls",
|
|
692
|
+
"json_typeof",
|
|
693
|
+
"jsonb_agg",
|
|
694
|
+
"jsonb_array_length",
|
|
695
|
+
"jsonb_build_array",
|
|
696
|
+
"jsonb_build_object",
|
|
697
|
+
"jsonb_extract_path",
|
|
698
|
+
"jsonb_extract_path_text",
|
|
699
|
+
"jsonb_insert",
|
|
700
|
+
"jsonb_object_agg",
|
|
701
|
+
"jsonb_object_keys",
|
|
702
|
+
"jsonb_path_exists",
|
|
703
|
+
"jsonb_path_match",
|
|
704
|
+
"jsonb_path_query",
|
|
705
|
+
"jsonb_path_query_array",
|
|
706
|
+
"jsonb_path_query_first",
|
|
707
|
+
"jsonb_pretty",
|
|
708
|
+
"jsonb_set",
|
|
709
|
+
"jsonb_set_lax",
|
|
710
|
+
"jsonb_strip_nulls",
|
|
711
|
+
"jsonb_typeof",
|
|
712
|
+
"justify_days",
|
|
713
|
+
"justify_hours",
|
|
714
|
+
"justify_interval",
|
|
715
|
+
"lag",
|
|
716
|
+
"last_value",
|
|
717
|
+
"lead",
|
|
718
|
+
"left",
|
|
719
|
+
"length",
|
|
720
|
+
"ln",
|
|
721
|
+
"log",
|
|
722
|
+
"lower",
|
|
723
|
+
"lpad",
|
|
724
|
+
"ltrim",
|
|
725
|
+
"make_date",
|
|
726
|
+
"make_interval",
|
|
727
|
+
"make_time",
|
|
728
|
+
"make_timestamp",
|
|
729
|
+
"make_timestamptz",
|
|
730
|
+
"max",
|
|
731
|
+
"md5",
|
|
732
|
+
"min",
|
|
733
|
+
"mod",
|
|
734
|
+
"nth_value",
|
|
735
|
+
"ntile",
|
|
736
|
+
"octet_length",
|
|
737
|
+
"percent_rank",
|
|
738
|
+
"pi",
|
|
739
|
+
"power",
|
|
740
|
+
"quote_ident",
|
|
741
|
+
"quote_literal",
|
|
742
|
+
"quote_nullable",
|
|
743
|
+
"radians",
|
|
744
|
+
"rank",
|
|
745
|
+
"regexp_count",
|
|
746
|
+
"regexp_instr",
|
|
747
|
+
"regexp_like",
|
|
748
|
+
"regexp_match",
|
|
749
|
+
"regexp_matches",
|
|
750
|
+
"regexp_replace",
|
|
751
|
+
"regexp_split_to_array",
|
|
752
|
+
"regexp_split_to_table",
|
|
753
|
+
"repeat",
|
|
754
|
+
"replace",
|
|
755
|
+
"reverse",
|
|
756
|
+
"right",
|
|
757
|
+
"round",
|
|
758
|
+
"row_number",
|
|
759
|
+
"rpad",
|
|
760
|
+
"rtrim",
|
|
761
|
+
"sign",
|
|
762
|
+
"split_part",
|
|
763
|
+
"sqrt",
|
|
764
|
+
"starts_with",
|
|
765
|
+
"string_agg",
|
|
766
|
+
"string_to_array",
|
|
767
|
+
"strpos",
|
|
768
|
+
"substr",
|
|
769
|
+
"substring",
|
|
770
|
+
"sum",
|
|
771
|
+
"timezone",
|
|
772
|
+
"to_ascii",
|
|
773
|
+
"to_char",
|
|
774
|
+
"to_date",
|
|
775
|
+
"to_hex",
|
|
776
|
+
"to_json",
|
|
777
|
+
"to_jsonb",
|
|
778
|
+
"to_number",
|
|
779
|
+
"to_timestamp",
|
|
780
|
+
"translate",
|
|
781
|
+
"trunc",
|
|
782
|
+
"unnest",
|
|
783
|
+
"upper",
|
|
784
|
+
"width_bucket"
|
|
785
|
+
];
|
|
623
786
|
var canonicalActionSchema = zod.z.object({
|
|
624
787
|
name: identifier2,
|
|
625
788
|
requires: zod.z.string().max(1e3).optional(),
|
|
@@ -680,7 +843,7 @@ var canonicalTableSchema = zod.z.object({
|
|
|
680
843
|
/** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
|
|
681
844
|
source: zod.z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
|
|
682
845
|
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
683
|
-
primaryKey: zod.z.array(columnIdentifier).
|
|
846
|
+
primaryKey: zod.z.array(columnIdentifier).max(8).optional(),
|
|
684
847
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
685
848
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
686
849
|
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
@@ -716,6 +879,29 @@ var canonicalTableSchema = zod.z.object({
|
|
|
716
879
|
* data plane. Omitted otherwise.
|
|
717
880
|
*/
|
|
718
881
|
materializes: identifier2.optional()
|
|
882
|
+
}).superRefine((table, ctx) => {
|
|
883
|
+
if (table.primaryKey?.length !== 0) return;
|
|
884
|
+
const fail4 = (message, path = ["primaryKey"]) => ctx.addIssue({ code: "custom", path, message: `unkeyed table '${table.table}' ${message}` });
|
|
885
|
+
if (table.source === void 0 || table.columns.length === 0) {
|
|
886
|
+
fail4("requires a typed synced source");
|
|
887
|
+
}
|
|
888
|
+
if ([table.policies.write, table.policies.insert, table.policies.update].some((policy) => policy !== void 0)) {
|
|
889
|
+
fail4("is read-only and cannot declare write policies", ["policies"]);
|
|
890
|
+
}
|
|
891
|
+
const ownedCollections = [table.indexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
|
|
892
|
+
const ownedMetadata = [table.cellMetadata, table.internal, table.materializes, table.renamedFrom];
|
|
893
|
+
if (ownedCollections.some((items) => items !== void 0 && items.length > 0) || ownedMetadata.some((value) => value !== void 0)) {
|
|
894
|
+
fail4("cannot declare owned storage, constraints or identity operations");
|
|
895
|
+
}
|
|
896
|
+
for (const [index, column] of table.columns.entries()) {
|
|
897
|
+
const ownedColumnMetadata = [column.default, column.defaultFn, column.onUpdateFn, column.renamedFrom, column.cast, column.cell];
|
|
898
|
+
if (column.unique || ownedColumnMetadata.some((value) => value !== void 0)) {
|
|
899
|
+
fail4("cannot declare column uniqueness, mutation defaults or migration metadata", ["columns", index]);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
for (const [index, link] of table.links.entries()) {
|
|
903
|
+
if (link.enforce || link.kind === "toMany") fail4("cannot provide an identity for this link", ["links", index]);
|
|
904
|
+
}
|
|
719
905
|
});
|
|
720
906
|
var MAX_VIEW_QUERY = 1e5;
|
|
721
907
|
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
@@ -772,6 +958,26 @@ var canonicalViewSchema = zod.z.object({
|
|
|
772
958
|
}
|
|
773
959
|
}
|
|
774
960
|
});
|
|
961
|
+
function validateUnkeyedLinkIdentities({ definition, ctx }) {
|
|
962
|
+
const tables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
963
|
+
const relations = [
|
|
964
|
+
...definition.tables.map((table, index) => ({ kind: "tables", index, links: table.links })),
|
|
965
|
+
...(definition.views ?? []).map((view, index) => ({ kind: "views", index, links: view.links }))
|
|
966
|
+
];
|
|
967
|
+
for (const relation of relations) {
|
|
968
|
+
for (const [linkIndex, link] of relation.links.entries()) {
|
|
969
|
+
if (link.kind !== "toOne" || link.namespace !== void 0 && link.namespace !== definition.namespace) continue;
|
|
970
|
+
const target = link.target === void 0 ? void 0 : tables.get(link.target);
|
|
971
|
+
if (target?.source !== void 0 && target.primaryKey?.length === 0) {
|
|
972
|
+
ctx.addIssue({
|
|
973
|
+
code: "custom",
|
|
974
|
+
path: [relation.kind, relation.index, "links", linkIndex],
|
|
975
|
+
message: `link '${link.name}' cannot use unkeyed table '${target.table}' as a to-one identity`
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
775
981
|
var CELL_PART_TYPES = {
|
|
776
982
|
at: "timestamptz",
|
|
777
983
|
by: "text",
|
|
@@ -835,7 +1041,7 @@ var exportGateSchema = zod.z.union([zod.z.literal(true), zod.z.object({ capabili
|
|
|
835
1041
|
var relationExportSchema = zod.z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
|
|
836
1042
|
var localDefinitionSchema = zod.z.object({
|
|
837
1043
|
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
838
|
-
namespace: identifier2.max(41),
|
|
1044
|
+
namespace: identifier2.max(41).regex(NAMESPACE_PATTERN, "invalid or reserved ORM namespace"),
|
|
839
1045
|
description: zod.z.string().max(2e3).optional(),
|
|
840
1046
|
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
841
1047
|
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -869,6 +1075,7 @@ var localDefinitionSchema = zod.z.object({
|
|
|
869
1075
|
}
|
|
870
1076
|
}
|
|
871
1077
|
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1078
|
+
validateUnkeyedLinkIdentities({ definition, ctx });
|
|
872
1079
|
const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
|
|
873
1080
|
for (const name2 of Object.keys(definition.exports ?? {})) {
|
|
874
1081
|
if (!relations.has(name2)) ctx.addIssue({ code: "custom", path: ["exports", name2], message: `export '${name2}' is not a local object` });
|
|
@@ -1191,7 +1398,7 @@ var limitIrSchema = zod.z.object({
|
|
|
1191
1398
|
variable: zod.z.string().optional(),
|
|
1192
1399
|
value: zod.z.number().int().positive().optional(),
|
|
1193
1400
|
default: zod.z.number().int().positive(),
|
|
1194
|
-
max: zod.z.number().int().positive()
|
|
1401
|
+
max: zod.z.number().int().positive().max(MAX_PERSISTED_READ_ROWS)
|
|
1195
1402
|
});
|
|
1196
1403
|
var selectionIrSchema = zod.z.lazy(
|
|
1197
1404
|
() => zod.z.object({
|
|
@@ -1856,6 +2063,7 @@ var Parser2 = class {
|
|
|
1856
2063
|
return node;
|
|
1857
2064
|
}
|
|
1858
2065
|
if (this.peekKeyword("exists")) return this.parseExists();
|
|
2066
|
+
if (this.peekKeyword("visible") && this.tokens[this.index + 1]?.kind === "lparen") return this.parseVisible();
|
|
1859
2067
|
return this.parseComparison();
|
|
1860
2068
|
}
|
|
1861
2069
|
parseBooleanCall(fn) {
|
|
@@ -1875,6 +2083,13 @@ var Parser2 = class {
|
|
|
1875
2083
|
this.expect("rparen", "')' closing exists(...)");
|
|
1876
2084
|
return { kind: "exists", path, where };
|
|
1877
2085
|
}
|
|
2086
|
+
parseVisible() {
|
|
2087
|
+
this.advance();
|
|
2088
|
+
this.expect("lparen", "'(' after visible");
|
|
2089
|
+
const path = this.parsePath("a link path in visible(...)");
|
|
2090
|
+
this.expect("rparen", "')' closing visible(...)");
|
|
2091
|
+
return { kind: "visible", path };
|
|
2092
|
+
}
|
|
1878
2093
|
parseComparison() {
|
|
1879
2094
|
const left = this.parseOperand();
|
|
1880
2095
|
const operator = this.expect("operator", "a comparison operator");
|
|
@@ -1990,6 +2205,24 @@ function parsePolicy(source) {
|
|
|
1990
2205
|
|
|
1991
2206
|
// src/predicate-v2/compile.ts
|
|
1992
2207
|
var POLICY_MAX_LINK_DEPTH = 3;
|
|
2208
|
+
function scopeToNamespace(pred, namespace) {
|
|
2209
|
+
switch (pred.kind) {
|
|
2210
|
+
case "capability":
|
|
2211
|
+
case "role":
|
|
2212
|
+
return pred.namespace ? pred : { ...pred, namespace };
|
|
2213
|
+
case "and":
|
|
2214
|
+
case "or":
|
|
2215
|
+
return { ...pred, operands: pred.operands.map((operand) => scopeToNamespace(operand, namespace)) };
|
|
2216
|
+
case "not":
|
|
2217
|
+
return { ...pred, operand: scopeToNamespace(pred.operand, namespace) };
|
|
2218
|
+
case "exists":
|
|
2219
|
+
return { ...pred, where: scopeToNamespace(pred.where, namespace) };
|
|
2220
|
+
case "aggPredicate":
|
|
2221
|
+
return pred.where ? { ...pred, where: scopeToNamespace(pred.where, namespace) } : pred;
|
|
2222
|
+
default:
|
|
2223
|
+
return pred;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
1993
2226
|
var CMP_BY_OP = {
|
|
1994
2227
|
"=": "eq",
|
|
1995
2228
|
"!=": "neq",
|
|
@@ -2023,7 +2256,7 @@ function resolveLink(info, ref) {
|
|
|
2023
2256
|
function columnCandidates(info) {
|
|
2024
2257
|
return [...info.columns.map((c) => c.name), ...info.links.map((l) => l.name)];
|
|
2025
2258
|
}
|
|
2026
|
-
var PolicyCompiler = class {
|
|
2259
|
+
var PolicyCompiler = class _PolicyCompiler {
|
|
2027
2260
|
constructor(ctx) {
|
|
2028
2261
|
this.ctx = ctx;
|
|
2029
2262
|
}
|
|
@@ -2036,13 +2269,15 @@ var PolicyCompiler = class {
|
|
|
2036
2269
|
return { kind: node.kind, operands: node.operands.map((operand) => this.compile(operand, table, depth)) };
|
|
2037
2270
|
case "not":
|
|
2038
2271
|
assertNoNegatedExternalClaim(node.operand);
|
|
2039
|
-
return { kind: "not", operand: this.compile(node.operand, table, depth) };
|
|
2272
|
+
return { kind: "not", operand: new _PolicyCompiler({ ...this.ctx, negated: true }).compile(node.operand, table, depth) };
|
|
2040
2273
|
case "capability":
|
|
2041
2274
|
return { kind: "capability", key: node.key };
|
|
2042
2275
|
case "role":
|
|
2043
2276
|
return { kind: "role", key: node.key };
|
|
2044
2277
|
case "exists":
|
|
2045
2278
|
return this.compileExists(node.path, node.where, table, depth);
|
|
2279
|
+
case "visible":
|
|
2280
|
+
return this.compileVisible(node.path, table, depth);
|
|
2046
2281
|
case "compare":
|
|
2047
2282
|
return this.compileCompare(node, table, depth);
|
|
2048
2283
|
}
|
|
@@ -2056,6 +2291,36 @@ var PolicyCompiler = class {
|
|
|
2056
2291
|
}
|
|
2057
2292
|
return pred;
|
|
2058
2293
|
}
|
|
2294
|
+
/** `visible(path)` → `exists(path, <final target's read policy>)`. */
|
|
2295
|
+
compileVisible(path, table, depth) {
|
|
2296
|
+
const { links, finalTable } = this.resolveLinkPath(path, table, depth, { toOneOnly: false });
|
|
2297
|
+
const readPolicy = this.ctx.resolver.readPolicy;
|
|
2298
|
+
if (!readPolicy) {
|
|
2299
|
+
throw new PolicyCompileError(
|
|
2300
|
+
`visible(${path.join(".")}) needs the target's read policy, which this compile context does not provide`,
|
|
2301
|
+
"visible-unresolvable"
|
|
2302
|
+
);
|
|
2303
|
+
}
|
|
2304
|
+
if (this.ctx.visiting.includes(finalTable)) {
|
|
2305
|
+
throw new PolicyCompileError(
|
|
2306
|
+
`visible(${path.join(".")}) on '${table}' reaches '${finalTable}' whose read policy is already being expanded (${[...this.ctx.visiting, finalTable].join(" \u2192 ")})`,
|
|
2307
|
+
"visible-cycle"
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
const expression = readPolicy(finalTable);
|
|
2311
|
+
let pred = expression === void 0 ? { kind: "const", value: true } : compilePolicyAt({
|
|
2312
|
+
expression,
|
|
2313
|
+
table: finalTable,
|
|
2314
|
+
ctx: { ...this.ctx, visiting: [...this.ctx.visiting, finalTable] },
|
|
2315
|
+
depth: depth + links.length
|
|
2316
|
+
});
|
|
2317
|
+
const owner = this.ctx.resolver.policyNamespace?.(finalTable);
|
|
2318
|
+
if (owner) pred = scopeToNamespace(pred, owner);
|
|
2319
|
+
for (let i = links.length - 1; i >= 0; i -= 1) {
|
|
2320
|
+
pred = { kind: "exists", link: links[i].name, where: pred };
|
|
2321
|
+
}
|
|
2322
|
+
return pred;
|
|
2323
|
+
}
|
|
2059
2324
|
resolveLinkPath(path, table, depth, { toOneOnly }) {
|
|
2060
2325
|
const links = [];
|
|
2061
2326
|
let current = table;
|
|
@@ -2116,6 +2381,7 @@ var PolicyCompiler = class {
|
|
|
2116
2381
|
);
|
|
2117
2382
|
}
|
|
2118
2383
|
if (valueSide.kind === "claim" && isExternalPolicyClaimKey(valueSide.key)) {
|
|
2384
|
+
if (this.ctx.negated) throw negatedExternalClaim(valueSide.key);
|
|
2119
2385
|
assertExternalClaimComparison({ op, key: valueSide.key, column: info.columns.find((c) => c.name === column) });
|
|
2120
2386
|
}
|
|
2121
2387
|
let cmp = this.buildCmp(op, column, valueSide);
|
|
@@ -2153,22 +2419,30 @@ function assertExternalClaimComparison({ op, key, column }) {
|
|
|
2153
2419
|
);
|
|
2154
2420
|
}
|
|
2155
2421
|
}
|
|
2422
|
+
function negatedExternalClaim(key) {
|
|
2423
|
+
return new PolicyCompileError(
|
|
2424
|
+
`claim('${key}') cannot appear under not(): a negated borrower comparison admits other borrowers' rows`,
|
|
2425
|
+
"unsupported-comparison"
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2156
2428
|
function assertNoNegatedExternalClaim(operand) {
|
|
2157
2429
|
walkPolicy(operand, (node) => {
|
|
2158
2430
|
if (node.kind !== "compare") return;
|
|
2159
2431
|
const claim = [node.left, node.right].find((side) => side.kind === "claim" && isExternalPolicyClaimKey(side.key));
|
|
2160
|
-
if (claim?.kind === "claim")
|
|
2161
|
-
throw new PolicyCompileError(
|
|
2162
|
-
`claim('${claim.key}') cannot appear under not(): a negated borrower comparison admits other borrowers' rows`,
|
|
2163
|
-
"unsupported-comparison"
|
|
2164
|
-
);
|
|
2165
|
-
}
|
|
2432
|
+
if (claim?.kind === "claim") throw negatedExternalClaim(claim.key);
|
|
2166
2433
|
});
|
|
2167
2434
|
}
|
|
2168
2435
|
function compilePredicateV2({ expression, table, resolver, maxLinkDepth }) {
|
|
2436
|
+
return compilePredicateAt({
|
|
2437
|
+
expression,
|
|
2438
|
+
table,
|
|
2439
|
+
depth: 0,
|
|
2440
|
+
ctx: { resolver, maxLinkDepth: maxLinkDepth ?? POLICY_MAX_LINK_DEPTH, visiting: [table] }
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
function compilePredicateAt({ expression, table, ctx, depth }) {
|
|
2169
2444
|
const parsed = typeof expression === "string" ? parsePolicy(expression) : expression;
|
|
2170
|
-
|
|
2171
|
-
return compiler.compile(parsed.ast, table, 0);
|
|
2445
|
+
return new PolicyCompiler(ctx).compile(parsed.ast, table, depth);
|
|
2172
2446
|
}
|
|
2173
2447
|
function assertClaimsOnlyWhen(parsed) {
|
|
2174
2448
|
walkPolicy(parsed.ast, (node) => {
|
|
@@ -2185,25 +2459,34 @@ function assertClaimsOnlyWhen(parsed) {
|
|
|
2185
2459
|
"when-not-claims-only"
|
|
2186
2460
|
);
|
|
2187
2461
|
}
|
|
2188
|
-
if (node.kind === "exists") {
|
|
2462
|
+
if (node.kind === "exists" || node.kind === "visible") {
|
|
2189
2463
|
throw new PolicyCompileError(
|
|
2190
|
-
|
|
2464
|
+
`variant 'when' guards are claims-only \u2014 ${node.kind}() is not allowed`,
|
|
2191
2465
|
"when-not-claims-only"
|
|
2192
2466
|
);
|
|
2193
2467
|
}
|
|
2194
2468
|
});
|
|
2195
2469
|
}
|
|
2196
|
-
function
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2470
|
+
function initialVisiting({ table, access }) {
|
|
2471
|
+
return (access ?? "read") === "read" ? [table] : [];
|
|
2472
|
+
}
|
|
2473
|
+
function compilePolicy({ expression, table, resolver, maxLinkDepth, access }) {
|
|
2474
|
+
return compilePolicyAt({
|
|
2475
|
+
expression,
|
|
2476
|
+
table,
|
|
2477
|
+
depth: 0,
|
|
2478
|
+
ctx: { resolver, maxLinkDepth: maxLinkDepth ?? POLICY_MAX_LINK_DEPTH, visiting: initialVisiting({ table, access }) }
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
function compilePolicyAt({ expression, table, ctx, depth }) {
|
|
2482
|
+
if (typeof expression === "string") return compilePredicateAt({ expression, table, ctx, depth });
|
|
2483
|
+
const defaultPred = compilePredicateAt({ expression: expression.default, table, ctx, depth });
|
|
2201
2484
|
if (expression.variants.length === 0) return defaultPred;
|
|
2202
2485
|
const branches = expression.variants.map((variant) => {
|
|
2203
2486
|
const parsedWhen = parsePolicy(variant.when);
|
|
2204
2487
|
assertClaimsOnlyWhen(parsedWhen);
|
|
2205
|
-
const when =
|
|
2206
|
-
const grant = variant.allow === true ? { kind: "const", value: true } :
|
|
2488
|
+
const when = compilePredicateAt({ expression: parsedWhen, table, ctx, depth });
|
|
2489
|
+
const grant = variant.allow === true ? { kind: "const", value: true } : compilePredicateAt({ expression: variant.policy, table, ctx, depth });
|
|
2207
2490
|
return { kind: "and", operands: [when, grant] };
|
|
2208
2491
|
});
|
|
2209
2492
|
return { kind: "or", operands: [defaultPred, ...branches] };
|
|
@@ -2419,12 +2702,6 @@ var PolicyInjector = class {
|
|
|
2419
2702
|
if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
|
|
2420
2703
|
if (root.op === "insert" || root.op === "insertOne") {
|
|
2421
2704
|
const insertPolicy = this.insert(table);
|
|
2422
|
-
if (predContainsExists(insertPolicy)) {
|
|
2423
|
-
throw new DocumentCompileError(
|
|
2424
|
-
`table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its insert policy traverses a link (exists)`,
|
|
2425
|
-
"insert-check-unrepresentable"
|
|
2426
|
-
);
|
|
2427
|
-
}
|
|
2428
2705
|
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
2429
2706
|
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
2430
2707
|
);
|
|
@@ -2439,22 +2716,14 @@ var PolicyInjector = class {
|
|
|
2439
2716
|
const updatePolicy = this.update(table);
|
|
2440
2717
|
const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
|
|
2441
2718
|
if (root.op === "update" || root.op === "updateByPk") {
|
|
2442
|
-
|
|
2443
|
-
return { ...root, where, ...check, returning };
|
|
2719
|
+
return { ...root, where, insertCheck: updatePolicy, returning };
|
|
2444
2720
|
}
|
|
2445
2721
|
if (root.op === "cowOverride") {
|
|
2446
2722
|
const insertPolicy = this.insert(table);
|
|
2447
|
-
if (predContainsExists(insertPolicy)) {
|
|
2448
|
-
throw new DocumentCompileError(
|
|
2449
|
-
`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)`,
|
|
2450
|
-
"insert-check-unrepresentable"
|
|
2451
|
-
);
|
|
2452
|
-
}
|
|
2453
2723
|
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
2454
2724
|
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
2455
2725
|
);
|
|
2456
|
-
|
|
2457
|
-
return { ...root, pinnedColumns: pins, cowCopyCheck: insertPolicy, where, ...check, returning };
|
|
2726
|
+
return { ...root, pinnedColumns: pins, cowCopyCheck: insertPolicy, where, insertCheck: updatePolicy, returning };
|
|
2458
2727
|
}
|
|
2459
2728
|
return { ...root, where, returning };
|
|
2460
2729
|
}
|
|
@@ -2676,7 +2945,7 @@ function scopePolicy(pred, namespace) {
|
|
|
2676
2945
|
switch (pred.kind) {
|
|
2677
2946
|
case "capability":
|
|
2678
2947
|
case "role":
|
|
2679
|
-
return { ...pred, namespace };
|
|
2948
|
+
return pred.namespace ? pred : { ...pred, namespace };
|
|
2680
2949
|
case "and":
|
|
2681
2950
|
case "or":
|
|
2682
2951
|
return { ...pred, operands: pred.operands.map((p) => scopePolicy(p, namespace)) };
|
|
@@ -2766,6 +3035,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2766
3035
|
const platformMeta = usesPlatformUsers ? platformUserMeta() : void 0;
|
|
2767
3036
|
if (platformMeta) tables[PLATFORM_USER_TABLE] = platformMeta;
|
|
2768
3037
|
const syncedTableNames = new Set(canonical.tables.filter((t) => t.source !== void 0).map((t) => t.table));
|
|
3038
|
+
const unkeyedSyncedTableNames = new Set(relations.filter((table) => table.source !== void 0 && table.primaryKey?.length === 0).map((table) => table.table));
|
|
2769
3039
|
const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
|
|
2770
3040
|
const claimsGatedMemo = /* @__PURE__ */ new Map();
|
|
2771
3041
|
const relationIsClaimsGated = (name2) => {
|
|
@@ -2779,6 +3049,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2779
3049
|
};
|
|
2780
3050
|
for (const table of relations) {
|
|
2781
3051
|
const isView = viewNames.has(table.table);
|
|
3052
|
+
const readOnly = isView || unkeyedSyncedTableNames.has(table.table);
|
|
2782
3053
|
const owner = owners.get(table.table);
|
|
2783
3054
|
const entries = resolveColumnEntries(table, enums, diagnostics);
|
|
2784
3055
|
columnEntries.set(table.table, entries);
|
|
@@ -2826,13 +3097,13 @@ function buildCompileContext(rawCanonical) {
|
|
|
2826
3097
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
2827
3098
|
claimsGated: relationIsClaimsGated(table.table),
|
|
2828
3099
|
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
2829
|
-
primaryKey: isView ? primaryKey : primaryKey.length > 0 ? primaryKey : ["id"],
|
|
3100
|
+
primaryKey: isView || unkeyedSyncedTableNames.has(table.table) ? primaryKey : primaryKey.length > 0 ? primaryKey : ["id"],
|
|
2830
3101
|
links: [],
|
|
2831
3102
|
filterable: data.map((c) => c.name),
|
|
2832
3103
|
orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
2833
|
-
insertable: resolved ? resolvedWritable(data) :
|
|
2834
|
-
settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey.includes(name2)) :
|
|
2835
|
-
appendable:
|
|
3104
|
+
insertable: resolved ? resolvedWritable(data) : readOnly ? [] : active.map((c) => c.name),
|
|
3105
|
+
settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey.includes(name2)) : readOnly ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
3106
|
+
appendable: readOnly ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
2836
3107
|
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
2837
3108
|
...cells ? { cells } : {},
|
|
2838
3109
|
...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey[0] }) } : {},
|
|
@@ -2856,6 +3127,16 @@ function buildCompileContext(rawCanonical) {
|
|
|
2856
3127
|
});
|
|
2857
3128
|
continue;
|
|
2858
3129
|
}
|
|
3130
|
+
const identityTable = link.kind === "toOne" ? targetName : link.kind === "toMany" ? table.table : void 0;
|
|
3131
|
+
if (identityTable !== void 0 && unkeyedSyncedTableNames.has(identityTable)) {
|
|
3132
|
+
diagnostics.push({
|
|
3133
|
+
severity: "error",
|
|
3134
|
+
code: "unkeyed-link-identity",
|
|
3135
|
+
table: table.table,
|
|
3136
|
+
message: `link '${link.name}' cannot use unkeyed table '${identityTable}' as a row identity`
|
|
3137
|
+
});
|
|
3138
|
+
continue;
|
|
3139
|
+
}
|
|
2859
3140
|
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
|
|
2860
3141
|
const onEntries = link.kind === "toMany" ? targetEntries : entries;
|
|
2861
3142
|
const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
|
|
@@ -2885,7 +3166,14 @@ function buildCompileContext(rawCanonical) {
|
|
|
2885
3166
|
const updatePolicies = /* @__PURE__ */ new Map();
|
|
2886
3167
|
const pins = /* @__PURE__ */ new Map();
|
|
2887
3168
|
const splitWritePolicyTables = /* @__PURE__ */ new Set();
|
|
2888
|
-
const
|
|
3169
|
+
const readPolicyExpressions = new Map(relations.map((relation) => [relation.table, relation.policies.read]));
|
|
3170
|
+
const resolver = {
|
|
3171
|
+
tables,
|
|
3172
|
+
readPolicy: (table) => readPolicyExpressions.get(table),
|
|
3173
|
+
// In a composed namespace, an imported table's read policy (inlined by
|
|
3174
|
+
// visible()) is checked against the namespace that owns it.
|
|
3175
|
+
...normalized.imports?.length ? { policyNamespace: (table) => owners.get(table)?.namespace } : {}
|
|
3176
|
+
};
|
|
2889
3177
|
for (const table of relations) {
|
|
2890
3178
|
const isView = viewNames.has(table.table);
|
|
2891
3179
|
const compilePolicyFor = (access, store) => {
|
|
@@ -2894,7 +3182,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2894
3182
|
return;
|
|
2895
3183
|
}
|
|
2896
3184
|
try {
|
|
2897
|
-
const pred = compilePolicy({ expression, table: table.table, resolver });
|
|
3185
|
+
const pred = compilePolicy({ expression, table: table.table, resolver, access });
|
|
2898
3186
|
store.set(table.table, normalized.imports?.length ? scopePolicy(pred, owners.get(table.table).namespace) : pred);
|
|
2899
3187
|
} catch (error) {
|
|
2900
3188
|
if (error instanceof PolicyCompileError || error instanceof PolicyParseError) {
|
|
@@ -2914,7 +3202,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2914
3202
|
compilePolicyFor("write", writePolicies);
|
|
2915
3203
|
compilePolicyFor("insert", insertPolicies);
|
|
2916
3204
|
compilePolicyFor("update", updatePolicies);
|
|
2917
|
-
if (isView && table.resolved === void 0) {
|
|
3205
|
+
if (isView && table.resolved === void 0 || unkeyedSyncedTableNames.has(table.table)) {
|
|
2918
3206
|
writePolicies.set(table.table, CONST_FALSE);
|
|
2919
3207
|
insertPolicies.set(table.table, CONST_FALSE);
|
|
2920
3208
|
updatePolicies.set(table.table, CONST_FALSE);
|
|
@@ -2989,7 +3277,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2989
3277
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
2990
3278
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
2991
3279
|
for (const table of relations) {
|
|
2992
|
-
if (table.internal === true) continue;
|
|
3280
|
+
if (table.internal === true || unkeyedSyncedTableNames.has(table.table)) continue;
|
|
2993
3281
|
const roots = queryRootNames(table.table);
|
|
2994
3282
|
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
2995
3283
|
if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
|
|
@@ -3369,8 +3657,10 @@ var OperationLowerer = class {
|
|
|
3369
3657
|
constructor(ctx, restricted) {
|
|
3370
3658
|
this.ctx = ctx;
|
|
3371
3659
|
this.variables = new Map(restricted.variables.map((v) => [v.name, v]));
|
|
3660
|
+
this.maxRows = restricted.readBudget ?? MAX_LIMIT;
|
|
3372
3661
|
}
|
|
3373
3662
|
variables;
|
|
3663
|
+
maxRows;
|
|
3374
3664
|
meta(table) {
|
|
3375
3665
|
return this.ctx.tables[table];
|
|
3376
3666
|
}
|
|
@@ -3574,13 +3864,13 @@ var OperationLowerer = class {
|
|
|
3574
3864
|
});
|
|
3575
3865
|
}
|
|
3576
3866
|
lowerLimit(node) {
|
|
3577
|
-
const base2 = { default: DEFAULT_LIMIT, max:
|
|
3867
|
+
const base2 = { default: DEFAULT_LIMIT, max: this.maxRows };
|
|
3578
3868
|
if (!node) return base2;
|
|
3579
3869
|
if (node.kind === graphql.Kind.VARIABLE) return { ...base2, variable: node.name.value };
|
|
3580
3870
|
if (node.kind === graphql.Kind.INT) {
|
|
3581
3871
|
const value = Number(node.value);
|
|
3582
3872
|
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
3583
|
-
return { ...base2, value: Math.min(value,
|
|
3873
|
+
return { ...base2, value: Math.min(value, this.maxRows) };
|
|
3584
3874
|
}
|
|
3585
3875
|
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
3586
3876
|
}
|
|
@@ -4379,7 +4669,13 @@ var OperationLowerer = class {
|
|
|
4379
4669
|
};
|
|
4380
4670
|
function lowerOperation({ restricted, ctx }) {
|
|
4381
4671
|
const lowerer = new OperationLowerer(ctx, restricted);
|
|
4382
|
-
|
|
4672
|
+
const roots = lowerer.lowerRoots(restricted.definition, restricted.kind);
|
|
4673
|
+
if (restricted.readBudget === void 0) return roots;
|
|
4674
|
+
const root = roots[0];
|
|
4675
|
+
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) {
|
|
4676
|
+
fail2("@readBudget requires one flat select root without related selections, aggregates, cells, or prefetch", "invalid-read-budget");
|
|
4677
|
+
}
|
|
4678
|
+
return roots;
|
|
4383
4679
|
}
|
|
4384
4680
|
function isCellColumn(meta, name2) {
|
|
4385
4681
|
if (!meta.cells) return false;
|
|
@@ -4438,6 +4734,19 @@ function parseDocuments({
|
|
|
4438
4734
|
}
|
|
4439
4735
|
return { documents: parsed, diagnostics };
|
|
4440
4736
|
}
|
|
4737
|
+
function parseReadBudget(operation) {
|
|
4738
|
+
const directives = operation.directives?.filter((directive) => directive.name.value === "readBudget") ?? [];
|
|
4739
|
+
if (directives.length === 0) return void 0;
|
|
4740
|
+
const value = directives[0]?.arguments?.find((argument) => argument.name.value === "maxRows")?.value;
|
|
4741
|
+
if (operation.operation !== "query" || directives.length !== 1 || value?.kind !== graphql.Kind.INT) {
|
|
4742
|
+
throw new DocumentCompileError("@readBudget requires a literal maxRows on a query", "invalid-read-budget");
|
|
4743
|
+
}
|
|
4744
|
+
const maxRows = Number(value.value);
|
|
4745
|
+
if (!Number.isSafeInteger(maxRows) || maxRows < 1 || maxRows > MAX_PERSISTED_READ_ROWS) {
|
|
4746
|
+
throw new DocumentCompileError(`@readBudget(maxRows:) must be between 1 and ${MAX_PERSISTED_READ_ROWS}`, "invalid-read-budget");
|
|
4747
|
+
}
|
|
4748
|
+
return maxRows;
|
|
4749
|
+
}
|
|
4441
4750
|
function unwrapType(node, variable) {
|
|
4442
4751
|
let nullable = true;
|
|
4443
4752
|
let list2 = false;
|
|
@@ -4598,6 +4907,7 @@ function restrictOperation({
|
|
|
4598
4907
|
name: name2,
|
|
4599
4908
|
kind: operation.operation === "mutation" ? "mutation" : "query",
|
|
4600
4909
|
requires: parseRequires(operation),
|
|
4910
|
+
readBudget: parseReadBudget(operation),
|
|
4601
4911
|
variables,
|
|
4602
4912
|
definition: operation
|
|
4603
4913
|
};
|
|
@@ -5638,6 +5948,118 @@ function bindNamedParameters({ sql, names }) {
|
|
|
5638
5948
|
}
|
|
5639
5949
|
return result;
|
|
5640
5950
|
}
|
|
5951
|
+
var parser2;
|
|
5952
|
+
var functions = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
5953
|
+
var scalarTypes = /* @__PURE__ */ new Set([
|
|
5954
|
+
"bool",
|
|
5955
|
+
"boolean",
|
|
5956
|
+
"int2",
|
|
5957
|
+
"int4",
|
|
5958
|
+
"int8",
|
|
5959
|
+
"integer",
|
|
5960
|
+
"bigint",
|
|
5961
|
+
"smallint",
|
|
5962
|
+
"numeric",
|
|
5963
|
+
"decimal",
|
|
5964
|
+
"float4",
|
|
5965
|
+
"float8",
|
|
5966
|
+
"real",
|
|
5967
|
+
"text",
|
|
5968
|
+
"varchar",
|
|
5969
|
+
"bpchar",
|
|
5970
|
+
"uuid",
|
|
5971
|
+
"date",
|
|
5972
|
+
"timestamp",
|
|
5973
|
+
"timestamptz",
|
|
5974
|
+
"time",
|
|
5975
|
+
"timetz",
|
|
5976
|
+
"interval",
|
|
5977
|
+
"json",
|
|
5978
|
+
"jsonb",
|
|
5979
|
+
"bytea"
|
|
5980
|
+
]);
|
|
5981
|
+
var sqlValues = /* @__PURE__ */ new Set([
|
|
5982
|
+
"SVFOP_CURRENT_DATE",
|
|
5983
|
+
"SVFOP_CURRENT_TIME",
|
|
5984
|
+
"SVFOP_CURRENT_TIME_N",
|
|
5985
|
+
"SVFOP_CURRENT_TIMESTAMP",
|
|
5986
|
+
"SVFOP_CURRENT_TIMESTAMP_N",
|
|
5987
|
+
"SVFOP_LOCALTIME",
|
|
5988
|
+
"SVFOP_LOCALTIME_N",
|
|
5989
|
+
"SVFOP_LOCALTIMESTAMP",
|
|
5990
|
+
"SVFOP_LOCALTIMESTAMP_N",
|
|
5991
|
+
"SVFOP_CURRENT_ROLE",
|
|
5992
|
+
"SVFOP_CURRENT_USER",
|
|
5993
|
+
"SVFOP_USER",
|
|
5994
|
+
"SVFOP_SESSION_USER",
|
|
5995
|
+
"SVFOP_CURRENT_CATALOG",
|
|
5996
|
+
"SVFOP_CURRENT_SCHEMA"
|
|
5997
|
+
]);
|
|
5998
|
+
async function validateManagedViewQueries({ canonical }) {
|
|
5999
|
+
for (const definition of [canonical, ...canonical.imports ?? []]) {
|
|
6000
|
+
for (const view of definition.views ?? []) {
|
|
6001
|
+
const fail4 = (message) => {
|
|
6002
|
+
throw new OrmDefinitionError(`managed view '${definition.namespace}/${view.view}': ${message}`);
|
|
6003
|
+
};
|
|
6004
|
+
parser2 ??= new pgParser.PgParser({ version: 17 });
|
|
6005
|
+
const parsed = await parser2.parse(view.query);
|
|
6006
|
+
if (parsed.error || !parsed.tree) fail4("invalid SELECT query");
|
|
6007
|
+
const statements = list(obj(parsed.tree).stmts);
|
|
6008
|
+
if (statements.length !== 1 || !obj(obj(statements[0]).stmt).SelectStmt) fail4("one SELECT query is required");
|
|
6009
|
+
validateTree({ tree: parsed.tree, schema: ormSchemaName(definition.namespace), enums: definition.enums.map((entry) => entry.name), fail: fail4 });
|
|
6010
|
+
}
|
|
6011
|
+
}
|
|
6012
|
+
}
|
|
6013
|
+
function pgCatalogName(parts) {
|
|
6014
|
+
if (parts.length === 1) return parts[0];
|
|
6015
|
+
if (parts.length === 2 && parts[0] === "pg_catalog") return parts[1];
|
|
6016
|
+
return void 0;
|
|
6017
|
+
}
|
|
6018
|
+
function reviewedFunctionName({ parts, format }) {
|
|
6019
|
+
return format === "COERCE_SQL_SYNTAX" && parts.length === 2 && parts[0] === "pg_catalog" && parts[1] === "position" ? "strpos" : pgCatalogName(parts);
|
|
6020
|
+
}
|
|
6021
|
+
function validateTree({ tree, ...scope }) {
|
|
6022
|
+
const pending = [tree];
|
|
6023
|
+
while (pending.length) {
|
|
6024
|
+
const value = pending.pop();
|
|
6025
|
+
if (Array.isArray(value)) {
|
|
6026
|
+
pending.push(...value);
|
|
6027
|
+
continue;
|
|
6028
|
+
}
|
|
6029
|
+
if (!value || typeof value !== "object") continue;
|
|
6030
|
+
const node = obj(value);
|
|
6031
|
+
for (const [kind, child] of Object.entries(node)) {
|
|
6032
|
+
validateNode({ kind, child, scope });
|
|
6033
|
+
pending.push(child);
|
|
6034
|
+
}
|
|
6035
|
+
}
|
|
6036
|
+
}
|
|
6037
|
+
function validateNode({ kind, child, scope }) {
|
|
6038
|
+
const { fail: fail4 } = scope;
|
|
6039
|
+
if (kind.endsWith("Stmt") && kind !== "SelectStmt") fail4("only SELECT statements are permitted");
|
|
6040
|
+
if (kind === "FuncCall") {
|
|
6041
|
+
const call = obj(child);
|
|
6042
|
+
const parts = strings(call.funcname);
|
|
6043
|
+
const name2 = reviewedFunctionName({ parts, format: call.funcformat });
|
|
6044
|
+
if (!name2 || !functions.has(name2)) fail4(`only explicitly allowlisted pg_catalog functions are permitted (found '${parts.join(".")}')`);
|
|
6045
|
+
}
|
|
6046
|
+
if (kind === "typeName" || kind === "TypeName") validateType({ node: obj(child), ...scope });
|
|
6047
|
+
if (kind === "A_Expr" && pgCatalogName(strings(obj(child).name)) === void 0) fail4("only pg_catalog operators are permitted");
|
|
6048
|
+
if (kind === "SortBy" && list(obj(child).useOp).length && pgCatalogName(strings(obj(child).useOp)) === void 0) fail4("only pg_catalog sort operators are permitted");
|
|
6049
|
+
if (kind === "SQLValueFunction" && !sqlValues.has(String(obj(child).op))) fail4("unsupported SQL value expression");
|
|
6050
|
+
if (kind === "ParamRef") fail4("view queries cannot contain parameters");
|
|
6051
|
+
if (kind === "A_Indirection" && list(obj(child).indirection).some((entry) => !obj(entry).A_Indices)) {
|
|
6052
|
+
fail4("composite field notation is not permitted in managed views");
|
|
6053
|
+
}
|
|
6054
|
+
}
|
|
6055
|
+
function validateType({ node, schema, enums, fail: fail4 }) {
|
|
6056
|
+
const parts = strings(node.names);
|
|
6057
|
+
const builtin = pgCatalogName(parts);
|
|
6058
|
+
const managedEnum = parts.length === 1 && enums.includes(parts[0]) || parts.length === 2 && parts[0] === schema && enums.includes(parts[1]);
|
|
6059
|
+
if (node.setof || node.pct_type || !managedEnum && (!builtin || !scalarTypes.has(builtin))) {
|
|
6060
|
+
fail4(`casts may only use supported pg_catalog scalar types or declared enums (found '${parts.join(".")}')`);
|
|
6061
|
+
}
|
|
6062
|
+
}
|
|
5641
6063
|
|
|
5642
6064
|
// src/sql/operations.ts
|
|
5643
6065
|
function isSqlOperationDocument(path, contents) {
|
|
@@ -5653,7 +6075,13 @@ function isSqlOperationDocument(path, contents) {
|
|
|
5653
6075
|
async function compileOperationDocuments(input) {
|
|
5654
6076
|
const sqlDocuments = input.documents.filter((d) => isSqlOperationDocument(d.path, d.contents));
|
|
5655
6077
|
const result = compileNamespace({ ...input, documents: input.documents.filter((d) => !isSqlOperationDocument(d.path, d.contents)) });
|
|
5656
|
-
if (!result.ok
|
|
6078
|
+
if (!result.ok) return result;
|
|
6079
|
+
const viewDiagnostics = await managedViewDiagnostics(input.canonical);
|
|
6080
|
+
if (viewDiagnostics.length) return { ok: false, diagnostics: [...result.diagnostics, ...viewDiagnostics] };
|
|
6081
|
+
if (!sqlDocuments.length) return result;
|
|
6082
|
+
return compileSqlDocuments({ input, result, sqlDocuments });
|
|
6083
|
+
}
|
|
6084
|
+
async function compileSqlDocuments({ input, result, sqlDocuments }) {
|
|
5657
6085
|
for (const document of sqlDocuments) {
|
|
5658
6086
|
try {
|
|
5659
6087
|
const definition = defineOperation(JSON.parse(document.contents));
|
|
@@ -5747,6 +6175,18 @@ async function compileOperationDocuments(input) {
|
|
|
5747
6175
|
result.manifest.opsHash = result.artifact.opsHash;
|
|
5748
6176
|
return { ...result, artifact: normalizeIr(result.artifact) };
|
|
5749
6177
|
}
|
|
6178
|
+
async function managedViewDiagnostics(canonical) {
|
|
6179
|
+
try {
|
|
6180
|
+
await validateManagedViewQueries({ canonical });
|
|
6181
|
+
return [];
|
|
6182
|
+
} catch (error) {
|
|
6183
|
+
return [{
|
|
6184
|
+
severity: "error",
|
|
6185
|
+
code: "invalid-view-query",
|
|
6186
|
+
message: error instanceof Error ? error.message : "invalid managed view query"
|
|
6187
|
+
}];
|
|
6188
|
+
}
|
|
6189
|
+
}
|
|
5750
6190
|
var parameterName = zod.z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(63).refine((value) => !["__proto__", "prototype", "constructor"].includes(value), "reserved parameter name");
|
|
5751
6191
|
var parameter = zod.z.discriminatedUnion("type", [
|
|
5752
6192
|
zod.z.object({ type: zod.z.literal("text"), value: zod.z.string().nullable() }).strict(),
|
|
@@ -5886,6 +6326,7 @@ exports.typeNameForTable = typeNameForTable;
|
|
|
5886
6326
|
exports.validateAppendInput = validateAppendInput;
|
|
5887
6327
|
exports.validateBoolExpInput = validateBoolExpInput;
|
|
5888
6328
|
exports.validateInsertInput = validateInsertInput;
|
|
6329
|
+
exports.validateManagedViewQueries = validateManagedViewQueries;
|
|
5889
6330
|
exports.validateOnConflictInput = validateOnConflictInput;
|
|
5890
6331
|
exports.validateOrderByInput = validateOrderByInput;
|
|
5891
6332
|
exports.validateSelectColumns = validateSelectColumns;
|