naro-cli 0.11.0 → 0.12.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 +202 -12
- package/dist/{chunk-L7JOGWP3.js → chunk-GFCG5C7D.js} +3678 -263
- package/dist/deploy.js +1 -1
- package/dist/index.js +302 -39
- package/package.json +5 -5
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { existsSync as existsSync7 } from "fs";
|
|
8
8
|
import { cp as cp2, mkdtemp as mkdtemp2, readFile as readFile13, rm as rm3 } from "fs/promises";
|
|
9
9
|
import { tmpdir as tmpdir2 } from "os";
|
|
10
|
-
import { basename as basename3, dirname as dirname4, join as join24, resolve as
|
|
10
|
+
import { basename as basename3, dirname as dirname4, join as join24, resolve as resolve10 } from "path";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
12
12
|
|
|
13
13
|
// ../../packages/build/src/artifact-upload.ts
|
|
@@ -6730,13 +6730,13 @@ var error17 = () => {
|
|
|
6730
6730
|
return `${sizing?.longLabel ?? "\u05D0\u05E8\u05D5\u05DA"} \u05DE\u05D3\u05D9: ${subject} \u05E6\u05E8\u05D9\u05DB\u05D4 \u05DC\u05D4\u05DB\u05D9\u05DC ${issue2.maximum.toString()} ${sizing?.unit ?? ""} ${issue2.inclusive ? "\u05D0\u05D5 \u05E4\u05D7\u05D5\u05EA" : "\u05DC\u05DB\u05DC \u05D4\u05D9\u05D5\u05EA\u05E8"}`.trim();
|
|
6731
6731
|
}
|
|
6732
6732
|
if (issue2.origin === "number") {
|
|
6733
|
-
const
|
|
6734
|
-
return `\u05D2\u05D3\u05D5\u05DC \u05DE\u05D3\u05D9: ${subject} \u05E6\u05E8\u05D9\u05DA \u05DC\u05D4\u05D9\u05D5\u05EA ${
|
|
6733
|
+
const comparison2 = issue2.inclusive ? `\u05E7\u05D8\u05DF \u05D0\u05D5 \u05E9\u05D5\u05D5\u05D4 \u05DC-${issue2.maximum}` : `\u05E7\u05D8\u05DF \u05DE-${issue2.maximum}`;
|
|
6734
|
+
return `\u05D2\u05D3\u05D5\u05DC \u05DE\u05D3\u05D9: ${subject} \u05E6\u05E8\u05D9\u05DA \u05DC\u05D4\u05D9\u05D5\u05EA ${comparison2}`;
|
|
6735
6735
|
}
|
|
6736
6736
|
if (issue2.origin === "array" || issue2.origin === "set") {
|
|
6737
6737
|
const verb = issue2.origin === "set" ? "\u05E6\u05E8\u05D9\u05DB\u05D4" : "\u05E6\u05E8\u05D9\u05DA";
|
|
6738
|
-
const
|
|
6739
|
-
return `\u05D2\u05D3\u05D5\u05DC \u05DE\u05D3\u05D9: ${subject} ${verb} \u05DC\u05D4\u05DB\u05D9\u05DC ${
|
|
6738
|
+
const comparison2 = issue2.inclusive ? `${issue2.maximum} ${sizing?.unit ?? ""} \u05D0\u05D5 \u05E4\u05D7\u05D5\u05EA` : `\u05E4\u05D7\u05D5\u05EA \u05DE-${issue2.maximum} ${sizing?.unit ?? ""}`;
|
|
6739
|
+
return `\u05D2\u05D3\u05D5\u05DC \u05DE\u05D3\u05D9: ${subject} ${verb} \u05DC\u05D4\u05DB\u05D9\u05DC ${comparison2}`.trim();
|
|
6740
6740
|
}
|
|
6741
6741
|
const adj = issue2.inclusive ? "<=" : "<";
|
|
6742
6742
|
const be = verbFor(issue2.origin ?? "value");
|
|
@@ -6752,8 +6752,8 @@ var error17 = () => {
|
|
|
6752
6752
|
return `${sizing?.shortLabel ?? "\u05E7\u05E6\u05E8"} \u05DE\u05D3\u05D9: ${subject} \u05E6\u05E8\u05D9\u05DB\u05D4 \u05DC\u05D4\u05DB\u05D9\u05DC ${issue2.minimum.toString()} ${sizing?.unit ?? ""} ${issue2.inclusive ? "\u05D0\u05D5 \u05D9\u05D5\u05EA\u05E8" : "\u05DC\u05E4\u05D7\u05D5\u05EA"}`.trim();
|
|
6753
6753
|
}
|
|
6754
6754
|
if (issue2.origin === "number") {
|
|
6755
|
-
const
|
|
6756
|
-
return `\u05E7\u05D8\u05DF \u05DE\u05D3\u05D9: ${subject} \u05E6\u05E8\u05D9\u05DA \u05DC\u05D4\u05D9\u05D5\u05EA ${
|
|
6755
|
+
const comparison2 = issue2.inclusive ? `\u05D2\u05D3\u05D5\u05DC \u05D0\u05D5 \u05E9\u05D5\u05D5\u05D4 \u05DC-${issue2.minimum}` : `\u05D2\u05D3\u05D5\u05DC \u05DE-${issue2.minimum}`;
|
|
6756
|
+
return `\u05E7\u05D8\u05DF \u05DE\u05D3\u05D9: ${subject} \u05E6\u05E8\u05D9\u05DA \u05DC\u05D4\u05D9\u05D5\u05EA ${comparison2}`;
|
|
6757
6757
|
}
|
|
6758
6758
|
if (issue2.origin === "array" || issue2.origin === "set") {
|
|
6759
6759
|
const verb = issue2.origin === "set" ? "\u05E6\u05E8\u05D9\u05DB\u05D4" : "\u05E6\u05E8\u05D9\u05DA";
|
|
@@ -6761,8 +6761,8 @@ var error17 = () => {
|
|
|
6761
6761
|
const singularPhrase = issue2.origin === "set" ? "\u05DC\u05E4\u05D7\u05D5\u05EA \u05E4\u05E8\u05D9\u05D8 \u05D0\u05D7\u05D3" : "\u05DC\u05E4\u05D7\u05D5\u05EA \u05E4\u05E8\u05D9\u05D8 \u05D0\u05D7\u05D3";
|
|
6762
6762
|
return `\u05E7\u05D8\u05DF \u05DE\u05D3\u05D9: ${subject} ${verb} \u05DC\u05D4\u05DB\u05D9\u05DC ${singularPhrase}`;
|
|
6763
6763
|
}
|
|
6764
|
-
const
|
|
6765
|
-
return `\u05E7\u05D8\u05DF \u05DE\u05D3\u05D9: ${subject} ${verb} \u05DC\u05D4\u05DB\u05D9\u05DC ${
|
|
6764
|
+
const comparison2 = issue2.inclusive ? `${issue2.minimum} ${sizing?.unit ?? ""} \u05D0\u05D5 \u05D9\u05D5\u05EA\u05E8` : `\u05D9\u05D5\u05EA\u05E8 \u05DE-${issue2.minimum} ${sizing?.unit ?? ""}`;
|
|
6765
|
+
return `\u05E7\u05D8\u05DF \u05DE\u05D3\u05D9: ${subject} ${verb} \u05DC\u05D4\u05DB\u05D9\u05DC ${comparison2}`.trim();
|
|
6766
6766
|
}
|
|
6767
6767
|
const adj = issue2.inclusive ? ">=" : ">";
|
|
6768
6768
|
const be = verbFor(issue2.origin ?? "value");
|
|
@@ -7967,8 +7967,8 @@ function ko_default() {
|
|
|
7967
7967
|
}
|
|
7968
7968
|
|
|
7969
7969
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/lt.js
|
|
7970
|
-
var capitalizeFirstCharacter = (
|
|
7971
|
-
return
|
|
7970
|
+
var capitalizeFirstCharacter = (text2) => {
|
|
7971
|
+
return text2.charAt(0).toUpperCase() + text2.slice(1);
|
|
7972
7972
|
};
|
|
7973
7973
|
function getUnitTypeFromNumber(number4) {
|
|
7974
7974
|
const abs = Math.abs(number4);
|
|
@@ -14939,7 +14939,238 @@ var connectRepositorySchema = external_exports.object({
|
|
|
14939
14939
|
});
|
|
14940
14940
|
|
|
14941
14941
|
// ../../packages/core/src/sql-classifier.ts
|
|
14942
|
+
var WORD_START = /[A-Za-z_\u0080-\uffff]/;
|
|
14943
|
+
var WORD_PART = /[A-Za-z0-9_$\u0080-\uffff]/;
|
|
14944
|
+
var DIGIT = /[0-9]/;
|
|
14945
|
+
var HEX = /[0-9A-Fa-f]/;
|
|
14946
|
+
var NUMBER_PART = /[0-9A-Za-z_.]/;
|
|
14947
|
+
var SPACE = /[ \t\n\r\f]/;
|
|
14948
|
+
var ISSPACE = /[ \t\n\r\f\v]/;
|
|
14942
14949
|
var NUL = String.fromCharCode(0);
|
|
14950
|
+
var BOM = 65279;
|
|
14951
|
+
var PRAGMA_PREFIX = "PRAGMA_";
|
|
14952
|
+
function asciiUpper(text2) {
|
|
14953
|
+
return text2.replace(/[a-z]+/g, (run) => run.toUpperCase());
|
|
14954
|
+
}
|
|
14955
|
+
function pragmaCandidate(sql, start, end, doubled) {
|
|
14956
|
+
const prefix = sql.slice(start + 1, start + 1 + PRAGMA_PREFIX.length);
|
|
14957
|
+
if (asciiUpper(prefix) !== PRAGMA_PREFIX) return "";
|
|
14958
|
+
const inner = sql.slice(start + 1, end - 1);
|
|
14959
|
+
return asciiUpper(
|
|
14960
|
+
doubled === null ? inner : inner.split(doubled + doubled).join(doubled)
|
|
14961
|
+
);
|
|
14962
|
+
}
|
|
14963
|
+
function readDoubled(sql, start, delimiter) {
|
|
14964
|
+
let i = start;
|
|
14965
|
+
for (; ; ) {
|
|
14966
|
+
const close = sql.indexOf(delimiter, i);
|
|
14967
|
+
if (close < 0) return -1;
|
|
14968
|
+
if (sql.charAt(close + 1) === delimiter) {
|
|
14969
|
+
i = close + 2;
|
|
14970
|
+
continue;
|
|
14971
|
+
}
|
|
14972
|
+
return close + 1;
|
|
14973
|
+
}
|
|
14974
|
+
}
|
|
14975
|
+
function withoutExplain(tokens) {
|
|
14976
|
+
if (!isWord(tokens[0], "EXPLAIN")) return tokens;
|
|
14977
|
+
return tokens.slice(
|
|
14978
|
+
isWord(tokens[1], "QUERY") && isWord(tokens[2], "PLAN") ? 3 : 1
|
|
14979
|
+
);
|
|
14980
|
+
}
|
|
14981
|
+
function isCreateTrigger(tokens) {
|
|
14982
|
+
const rest = withoutExplain(tokens);
|
|
14983
|
+
if (!isWord(rest[0], "CREATE")) return false;
|
|
14984
|
+
const second = rest[1];
|
|
14985
|
+
if (isWord(second, "TRIGGER")) return true;
|
|
14986
|
+
return (isWord(second, "TEMP") || isWord(second, "TEMPORARY")) && isWord(rest[2], "TRIGGER");
|
|
14987
|
+
}
|
|
14988
|
+
function lex(sql, names = false) {
|
|
14989
|
+
const statements = [];
|
|
14990
|
+
const spans = [];
|
|
14991
|
+
let current = [];
|
|
14992
|
+
let tokenStart = 0;
|
|
14993
|
+
let currentStart = 0;
|
|
14994
|
+
const push = (token) => {
|
|
14995
|
+
if (current.length === 0) currentStart = tokenStart;
|
|
14996
|
+
current.push(token);
|
|
14997
|
+
};
|
|
14998
|
+
let triggerBody = false;
|
|
14999
|
+
let afterBodySemicolon = -1;
|
|
15000
|
+
const fail2 = (reason) => ({
|
|
15001
|
+
ok: false,
|
|
15002
|
+
reason,
|
|
15003
|
+
statements: statements.length + 1
|
|
15004
|
+
});
|
|
15005
|
+
if (sql.includes(NUL)) return fail2("NUL byte in SQL");
|
|
15006
|
+
let i = 0;
|
|
15007
|
+
while (i < sql.length) {
|
|
15008
|
+
tokenStart = i;
|
|
15009
|
+
const c = sql.charAt(i);
|
|
15010
|
+
const next = sql.charAt(i + 1);
|
|
15011
|
+
if (c === "\v") return fail2("illegal control character");
|
|
15012
|
+
if (SPACE.test(c)) {
|
|
15013
|
+
i += 1;
|
|
15014
|
+
while (i < sql.length && ISSPACE.test(sql.charAt(i))) i += 1;
|
|
15015
|
+
continue;
|
|
15016
|
+
}
|
|
15017
|
+
if (sql.charCodeAt(i) === BOM) {
|
|
15018
|
+
i += 1;
|
|
15019
|
+
continue;
|
|
15020
|
+
}
|
|
15021
|
+
if (c === "-" && next === "-") {
|
|
15022
|
+
const end = sql.indexOf("\n", i + 2);
|
|
15023
|
+
i = end < 0 ? sql.length : end;
|
|
15024
|
+
continue;
|
|
15025
|
+
}
|
|
15026
|
+
if (c === "/" && next === "*") {
|
|
15027
|
+
const end = sql.indexOf("*/", i + 2);
|
|
15028
|
+
if (end < 0) return fail2("unterminated block comment");
|
|
15029
|
+
i = end + 2;
|
|
15030
|
+
continue;
|
|
15031
|
+
}
|
|
15032
|
+
if ((c === "x" || c === "X") && next === "'") {
|
|
15033
|
+
let end = i + 2;
|
|
15034
|
+
while (end < sql.length && HEX.test(sql.charAt(end))) end += 1;
|
|
15035
|
+
if (sql.charAt(end) !== "'") return fail2("malformed blob literal");
|
|
15036
|
+
push({ type: "blob" });
|
|
15037
|
+
i = end + 1;
|
|
15038
|
+
continue;
|
|
15039
|
+
}
|
|
15040
|
+
if (c === "'") {
|
|
15041
|
+
const end = readDoubled(sql, i + 1, "'");
|
|
15042
|
+
if (end < 0) return fail2("unterminated string literal");
|
|
15043
|
+
push({
|
|
15044
|
+
type: "string",
|
|
15045
|
+
value: pragmaCandidate(sql, i, end, "'")
|
|
15046
|
+
});
|
|
15047
|
+
i = end;
|
|
15048
|
+
continue;
|
|
15049
|
+
}
|
|
15050
|
+
if (c === '"' || c === "`") {
|
|
15051
|
+
const end = readDoubled(sql, i + 1, c);
|
|
15052
|
+
if (end < 0) return fail2("unterminated quoted identifier");
|
|
15053
|
+
push({
|
|
15054
|
+
type: "quoted",
|
|
15055
|
+
value: pragmaCandidate(sql, i, end, c),
|
|
15056
|
+
...names ? {
|
|
15057
|
+
name: sql.slice(i + 1, end - 1).split(c + c).join(c)
|
|
15058
|
+
} : {}
|
|
15059
|
+
});
|
|
15060
|
+
i = end;
|
|
15061
|
+
continue;
|
|
15062
|
+
}
|
|
15063
|
+
if (c === "[") {
|
|
15064
|
+
const end = sql.indexOf("]", i + 1);
|
|
15065
|
+
if (end < 0) return fail2("unterminated bracketed identifier");
|
|
15066
|
+
push({
|
|
15067
|
+
type: "quoted",
|
|
15068
|
+
// No doubling inside brackets: the first `]` closes.
|
|
15069
|
+
value: pragmaCandidate(sql, i, end + 1, null),
|
|
15070
|
+
...names ? { name: sql.slice(i + 1, end) } : {}
|
|
15071
|
+
});
|
|
15072
|
+
i = end + 1;
|
|
15073
|
+
continue;
|
|
15074
|
+
}
|
|
15075
|
+
if (c === ";") {
|
|
15076
|
+
if (triggerBody) {
|
|
15077
|
+
afterBodySemicolon = current.length;
|
|
15078
|
+
} else {
|
|
15079
|
+
if (current.length > 0) {
|
|
15080
|
+
statements.push(current);
|
|
15081
|
+
spans.push({ start: currentStart, end: i });
|
|
15082
|
+
}
|
|
15083
|
+
current = [];
|
|
15084
|
+
}
|
|
15085
|
+
i += 1;
|
|
15086
|
+
continue;
|
|
15087
|
+
}
|
|
15088
|
+
if (c === "(" || c === ")" || c === ",") {
|
|
15089
|
+
push({ type: "punct", value: c });
|
|
15090
|
+
i += 1;
|
|
15091
|
+
continue;
|
|
15092
|
+
}
|
|
15093
|
+
if (WORD_START.test(c)) {
|
|
15094
|
+
let end = i + 1;
|
|
15095
|
+
while (end < sql.length && WORD_PART.test(sql.charAt(end))) end += 1;
|
|
15096
|
+
const value = asciiUpper(sql.slice(i, end));
|
|
15097
|
+
push({ type: "word", value });
|
|
15098
|
+
if (triggerBody) {
|
|
15099
|
+
if (value === "END" && current.length - 1 === afterBodySemicolon)
|
|
15100
|
+
triggerBody = false;
|
|
15101
|
+
} else if (value === "BEGIN" && isCreateTrigger(current)) {
|
|
15102
|
+
triggerBody = true;
|
|
15103
|
+
afterBodySemicolon = -1;
|
|
15104
|
+
}
|
|
15105
|
+
i = end;
|
|
15106
|
+
continue;
|
|
15107
|
+
}
|
|
15108
|
+
if (DIGIT.test(c) || c === "." && DIGIT.test(next)) {
|
|
15109
|
+
let end = i + 1;
|
|
15110
|
+
while (end < sql.length && NUMBER_PART.test(sql.charAt(end))) end += 1;
|
|
15111
|
+
push({ type: "number" });
|
|
15112
|
+
i = end;
|
|
15113
|
+
continue;
|
|
15114
|
+
}
|
|
15115
|
+
if (c === "?") {
|
|
15116
|
+
let end = i + 1;
|
|
15117
|
+
while (end < sql.length && DIGIT.test(sql.charAt(end))) end += 1;
|
|
15118
|
+
push({ type: "other" });
|
|
15119
|
+
i = end;
|
|
15120
|
+
continue;
|
|
15121
|
+
}
|
|
15122
|
+
if (c === ":" || c === "@" || c === "$" || c === "#") {
|
|
15123
|
+
let end = i + 1;
|
|
15124
|
+
let nameChars = 0;
|
|
15125
|
+
for (; ; ) {
|
|
15126
|
+
const ch = sql.charAt(end);
|
|
15127
|
+
if (ch !== "" && WORD_PART.test(ch)) {
|
|
15128
|
+
nameChars += 1;
|
|
15129
|
+
end += 1;
|
|
15130
|
+
continue;
|
|
15131
|
+
}
|
|
15132
|
+
if (ch === ":" && sql.charAt(end + 1) === ":") {
|
|
15133
|
+
end += 2;
|
|
15134
|
+
continue;
|
|
15135
|
+
}
|
|
15136
|
+
if (ch === "(" && nameChars > 0) {
|
|
15137
|
+
let close = end + 1;
|
|
15138
|
+
while (close < sql.length && sql.charAt(close) !== ")" && !ISSPACE.test(sql.charAt(close)))
|
|
15139
|
+
close += 1;
|
|
15140
|
+
if (sql.charAt(close) !== ")")
|
|
15141
|
+
return fail2("malformed bound-parameter suffix");
|
|
15142
|
+
end = close + 1;
|
|
15143
|
+
}
|
|
15144
|
+
break;
|
|
15145
|
+
}
|
|
15146
|
+
push({ type: "other" });
|
|
15147
|
+
i = end;
|
|
15148
|
+
continue;
|
|
15149
|
+
}
|
|
15150
|
+
push(names ? { type: "other", char: c } : { type: "other" });
|
|
15151
|
+
i += 1;
|
|
15152
|
+
}
|
|
15153
|
+
if (current.length > 0) {
|
|
15154
|
+
statements.push(current);
|
|
15155
|
+
spans.push({ start: currentStart, end: sql.length });
|
|
15156
|
+
}
|
|
15157
|
+
return { ok: true, statements, spans };
|
|
15158
|
+
}
|
|
15159
|
+
function isWord(token, value) {
|
|
15160
|
+
return token?.type === "word" && (value === void 0 || token.value === value);
|
|
15161
|
+
}
|
|
15162
|
+
function splitStatements(sql) {
|
|
15163
|
+
const lexed = lex(sql);
|
|
15164
|
+
if (!lexed.ok) return { ok: false, reason: lexed.reason };
|
|
15165
|
+
return {
|
|
15166
|
+
ok: true,
|
|
15167
|
+
statements: lexed.spans.map(({ start, end }) => ({
|
|
15168
|
+
text: sql.slice(start, end),
|
|
15169
|
+
start,
|
|
15170
|
+
end
|
|
15171
|
+
}))
|
|
15172
|
+
};
|
|
15173
|
+
}
|
|
14943
15174
|
|
|
14944
15175
|
// ../../packages/build/src/next-version.ts
|
|
14945
15176
|
var NEXT_RELEASE_DATES = {
|
|
@@ -16853,7 +17084,7 @@ function createSmokeTester(options = {}) {
|
|
|
16853
17084
|
}
|
|
16854
17085
|
}
|
|
16855
17086
|
);
|
|
16856
|
-
const
|
|
17087
|
+
const probes2 = [];
|
|
16857
17088
|
const withOutput = (reason) => recent.length > 0 ? `${reason} Last output: ${recent.join(" | ")}` : reason;
|
|
16858
17089
|
try {
|
|
16859
17090
|
const deadline = Date.now() + bootTimeoutMs;
|
|
@@ -16870,7 +17101,7 @@ function createSmokeTester(options = {}) {
|
|
|
16870
17101
|
reason: withOutput(
|
|
16871
17102
|
`The server exited with code ${String(exit.exited)} before answering a request.`
|
|
16872
17103
|
),
|
|
16873
|
-
probes,
|
|
17104
|
+
probes: probes2,
|
|
16874
17105
|
// The port was free when allocated and taken by the time the
|
|
16875
17106
|
// server bound it — a race, not a broken build.
|
|
16876
17107
|
portTaken: recent.some(
|
|
@@ -16896,7 +17127,7 @@ function createSmokeTester(options = {}) {
|
|
|
16896
17127
|
reason: withOutput(
|
|
16897
17128
|
`The server did not answer within ${String(bootTimeoutMs)}ms (${lastError}).`
|
|
16898
17129
|
),
|
|
16899
|
-
probes
|
|
17130
|
+
probes: probes2
|
|
16900
17131
|
};
|
|
16901
17132
|
}
|
|
16902
17133
|
for (const probe of request.probes) {
|
|
@@ -16906,24 +17137,24 @@ function createSmokeTester(options = {}) {
|
|
|
16906
17137
|
probeTimeoutMs
|
|
16907
17138
|
);
|
|
16908
17139
|
const result = "status" in answer ? { ...probe, status: answer.status, ok: probeOk(answer.status) } : { ...probe, status: null, ok: false, detail: answer.error };
|
|
16909
|
-
|
|
17140
|
+
probes2.push(result);
|
|
16910
17141
|
await observer.onLog({
|
|
16911
17142
|
level: result.ok ? "INFO" : "ERROR",
|
|
16912
17143
|
stage,
|
|
16913
17144
|
message: `Smoke ${probe.label}: GET ${probe.path} -> ${result.status === null ? result.detail ?? "no response" : String(result.status)}`
|
|
16914
17145
|
});
|
|
16915
17146
|
}
|
|
16916
|
-
const failed2 =
|
|
17147
|
+
const failed2 = probes2.filter((probe) => !probe.ok);
|
|
16917
17148
|
if (failed2.length > 0) {
|
|
16918
17149
|
return {
|
|
16919
17150
|
ok: false,
|
|
16920
|
-
reason: `${String(failed2.length)} of ${String(
|
|
17151
|
+
reason: `${String(failed2.length)} of ${String(probes2.length)} smoke probes failed: ${failed2.map(
|
|
16921
17152
|
(probe) => `${probe.path} (${probe.status === null ? probe.detail ?? "no response" : String(probe.status)})`
|
|
16922
17153
|
).join(", ")}.`,
|
|
16923
|
-
probes
|
|
17154
|
+
probes: probes2
|
|
16924
17155
|
};
|
|
16925
17156
|
}
|
|
16926
|
-
return { ok: true, probes };
|
|
17157
|
+
return { ok: true, probes: probes2 };
|
|
16927
17158
|
} finally {
|
|
16928
17159
|
await server.stop();
|
|
16929
17160
|
}
|
|
@@ -16942,12 +17173,12 @@ function createSmokeTester(options = {}) {
|
|
|
16942
17173
|
};
|
|
16943
17174
|
}
|
|
16944
17175
|
function defaultSmokeProbes(matcherRoutes) {
|
|
16945
|
-
const
|
|
17176
|
+
const probes2 = [{ path: "/", label: "root" }];
|
|
16946
17177
|
for (const route of matcherRoutes) {
|
|
16947
17178
|
if (route === "/") continue;
|
|
16948
|
-
|
|
17179
|
+
probes2.push({ path: route, label: "middleware matcher" });
|
|
16949
17180
|
}
|
|
16950
|
-
return
|
|
17181
|
+
return probes2;
|
|
16951
17182
|
}
|
|
16952
17183
|
|
|
16953
17184
|
// ../../packages/build/src/strategy/strategies/opennext-aws.ts
|
|
@@ -18514,8 +18745,8 @@ function sendPage(res, status, html) {
|
|
|
18514
18745
|
async function startCallbackServer() {
|
|
18515
18746
|
let resolveCode;
|
|
18516
18747
|
let rejectCode;
|
|
18517
|
-
const codePromise = new Promise((
|
|
18518
|
-
resolveCode =
|
|
18748
|
+
const codePromise = new Promise((resolve11, reject) => {
|
|
18749
|
+
resolveCode = resolve11;
|
|
18519
18750
|
rejectCode = reject;
|
|
18520
18751
|
});
|
|
18521
18752
|
const server = createServer2((req, res) => {
|
|
@@ -18544,7 +18775,7 @@ async function startCallbackServer() {
|
|
|
18544
18775
|
sendPage(res, 200, successPage());
|
|
18545
18776
|
resolveCode(code);
|
|
18546
18777
|
});
|
|
18547
|
-
await new Promise((
|
|
18778
|
+
await new Promise((resolve11) => server.listen(0, "127.0.0.1", resolve11));
|
|
18548
18779
|
const port = server.address().port;
|
|
18549
18780
|
return {
|
|
18550
18781
|
port,
|
|
@@ -19028,7 +19259,7 @@ var DeploymentsApi = class {
|
|
|
19028
19259
|
async wait(deploymentId, options = {}) {
|
|
19029
19260
|
const intervalMs = options.intervalMs ?? 2500;
|
|
19030
19261
|
const timeoutMs = options.timeoutMs ?? 15 * 6e4;
|
|
19031
|
-
const sleep2 = options.sleep ?? ((ms) => new Promise((
|
|
19262
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
|
|
19032
19263
|
const startedAt = Date.now();
|
|
19033
19264
|
let lastStatus = null;
|
|
19034
19265
|
let lastSequence = -1;
|
|
@@ -19293,14 +19524,12 @@ var DbApi = class {
|
|
|
19293
19524
|
* retrying rather than re-running the file blind.
|
|
19294
19525
|
*/
|
|
19295
19526
|
async applyMigration(projectId, input) {
|
|
19296
|
-
|
|
19297
|
-
|
|
19298
|
-
{
|
|
19299
|
-
|
|
19300
|
-
|
|
19301
|
-
|
|
19302
|
-
}
|
|
19303
|
-
);
|
|
19527
|
+
const body = await this.client.request(this.path(projectId, "/migrations"), {
|
|
19528
|
+
method: "POST",
|
|
19529
|
+
body: { name: input.name, sql: input.sql },
|
|
19530
|
+
context: "Apply migration"
|
|
19531
|
+
});
|
|
19532
|
+
return { ...body, warnings: body.warnings ?? [] };
|
|
19304
19533
|
}
|
|
19305
19534
|
/**
|
|
19306
19535
|
* The current Time Travel bookmark, or the one `timestamp` (RFC 3339 with a
|
|
@@ -19368,7 +19597,7 @@ var DbApi = class {
|
|
|
19368
19597
|
*/
|
|
19369
19598
|
async export(projectId, options = {}, wait = {}) {
|
|
19370
19599
|
const timeoutMs = wait.timeoutMs ?? EXPORT_TIMEOUT_MS;
|
|
19371
|
-
const sleep2 = wait.sleep ?? ((ms) => new Promise((
|
|
19600
|
+
const sleep2 = wait.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
|
|
19372
19601
|
const given = wait.pollIntervalMs;
|
|
19373
19602
|
const firstPollMs = given !== void 0 && Number.isFinite(given) && given > 0 ? given : EXPORT_FIRST_POLL_MS;
|
|
19374
19603
|
const maxPollMs = Math.max(EXPORT_MAX_POLL_MS, firstPollMs);
|
|
@@ -19838,29 +20067,29 @@ var RESTORE_OUTCOME_UNKNOWN_CODES = /* @__PURE__ */ new Set([
|
|
|
19838
20067
|
"TIMEOUT",
|
|
19839
20068
|
"NETWORK_ERROR"
|
|
19840
20069
|
]);
|
|
19841
|
-
function cellText(
|
|
19842
|
-
const index =
|
|
20070
|
+
function cellText(statement2, row, column2) {
|
|
20071
|
+
const index = statement2.columns.indexOf(column2);
|
|
19843
20072
|
const value = index < 0 ? void 0 : row[index];
|
|
19844
20073
|
return value === null || value === void 0 ? "" : String(value);
|
|
19845
20074
|
}
|
|
19846
20075
|
var MAX_CELL_CHARS = 60;
|
|
19847
20076
|
function formatCell(value) {
|
|
19848
20077
|
if (value === null || value === void 0) return "NULL";
|
|
19849
|
-
const
|
|
19850
|
-
const oneLine =
|
|
20078
|
+
const text2 = typeof value === "string" ? value : typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
20079
|
+
const oneLine = text2.replace(/\s+/g, " ");
|
|
19851
20080
|
return oneLine.length > MAX_CELL_CHARS ? `${oneLine.slice(0, MAX_CELL_CHARS - 1)}\u2026` : oneLine;
|
|
19852
20081
|
}
|
|
19853
|
-
function renderRows(
|
|
19854
|
-
if (
|
|
19855
|
-
const cells =
|
|
19856
|
-
(row) =>
|
|
20082
|
+
function renderRows(statement2) {
|
|
20083
|
+
if (statement2.columns.length === 0) return [];
|
|
20084
|
+
const cells = statement2.rows.map(
|
|
20085
|
+
(row) => statement2.columns.map((_, index) => formatCell(row[index]))
|
|
19857
20086
|
);
|
|
19858
|
-
const widths =
|
|
19859
|
-
(
|
|
20087
|
+
const widths = statement2.columns.map(
|
|
20088
|
+
(column2, index) => Math.max(column2.length, ...cells.map((row) => row[index]?.length ?? 0))
|
|
19860
20089
|
);
|
|
19861
20090
|
const line = (values) => values.map((value, index) => value.padEnd(widths[index] ?? 0)).join(" ").trimEnd();
|
|
19862
20091
|
return [
|
|
19863
|
-
line(
|
|
20092
|
+
line(statement2.columns),
|
|
19864
20093
|
widths.map((width) => "-".repeat(width)).join(" "),
|
|
19865
20094
|
...cells.map(line)
|
|
19866
20095
|
];
|
|
@@ -19868,18 +20097,18 @@ function renderRows(statement) {
|
|
|
19868
20097
|
function formatMs(ms) {
|
|
19869
20098
|
return ms >= 100 ? String(Math.round(ms)) : ms.toFixed(1);
|
|
19870
20099
|
}
|
|
19871
|
-
function summarizeStatement(
|
|
19872
|
-
const rows =
|
|
19873
|
-
const { meta: meta3 } =
|
|
20100
|
+
function summarizeStatement(statement2) {
|
|
20101
|
+
const rows = statement2.rows.length;
|
|
20102
|
+
const { meta: meta3 } = statement2;
|
|
19874
20103
|
return `\u21B3 ${String(rows)} row${rows === 1 ? "" : "s"} \xB7 ${String(meta3.rowsRead)} read \xB7 ${String(meta3.rowsWritten)} written \xB7 ${formatMs(meta3.durationMs)} ms`;
|
|
19875
20104
|
}
|
|
19876
20105
|
|
|
19877
20106
|
// ../../packages/sdk/src/dotenv.ts
|
|
19878
20107
|
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
19879
20108
|
import { join as join19 } from "path";
|
|
19880
|
-
function parseDotenv(
|
|
20109
|
+
function parseDotenv(text2) {
|
|
19881
20110
|
const out = {};
|
|
19882
|
-
for (const line of
|
|
20111
|
+
for (const line of text2.split(/\r?\n/)) {
|
|
19883
20112
|
const trimmed = line.trim();
|
|
19884
20113
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
19885
20114
|
const eq = trimmed.indexOf("=");
|
|
@@ -19982,14 +20211,14 @@ function scanEnvReferences(dir) {
|
|
|
19982
20211
|
const files = [];
|
|
19983
20212
|
collect(dir, files);
|
|
19984
20213
|
for (const file2 of files) {
|
|
19985
|
-
let
|
|
20214
|
+
let text2;
|
|
19986
20215
|
try {
|
|
19987
20216
|
if (statSync(file2).size > MAX_FILE_BYTES2) continue;
|
|
19988
|
-
|
|
20217
|
+
text2 = readFileSync3(file2, "utf8");
|
|
19989
20218
|
} catch {
|
|
19990
20219
|
continue;
|
|
19991
20220
|
}
|
|
19992
|
-
for (const match of
|
|
20221
|
+
for (const match of text2.matchAll(REFERENCE)) {
|
|
19993
20222
|
const key = match[1] ?? match[2];
|
|
19994
20223
|
if (!key || ignored(key)) continue;
|
|
19995
20224
|
found.set(key, "code");
|
|
@@ -20177,32 +20406,156 @@ var SYSTEM_TABLE_PREFIXES = [
|
|
|
20177
20406
|
"sqlite_"
|
|
20178
20407
|
];
|
|
20179
20408
|
var SYSTEM_TABLE_NAMES = ["d1_migrations"];
|
|
20180
|
-
function asciiLower(
|
|
20181
|
-
return
|
|
20409
|
+
function asciiLower(text2) {
|
|
20410
|
+
return text2.replace(/[A-Z]+/g, (run) => run.toLowerCase());
|
|
20182
20411
|
}
|
|
20183
20412
|
function isSystemTable(name) {
|
|
20184
20413
|
const lower = asciiLower(name);
|
|
20185
20414
|
return SYSTEM_TABLE_PREFIXES.some((prefix) => lower.startsWith(prefix)) || SYSTEM_TABLE_NAMES.includes(lower);
|
|
20186
20415
|
}
|
|
20187
|
-
function likeLiteral(
|
|
20188
|
-
return
|
|
20416
|
+
function likeLiteral(text2) {
|
|
20417
|
+
return text2.replace(/[\\%_]/g, (char) => `\\${char}`).replace(/'/g, "''");
|
|
20189
20418
|
}
|
|
20190
20419
|
var COLUMN_REFERENCE = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$/;
|
|
20191
|
-
function systemTablePredicate(
|
|
20192
|
-
if (!COLUMN_REFERENCE.test(
|
|
20193
|
-
throw new Error(`Not a column reference: ${
|
|
20420
|
+
function systemTablePredicate(column2 = "name") {
|
|
20421
|
+
if (!COLUMN_REFERENCE.test(column2))
|
|
20422
|
+
throw new Error(`Not a column reference: ${column2}`);
|
|
20194
20423
|
return [
|
|
20195
20424
|
...SYSTEM_TABLE_PREFIXES.map(
|
|
20196
|
-
(prefix) => `${
|
|
20425
|
+
(prefix) => `${column2} NOT LIKE '${likeLiteral(prefix)}%' ESCAPE '\\'`
|
|
20197
20426
|
),
|
|
20198
20427
|
...SYSTEM_TABLE_NAMES.map(
|
|
20199
|
-
(name) => `${
|
|
20428
|
+
(name) => `${column2} NOT LIKE '${likeLiteral(name)}' ESCAPE '\\'`
|
|
20200
20429
|
),
|
|
20201
|
-
`${
|
|
20430
|
+
`${column2} NOT IN (SELECT name FROM pragma_table_list WHERE schema = 'main' AND type = 'shadow')`
|
|
20202
20431
|
].join(" AND ");
|
|
20203
20432
|
}
|
|
20204
20433
|
var SYSTEM_TABLE_SQL_PREDICATE = systemTablePredicate("name");
|
|
20205
20434
|
|
|
20435
|
+
// ../../packages/data-core/src/errors.ts
|
|
20436
|
+
var DATABASE_FULL_MESSAGE = "The database has reached D1's 10 GB limit, which cannot be raised. Delete data to free space; writes fail until it is under the limit.";
|
|
20437
|
+
var CATALOG = {
|
|
20438
|
+
INVALID_API_KEY: {
|
|
20439
|
+
status: 401,
|
|
20440
|
+
message: () => "The API key is missing or not valid for this project."
|
|
20441
|
+
},
|
|
20442
|
+
INVALID_SESSION: {
|
|
20443
|
+
status: 401,
|
|
20444
|
+
message: () => "The session is expired or not valid. Sign in again."
|
|
20445
|
+
},
|
|
20446
|
+
INVALID_QUERY: {
|
|
20447
|
+
status: 400,
|
|
20448
|
+
message: () => "The query is not a valid Naro query."
|
|
20449
|
+
},
|
|
20450
|
+
TABLE_NOT_FOUND: {
|
|
20451
|
+
status: 404,
|
|
20452
|
+
message: ({ table }) => `Table ${table} was not found.`
|
|
20453
|
+
},
|
|
20454
|
+
COLUMN_NOT_FOUND: {
|
|
20455
|
+
status: 400,
|
|
20456
|
+
message: ({ column: column2, table }) => `Column ${column2} was not found on ${table}.`
|
|
20457
|
+
},
|
|
20458
|
+
PERMISSION_DENIED: {
|
|
20459
|
+
status: 403,
|
|
20460
|
+
// P2-7 §6: Postgres's words for a WITH CHECK that fails —
|
|
20461
|
+
// `new row violates row-level security policy ["p"] for table t`, the
|
|
20462
|
+
// restrictive policy's name in double quotes as Postgres writes it.
|
|
20463
|
+
message: ({ action, table, check: check2, policy }) => check2 ? `New row violates row-level security policy ${policy === void 0 ? "" : `"${policy}" `}for table ${table}.` : `This request is not allowed by the ${action} policy of ${table}.`
|
|
20464
|
+
},
|
|
20465
|
+
POLICY_VIOLATION: {
|
|
20466
|
+
status: 403,
|
|
20467
|
+
message: ({ column: column2 }) => `${column2} must be the signed-in user's id.`
|
|
20468
|
+
},
|
|
20469
|
+
FILTER_REQUIRED: {
|
|
20470
|
+
status: 400,
|
|
20471
|
+
message: () => "update and delete need at least one filter."
|
|
20472
|
+
},
|
|
20473
|
+
TOO_MANY_PARAMETERS: {
|
|
20474
|
+
status: 400,
|
|
20475
|
+
message: () => "The query needs more than 100 values in one statement."
|
|
20476
|
+
},
|
|
20477
|
+
NOT_SINGLE_ROW: {
|
|
20478
|
+
status: 406,
|
|
20479
|
+
message: ({ count: count2 }) => `Expected exactly one row, got ${String(count2)}.`
|
|
20480
|
+
},
|
|
20481
|
+
CONFLICT: {
|
|
20482
|
+
status: 409,
|
|
20483
|
+
message: ({ column: column2 }) => `A row with the same ${column2} already exists.`
|
|
20484
|
+
},
|
|
20485
|
+
CONSTRAINT_VIOLATION: {
|
|
20486
|
+
status: 400,
|
|
20487
|
+
message: ({ kind, column: column2 }) => `The row breaks a ${kind} constraint on ${column2}.`
|
|
20488
|
+
},
|
|
20489
|
+
PAYLOAD_TOO_LARGE: {
|
|
20490
|
+
status: 413,
|
|
20491
|
+
message: () => "The request body is larger than 1 MiB."
|
|
20492
|
+
},
|
|
20493
|
+
INTERNAL_ERROR: {
|
|
20494
|
+
status: 500,
|
|
20495
|
+
message: () => "Something went wrong. Try again."
|
|
20496
|
+
},
|
|
20497
|
+
// Not in spec §14: an `owner` rule whose owner column is unset or no longer
|
|
20498
|
+
// on the table. The control plane refuses to write one, but `_naro_policies`
|
|
20499
|
+
// is an ordinary table an operator can edit by hand, and a column can be
|
|
20500
|
+
// dropped after the policy was set. It is the project's configuration that
|
|
20501
|
+
// is broken, not the request, hence 500 — and never an allow.
|
|
20502
|
+
//
|
|
20503
|
+
// P2-7 §6: a named policy that cannot be applied (it cannot be read, or
|
|
20504
|
+
// names a column, table or problem the schema does not allow), policies
|
|
20505
|
+
// that read each other (Postgres 42P17's words), or too many values.
|
|
20506
|
+
POLICY_INVALID: {
|
|
20507
|
+
status: 500,
|
|
20508
|
+
message: ({
|
|
20509
|
+
action,
|
|
20510
|
+
table,
|
|
20511
|
+
condition,
|
|
20512
|
+
policy,
|
|
20513
|
+
recursion,
|
|
20514
|
+
values,
|
|
20515
|
+
bytes
|
|
20516
|
+
}) => {
|
|
20517
|
+
if (recursion)
|
|
20518
|
+
return `Infinite recursion detected in policy for relation ${table}.`;
|
|
20519
|
+
if (values)
|
|
20520
|
+
return `The policies of ${table} bind more than 50 values in one statement.`;
|
|
20521
|
+
if (bytes)
|
|
20522
|
+
return `The policies of ${table} write more than 50000 bytes of SQL into one statement.`;
|
|
20523
|
+
if (policy !== void 0)
|
|
20524
|
+
return `Policy ${policy} on ${table} cannot be applied.`;
|
|
20525
|
+
return condition ? `The ${action} policy of ${table} has a condition that cannot be applied.` : `The ${action} policy of ${table} is owner but has no valid owner column.`;
|
|
20526
|
+
}
|
|
20527
|
+
},
|
|
20528
|
+
// PR C (2026-09-23-d1-write-quota-design.md §5.2, §8): the project's D1
|
|
20529
|
+
// wrote more rows today (UTC) than its daily limit. Refuses Data API writes
|
|
20530
|
+
// and sign-up; the worker adds `x-retry-after`. No numbers — this answer
|
|
20531
|
+
// goes to anyone on the internet holding the anon key.
|
|
20532
|
+
WRITE_QUOTA_EXCEEDED: {
|
|
20533
|
+
status: 429,
|
|
20534
|
+
message: () => "This project has reached its daily database write limit. Reads still work; writes resume at 00:00 UTC."
|
|
20535
|
+
},
|
|
20536
|
+
// PR D: D1 refused a write because the database is at its size limit,
|
|
20537
|
+
// which Cloudflare does not raise. 507 Insufficient Storage: no retry
|
|
20538
|
+
// helps until rows are deleted. Reads still work.
|
|
20539
|
+
DATABASE_FULL: {
|
|
20540
|
+
status: 507,
|
|
20541
|
+
message: () => DATABASE_FULL_MESSAGE
|
|
20542
|
+
}
|
|
20543
|
+
};
|
|
20544
|
+
function dataError(code, ...args) {
|
|
20545
|
+
const [vars, details] = args;
|
|
20546
|
+
const entry = CATALOG[code];
|
|
20547
|
+
const error51 = {
|
|
20548
|
+
code,
|
|
20549
|
+
message: entry.message(vars),
|
|
20550
|
+
status: entry.status
|
|
20551
|
+
};
|
|
20552
|
+
if (details !== void 0) error51.details = details;
|
|
20553
|
+
return error51;
|
|
20554
|
+
}
|
|
20555
|
+
function invalidQuery(path, reason) {
|
|
20556
|
+
return dataError("INVALID_QUERY", void 0, { path, reason });
|
|
20557
|
+
}
|
|
20558
|
+
|
|
20206
20559
|
// ../../packages/data-core/src/d1.ts
|
|
20207
20560
|
function rowsOf(result) {
|
|
20208
20561
|
const rows = result?.results ?? [];
|
|
@@ -20256,11 +20609,11 @@ function isDigit(char) {
|
|
|
20256
20609
|
return char >= "0" && char <= "9";
|
|
20257
20610
|
}
|
|
20258
20611
|
var NUMBER = /^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/;
|
|
20259
|
-
function tokenize(
|
|
20612
|
+
function tokenize(text2) {
|
|
20260
20613
|
const tokens = [];
|
|
20261
20614
|
let index = 0;
|
|
20262
|
-
const at = (offset = 0) =>
|
|
20263
|
-
while (index <
|
|
20615
|
+
const at = (offset = 0) => text2.charAt(index + offset);
|
|
20616
|
+
while (index < text2.length) {
|
|
20264
20617
|
const char = at();
|
|
20265
20618
|
const position = index + 1;
|
|
20266
20619
|
if (char === "\0")
|
|
@@ -20270,12 +20623,12 @@ function tokenize(text) {
|
|
|
20270
20623
|
continue;
|
|
20271
20624
|
}
|
|
20272
20625
|
if (char === "-" && at(1) === "-") {
|
|
20273
|
-
while (index <
|
|
20626
|
+
while (index < text2.length && at() !== "\n" && at() !== "\r") index++;
|
|
20274
20627
|
continue;
|
|
20275
20628
|
}
|
|
20276
20629
|
if (char === "/" && at(1) === "*") {
|
|
20277
20630
|
let depth = 0;
|
|
20278
|
-
while (index <
|
|
20631
|
+
while (index < text2.length) {
|
|
20279
20632
|
if (at() === "/" && at(1) === "*") {
|
|
20280
20633
|
depth++;
|
|
20281
20634
|
index += 2;
|
|
@@ -20293,7 +20646,7 @@ function tokenize(text) {
|
|
|
20293
20646
|
let value = "";
|
|
20294
20647
|
index++;
|
|
20295
20648
|
for (; ; ) {
|
|
20296
|
-
if (index >=
|
|
20649
|
+
if (index >= text2.length)
|
|
20297
20650
|
throw new PolicySyntaxError("unterminated quoted string", position);
|
|
20298
20651
|
if (at() === "'") {
|
|
20299
20652
|
if (at(1) === "'") {
|
|
@@ -20314,7 +20667,7 @@ function tokenize(text) {
|
|
|
20314
20667
|
let value = "";
|
|
20315
20668
|
index++;
|
|
20316
20669
|
for (; ; ) {
|
|
20317
|
-
if (index >=
|
|
20670
|
+
if (index >= text2.length)
|
|
20318
20671
|
throw new PolicySyntaxError(
|
|
20319
20672
|
"unterminated quoted identifier",
|
|
20320
20673
|
position
|
|
@@ -20342,10 +20695,10 @@ function tokenize(text) {
|
|
|
20342
20695
|
position
|
|
20343
20696
|
);
|
|
20344
20697
|
if (isDigit(char) || char === "." && isDigit(at(1))) {
|
|
20345
|
-
const match = NUMBER.exec(
|
|
20698
|
+
const match = NUMBER.exec(text2.slice(index));
|
|
20346
20699
|
const value = match?.[0] ?? char;
|
|
20347
20700
|
index += value.length;
|
|
20348
|
-
if (index <
|
|
20701
|
+
if (index < text2.length && isIdentChar(at()))
|
|
20349
20702
|
throw new PolicySyntaxError(
|
|
20350
20703
|
`trailing junk after numeric literal ${value}`,
|
|
20351
20704
|
position
|
|
@@ -20355,9 +20708,9 @@ function tokenize(text) {
|
|
|
20355
20708
|
}
|
|
20356
20709
|
if (isIdentStart(char)) {
|
|
20357
20710
|
let end = index + 1;
|
|
20358
|
-
while (end <
|
|
20359
|
-
const value =
|
|
20360
|
-
if (
|
|
20711
|
+
while (end < text2.length && isIdentChar(text2.charAt(end))) end++;
|
|
20712
|
+
const value = text2.slice(index, end);
|
|
20713
|
+
if (text2.charAt(end) === "'")
|
|
20361
20714
|
throw new PolicySyntaxError(
|
|
20362
20715
|
`${value}'\u2026' strings are not supported: use a plain '\u2026' string`,
|
|
20363
20716
|
position
|
|
@@ -20367,7 +20720,7 @@ function tokenize(text) {
|
|
|
20367
20720
|
continue;
|
|
20368
20721
|
}
|
|
20369
20722
|
const operator = OPERATORS.find(
|
|
20370
|
-
(candidate) =>
|
|
20723
|
+
(candidate) => text2.startsWith(candidate, index)
|
|
20371
20724
|
);
|
|
20372
20725
|
if (!operator)
|
|
20373
20726
|
throw new PolicySyntaxError(
|
|
@@ -20381,7 +20734,7 @@ function tokenize(text) {
|
|
|
20381
20734
|
kind: "eof",
|
|
20382
20735
|
value: "",
|
|
20383
20736
|
quoted: false,
|
|
20384
|
-
position:
|
|
20737
|
+
position: text2.length + 1
|
|
20385
20738
|
});
|
|
20386
20739
|
return tokens;
|
|
20387
20740
|
}
|
|
@@ -20450,7 +20803,155 @@ var TokenStream = class {
|
|
|
20450
20803
|
}
|
|
20451
20804
|
};
|
|
20452
20805
|
|
|
20806
|
+
// ../../packages/data-core/src/collation.ts
|
|
20807
|
+
var COLLATIONS = ["BINARY", "NOCASE", "RTRIM"];
|
|
20808
|
+
function isSpace2(char) {
|
|
20809
|
+
return char === " " || char === " " || char === "\n" || char === "\f" || char === "\r";
|
|
20810
|
+
}
|
|
20811
|
+
function isWordChar(char) {
|
|
20812
|
+
return /[A-Za-z0-9_$]/.test(char) || char.charCodeAt(0) >= 128;
|
|
20813
|
+
}
|
|
20814
|
+
function asciiUpper2(text2) {
|
|
20815
|
+
return text2.replace(/[a-z]/g, (letter) => letter.toUpperCase());
|
|
20816
|
+
}
|
|
20817
|
+
function quoted(sql, start, close, doubles) {
|
|
20818
|
+
let value = "";
|
|
20819
|
+
for (let index = start + 1; index < sql.length; index++) {
|
|
20820
|
+
const char = sql.charAt(index);
|
|
20821
|
+
if (char !== close) {
|
|
20822
|
+
value += char;
|
|
20823
|
+
continue;
|
|
20824
|
+
}
|
|
20825
|
+
if (doubles && sql.charAt(index + 1) === close) {
|
|
20826
|
+
value += close;
|
|
20827
|
+
index++;
|
|
20828
|
+
continue;
|
|
20829
|
+
}
|
|
20830
|
+
return { value, end: index + 1 };
|
|
20831
|
+
}
|
|
20832
|
+
return void 0;
|
|
20833
|
+
}
|
|
20834
|
+
function tokenize2(sql) {
|
|
20835
|
+
const tokens = [];
|
|
20836
|
+
let index = 0;
|
|
20837
|
+
while (index < sql.length) {
|
|
20838
|
+
const char = sql.charAt(index);
|
|
20839
|
+
if (isSpace2(char)) {
|
|
20840
|
+
index++;
|
|
20841
|
+
} else if (char === "-" && sql.charAt(index + 1) === "-") {
|
|
20842
|
+
const end = sql.indexOf("\n", index);
|
|
20843
|
+
index = end < 0 ? sql.length : end + 1;
|
|
20844
|
+
} else if (char === "/" && sql.charAt(index + 1) === "*") {
|
|
20845
|
+
const end = sql.indexOf("*/", index + 2);
|
|
20846
|
+
if (end < 0) return void 0;
|
|
20847
|
+
index = end + 2;
|
|
20848
|
+
} else if (char === '"' || char === "`" || char === "[" || char === "'") {
|
|
20849
|
+
const close = char === "[" ? "]" : char;
|
|
20850
|
+
const read = quoted(sql, index, close, char !== "[");
|
|
20851
|
+
if (!read) return void 0;
|
|
20852
|
+
tokens.push({
|
|
20853
|
+
kind: char === "'" ? "string" : "name",
|
|
20854
|
+
value: read.value
|
|
20855
|
+
});
|
|
20856
|
+
index = read.end;
|
|
20857
|
+
} else if (char === "(" || char === ")" || char === ",") {
|
|
20858
|
+
tokens.push({ kind: "punct", value: char });
|
|
20859
|
+
index++;
|
|
20860
|
+
} else if (isWordChar(char)) {
|
|
20861
|
+
let end = index;
|
|
20862
|
+
while (end < sql.length && isWordChar(sql.charAt(end))) end++;
|
|
20863
|
+
tokens.push({ kind: "word", value: sql.slice(index, end) });
|
|
20864
|
+
index = end;
|
|
20865
|
+
} else {
|
|
20866
|
+
tokens.push({ kind: "other", value: char });
|
|
20867
|
+
index++;
|
|
20868
|
+
}
|
|
20869
|
+
}
|
|
20870
|
+
return tokens;
|
|
20871
|
+
}
|
|
20872
|
+
function isWord2(token, word) {
|
|
20873
|
+
return token?.kind === "word" && asciiLower(token.value) === word;
|
|
20874
|
+
}
|
|
20875
|
+
function definitions(tokens) {
|
|
20876
|
+
const open = tokens.findIndex(
|
|
20877
|
+
(token) => token.kind === "punct" && token.value === "("
|
|
20878
|
+
);
|
|
20879
|
+
if (open < 0) return void 0;
|
|
20880
|
+
const found = [[]];
|
|
20881
|
+
let depth = 0;
|
|
20882
|
+
for (const token of tokens.slice(open + 1)) {
|
|
20883
|
+
if (token.kind === "punct" && token.value === "(") depth++;
|
|
20884
|
+
if (token.kind === "punct" && token.value === ")") {
|
|
20885
|
+
if (depth === 0) return found;
|
|
20886
|
+
depth--;
|
|
20887
|
+
}
|
|
20888
|
+
if (depth === 0 && token.kind === "punct" && token.value === ",") {
|
|
20889
|
+
found.push([]);
|
|
20890
|
+
continue;
|
|
20891
|
+
}
|
|
20892
|
+
found.at(-1)?.push(token);
|
|
20893
|
+
}
|
|
20894
|
+
return void 0;
|
|
20895
|
+
}
|
|
20896
|
+
function collationOf(definition) {
|
|
20897
|
+
const named = [];
|
|
20898
|
+
let depth = 0;
|
|
20899
|
+
for (const [index, token] of definition.entries()) {
|
|
20900
|
+
if (token.kind === "punct" && token.value === "(") depth++;
|
|
20901
|
+
else if (token.kind === "punct" && token.value === ")") depth--;
|
|
20902
|
+
else if (depth === 0 && index > 0 && isWord2(token, "collate")) {
|
|
20903
|
+
const name = definition[index + 1];
|
|
20904
|
+
if (!name || name.kind === "punct" || name.kind === "other") return null;
|
|
20905
|
+
named.push(asciiUpper2(name.value));
|
|
20906
|
+
}
|
|
20907
|
+
}
|
|
20908
|
+
if (named.length === 0) return "BINARY";
|
|
20909
|
+
const [first] = named;
|
|
20910
|
+
if (named.some((name) => name !== first)) return null;
|
|
20911
|
+
return COLLATIONS.includes(first ?? "") ? first : null;
|
|
20912
|
+
}
|
|
20913
|
+
function declaredCollations(sql, columns) {
|
|
20914
|
+
const unknown2 = columns.map(() => null);
|
|
20915
|
+
if (typeof sql !== "string") return unknown2;
|
|
20916
|
+
const tokens = tokenize2(sql);
|
|
20917
|
+
if (!tokens || !isWord2(tokens[0], "create")) return unknown2;
|
|
20918
|
+
const kind = isWord2(tokens[1], "temp") || isWord2(tokens[1], "temporary") ? tokens[2] : tokens[1];
|
|
20919
|
+
if (!isWord2(kind, "table")) return unknown2;
|
|
20920
|
+
const found = definitions(tokens);
|
|
20921
|
+
if (!found || found.length < columns.length) return unknown2;
|
|
20922
|
+
const collations = [];
|
|
20923
|
+
for (const [index, column2] of columns.entries()) {
|
|
20924
|
+
const definition = found[index] ?? [];
|
|
20925
|
+
const name = definition[0];
|
|
20926
|
+
if (!name || name.kind === "punct" || name.kind === "other" || asciiLower(name.value) !== asciiLower(column2))
|
|
20927
|
+
return unknown2;
|
|
20928
|
+
collations.push(collationOf(definition));
|
|
20929
|
+
}
|
|
20930
|
+
return collations;
|
|
20931
|
+
}
|
|
20932
|
+
|
|
20453
20933
|
// ../../packages/data-core/src/schema.ts
|
|
20934
|
+
function columnAffinity(column2) {
|
|
20935
|
+
const type = asciiLower(column2.type);
|
|
20936
|
+
if (type.includes("int")) return "INTEGER";
|
|
20937
|
+
if (type.includes("char") || type.includes("clob") || type.includes("text"))
|
|
20938
|
+
return "TEXT";
|
|
20939
|
+
if (type.includes("blob") || type.trim() === "") return "BLOB";
|
|
20940
|
+
if (type.includes("real") || type.includes("floa") || type.includes("doub"))
|
|
20941
|
+
return "REAL";
|
|
20942
|
+
return "NUMERIC";
|
|
20943
|
+
}
|
|
20944
|
+
function numberClass(type) {
|
|
20945
|
+
const affinity2 = columnAffinity({ type });
|
|
20946
|
+
if (affinity2 === "TEXT" || affinity2 === "BLOB") return void 0;
|
|
20947
|
+
if (affinity2 !== "NUMERIC") return "number";
|
|
20948
|
+
const lower = asciiLower(type);
|
|
20949
|
+
return ["num", "dec", "bool", "bit"].some((word) => lower.includes(word)) ? "number" : "fallback";
|
|
20950
|
+
}
|
|
20951
|
+
var NUMBER_TEXT = /^[ \t\n\v\f\r]*[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[ \t\n\v\f\r]*$/;
|
|
20952
|
+
function storesAsNumber(value) {
|
|
20953
|
+
return value.includes("\0") || NUMBER_TEXT.test(value);
|
|
20954
|
+
}
|
|
20454
20955
|
var SCHEMA_SQL = [
|
|
20455
20956
|
`SELECT t.name AS table_name, t.type AS table_type, c.cid AS cid, c.name AS column_name, c.type AS column_type, c."notnull" AS not_null, c.dflt_value AS default_value, c.pk AS pk, c.hidden AS hidden FROM pragma_table_list AS t JOIN pragma_table_xinfo(t.name, t.schema) AS c WHERE t.schema = 'main' AND t.type IN ('table', 'virtual') AND ${systemTablePredicate("t.name")} ORDER BY t.name, c.cid`,
|
|
20456
20957
|
// Phase 2-2 (적대적 리뷰 반영): each table's CREATE TABLE text, where
|
|
@@ -20469,6 +20970,80 @@ function tableNamesFromRows(rows) {
|
|
|
20469
20970
|
}
|
|
20470
20971
|
return names;
|
|
20471
20972
|
}
|
|
20973
|
+
function tableNames(snapshot) {
|
|
20974
|
+
return [...snapshot.tables.values()].map((table) => table.name);
|
|
20975
|
+
}
|
|
20976
|
+
function text(value) {
|
|
20977
|
+
return typeof value === "string" ? value : "";
|
|
20978
|
+
}
|
|
20979
|
+
function integer2(value) {
|
|
20980
|
+
const number4 = Number(value);
|
|
20981
|
+
return Number.isFinite(number4) ? number4 : 0;
|
|
20982
|
+
}
|
|
20983
|
+
function snapshotFromRows(rows) {
|
|
20984
|
+
const building = /* @__PURE__ */ new Map();
|
|
20985
|
+
const ddl = /* @__PURE__ */ new Map();
|
|
20986
|
+
for (const row of rows)
|
|
20987
|
+
if (Object.hasOwn(row, "table_sql") && typeof row.table_name === "string")
|
|
20988
|
+
ddl.set(row.table_name, row.table_sql);
|
|
20989
|
+
for (const row of rows) {
|
|
20990
|
+
if (Object.hasOwn(row, "table_sql")) continue;
|
|
20991
|
+
const tableName = text(row.table_name);
|
|
20992
|
+
const kind = row.table_type;
|
|
20993
|
+
if (tableName === "" || kind !== "table" && kind !== "virtual") continue;
|
|
20994
|
+
if (isSystemTable(tableName)) continue;
|
|
20995
|
+
const hidden = integer2(row.hidden);
|
|
20996
|
+
if (hidden === 1) continue;
|
|
20997
|
+
const columnName = text(row.column_name);
|
|
20998
|
+
if (columnName === "") continue;
|
|
20999
|
+
const key = asciiLower(tableName);
|
|
21000
|
+
let table = building.get(key);
|
|
21001
|
+
if (!table) {
|
|
21002
|
+
table = { name: tableName, kind, columns: [] };
|
|
21003
|
+
building.set(key, table);
|
|
21004
|
+
}
|
|
21005
|
+
const defaultValue = row.default_value;
|
|
21006
|
+
table.columns.push({
|
|
21007
|
+
name: columnName,
|
|
21008
|
+
cid: integer2(row.cid),
|
|
21009
|
+
type: text(row.column_type),
|
|
21010
|
+
notNull: integer2(row.not_null) === 1,
|
|
21011
|
+
primaryKey: integer2(row.pk),
|
|
21012
|
+
defaultValue: defaultValue === null || defaultValue === void 0 ? null : String(defaultValue),
|
|
21013
|
+
generated: hidden === 2 || hidden === 3,
|
|
21014
|
+
collation: null
|
|
21015
|
+
});
|
|
21016
|
+
}
|
|
21017
|
+
const tables = /* @__PURE__ */ new Map();
|
|
21018
|
+
for (const [key, table] of building) {
|
|
21019
|
+
const sorted2 = table.columns.toSorted(
|
|
21020
|
+
(left, right) => left.cid - right.cid
|
|
21021
|
+
);
|
|
21022
|
+
const collations = table.kind === "table" ? declaredCollations(
|
|
21023
|
+
ddl.get(table.name),
|
|
21024
|
+
sorted2.map((column2) => column2.name)
|
|
21025
|
+
) : sorted2.map(() => null);
|
|
21026
|
+
const columns = sorted2.map((column2, index) => ({
|
|
21027
|
+
...column2,
|
|
21028
|
+
collation: collations[index] ?? null
|
|
21029
|
+
}));
|
|
21030
|
+
const sql = ddl.get(table.name);
|
|
21031
|
+
tables.set(key, {
|
|
21032
|
+
name: table.name,
|
|
21033
|
+
kind: table.kind,
|
|
21034
|
+
columns,
|
|
21035
|
+
byName: new Map(
|
|
21036
|
+
columns.map((column2) => [asciiLower(column2.name), column2])
|
|
21037
|
+
),
|
|
21038
|
+
sql: table.kind === "table" && typeof sql === "string" ? sql : null
|
|
21039
|
+
});
|
|
21040
|
+
}
|
|
21041
|
+
return { tables };
|
|
21042
|
+
}
|
|
21043
|
+
function resolveTable(snapshot, name) {
|
|
21044
|
+
if (isSystemTable(name)) return void 0;
|
|
21045
|
+
return snapshot.tables.get(asciiLower(name));
|
|
21046
|
+
}
|
|
20472
21047
|
async function readTableNames(db) {
|
|
20473
21048
|
return tableNamesFromRows(rowsOf(await db.prepare(TABLE_NAMES_SQL).all()));
|
|
20474
21049
|
}
|
|
@@ -20483,14 +21058,20 @@ function isPlainObject2(value) {
|
|
|
20483
21058
|
function isValue(value) {
|
|
20484
21059
|
return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
|
|
20485
21060
|
}
|
|
20486
|
-
function
|
|
21061
|
+
function toBoundValue(value) {
|
|
21062
|
+
if (value === null) return null;
|
|
21063
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
21064
|
+
if (typeof value === "string" || typeof value === "number") return value;
|
|
21065
|
+
return JSON.stringify(value);
|
|
21066
|
+
}
|
|
21067
|
+
function utf8ByteLength(text2) {
|
|
20487
21068
|
let bytes = 0;
|
|
20488
|
-
for (let index = 0; index <
|
|
20489
|
-
const unit =
|
|
21069
|
+
for (let index = 0; index < text2.length; index++) {
|
|
21070
|
+
const unit = text2.charCodeAt(index);
|
|
20490
21071
|
if (unit < 128) bytes += 1;
|
|
20491
21072
|
else if (unit < 2048) bytes += 2;
|
|
20492
|
-
else if (unit >= 55296 && unit <= 56319 && index + 1 <
|
|
20493
|
-
const next =
|
|
21073
|
+
else if (unit >= 55296 && unit <= 56319 && index + 1 < text2.length) {
|
|
21074
|
+
const next = text2.charCodeAt(index + 1);
|
|
20494
21075
|
if (next >= 56320 && next <= 57343) {
|
|
20495
21076
|
bytes += 4;
|
|
20496
21077
|
index++;
|
|
@@ -20501,6 +21082,19 @@ function utf8ByteLength(text) {
|
|
|
20501
21082
|
}
|
|
20502
21083
|
|
|
20503
21084
|
// ../../packages/data-core/src/sql-fragments.ts
|
|
21085
|
+
function quoteIdentifier(name) {
|
|
21086
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
21087
|
+
}
|
|
21088
|
+
var Failure = class extends Error {
|
|
21089
|
+
constructor(error51, reason) {
|
|
21090
|
+
super(error51.code);
|
|
21091
|
+
this.error = error51;
|
|
21092
|
+
this.reason = reason;
|
|
21093
|
+
}
|
|
21094
|
+
};
|
|
21095
|
+
function fail(error51, reason) {
|
|
21096
|
+
throw new Failure(error51, reason);
|
|
21097
|
+
}
|
|
20504
21098
|
function globLiteral(char) {
|
|
20505
21099
|
return char === "*" || char === "?" || char === "[" ? `[${char}]` : char;
|
|
20506
21100
|
}
|
|
@@ -20525,6 +21119,121 @@ function patternTooLong(pattern, caseInsensitive) {
|
|
|
20525
21119
|
if (pattern.length > MAX_PATTERN_BYTES * 2) return true;
|
|
20526
21120
|
return utf8ByteLength(boundPattern(pattern, caseInsensitive)) > MAX_PATTERN_BYTES;
|
|
20527
21121
|
}
|
|
21122
|
+
function isBoolean(target, value) {
|
|
21123
|
+
return `coalesce(CAST(${target} AS NUMERIC) ${value ? "<>" : "="} 0, 0)`;
|
|
21124
|
+
}
|
|
21125
|
+
var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
21126
|
+
function distinctArrayValues(values) {
|
|
21127
|
+
const seen = /* @__PURE__ */ new Set();
|
|
21128
|
+
const out = [];
|
|
21129
|
+
for (const item of values) {
|
|
21130
|
+
const bound = typeof item === "string" ? item.replace(LONE_SURROGATE, "\uFFFD") : item;
|
|
21131
|
+
const key = `${typeof bound}:${String(bound)}`;
|
|
21132
|
+
if (seen.has(key)) continue;
|
|
21133
|
+
seen.add(key);
|
|
21134
|
+
out.push(bound);
|
|
21135
|
+
}
|
|
21136
|
+
return out;
|
|
21137
|
+
}
|
|
21138
|
+
var MAX_ARRAY_TEXT_LENGTH = 65536;
|
|
21139
|
+
var ELEMENT = "_naro_j";
|
|
21140
|
+
function arraySql(target, operator, count2, bind2, nullElement = false) {
|
|
21141
|
+
const isNull = target();
|
|
21142
|
+
const length = target();
|
|
21143
|
+
const valid = target();
|
|
21144
|
+
const type = target();
|
|
21145
|
+
const each = () => `json_each(${target()}) AS ${ELEMENT}`;
|
|
21146
|
+
const element = `${ELEMENT}.value`;
|
|
21147
|
+
const list = () => `(${Array.from({ length: count2 }, bind2).join(", ")})`;
|
|
21148
|
+
let test;
|
|
21149
|
+
if (operator === "contains" && nullElement) test = "0";
|
|
21150
|
+
else if (count2 === 0)
|
|
21151
|
+
test = operator === "containedBy" ? `NOT EXISTS (SELECT 1 FROM ${each()})` : "0";
|
|
21152
|
+
else if (operator === "contains")
|
|
21153
|
+
test = `(SELECT COUNT(DISTINCT ${element}) FROM ${each()} WHERE ${element} IN ${list()}) = ${String(count2)}`;
|
|
21154
|
+
else if (operator === "overlaps")
|
|
21155
|
+
test = `EXISTS (SELECT 1 FROM ${each()} WHERE ${element} IN ${list()})`;
|
|
21156
|
+
else
|
|
21157
|
+
test = `NOT EXISTS (SELECT 1 FROM ${each()} WHERE ${element} IS NULL OR ${element} NOT IN ${list()})`;
|
|
21158
|
+
return `(CASE WHEN ${isNull} IS NULL OR length(${length}) > ${String(MAX_ARRAY_TEXT_LENGTH)} THEN NULL WHEN json_valid(${valid}) THEN json_type(${type}) = 'array' AND ${test} ELSE 0 END)`;
|
|
21159
|
+
}
|
|
21160
|
+
function isNumeric(item) {
|
|
21161
|
+
const affinity2 = columnAffinity(item);
|
|
21162
|
+
return affinity2 !== "TEXT" && affinity2 !== "BLOB";
|
|
21163
|
+
}
|
|
21164
|
+
function refusesText(item, text2) {
|
|
21165
|
+
const kind = numberClass(item.type);
|
|
21166
|
+
return kind === "number" || kind === "fallback" && storesAsNumber(text2);
|
|
21167
|
+
}
|
|
21168
|
+
function readsAsIs(item, value) {
|
|
21169
|
+
return typeof value === "string" && numberClass(item.type) === "fallback" && !storesAsNumber(value);
|
|
21170
|
+
}
|
|
21171
|
+
function asStored(item, sql) {
|
|
21172
|
+
switch (columnAffinity(item)) {
|
|
21173
|
+
case "INTEGER":
|
|
21174
|
+
case "NUMERIC":
|
|
21175
|
+
return `CAST(${sql} AS NUMERIC)`;
|
|
21176
|
+
case "REAL":
|
|
21177
|
+
return `CAST(${sql} AS REAL)`;
|
|
21178
|
+
case "TEXT":
|
|
21179
|
+
return `CAST(${sql} AS TEXT)`;
|
|
21180
|
+
case "BLOB":
|
|
21181
|
+
return sql;
|
|
21182
|
+
}
|
|
21183
|
+
}
|
|
21184
|
+
var LITERAL_DEFAULT = /^(?:null|true|false|[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?|'(?:[^']|'')*')$/i;
|
|
21185
|
+
function omittedSql(item) {
|
|
21186
|
+
if (item.primaryKey > 0) return void 0;
|
|
21187
|
+
const text2 = item.defaultValue;
|
|
21188
|
+
if (text2 === null) return "NULL";
|
|
21189
|
+
if (!LITERAL_DEFAULT.test(text2)) return void 0;
|
|
21190
|
+
const word = asciiLower(text2);
|
|
21191
|
+
if (word === "null") return "NULL";
|
|
21192
|
+
if (word === "true" || word === "false")
|
|
21193
|
+
return asStored(item, word === "true" ? "1" : "0");
|
|
21194
|
+
if (text2.startsWith("'") && isNumeric(item)) return void 0;
|
|
21195
|
+
return asStored(item, text2);
|
|
21196
|
+
}
|
|
21197
|
+
var DEFAULT_KEYWORDS = /* @__PURE__ */ new Set([
|
|
21198
|
+
"true",
|
|
21199
|
+
"false",
|
|
21200
|
+
"null",
|
|
21201
|
+
"current_date",
|
|
21202
|
+
"current_time",
|
|
21203
|
+
"current_timestamp"
|
|
21204
|
+
]);
|
|
21205
|
+
var BARE_WORD = /^[A-Za-z_\u0080-][A-Za-z0-9_$\u0080-]*$/;
|
|
21206
|
+
function quotedName(text2) {
|
|
21207
|
+
const double = /^"((?:[^"]|"")*)"$/.exec(text2);
|
|
21208
|
+
if (double) return (double[1] ?? "").replaceAll('""', '"');
|
|
21209
|
+
const tick = /^`((?:[^`]|``)*)`$/.exec(text2);
|
|
21210
|
+
if (tick) return (tick[1] ?? "").replaceAll("``", "`");
|
|
21211
|
+
const bracket = /^\[([^\]]*)\]$/.exec(text2);
|
|
21212
|
+
return bracket ? bracket[1] ?? "" : void 0;
|
|
21213
|
+
}
|
|
21214
|
+
function stringDefault(item) {
|
|
21215
|
+
let text2 = item.defaultValue?.trim();
|
|
21216
|
+
if (text2 === void 0) return void 0;
|
|
21217
|
+
while (text2.startsWith("(") && text2.endsWith(")"))
|
|
21218
|
+
text2 = text2.slice(1, -1).trim();
|
|
21219
|
+
if (/^\+\s*(?:'|x')/i.test(text2)) text2 = text2.slice(1).trim();
|
|
21220
|
+
if (/^'(?:[^']|'')*'$/.test(text2))
|
|
21221
|
+
return { kind: "text", text: text2.slice(1, -1).replaceAll("''", "'") };
|
|
21222
|
+
if (/^x'[0-9a-f]*'$/i.test(text2)) return { kind: "blob" };
|
|
21223
|
+
const name = quotedName(text2);
|
|
21224
|
+
if (name !== void 0) return { kind: "text", text: name };
|
|
21225
|
+
if (BARE_WORD.test(text2) && !DEFAULT_KEYWORDS.has(asciiLower(text2)))
|
|
21226
|
+
return { kind: "text", text: text2 };
|
|
21227
|
+
return void 0;
|
|
21228
|
+
}
|
|
21229
|
+
var DEFAULT_CHECK_NONCE = "0";
|
|
21230
|
+
function checkMarker(action, nonce, restrictive) {
|
|
21231
|
+
const index = restrictive === void 0 ? "" : `:${String(restrictive)}`;
|
|
21232
|
+
return `json_extract('{}', 'naro:check:${action}:${nonce}${index}')`;
|
|
21233
|
+
}
|
|
21234
|
+
function schemaMarker(nonce) {
|
|
21235
|
+
return `json_extract('{}', 'naro:schema:${nonce}')`;
|
|
21236
|
+
}
|
|
20528
21237
|
|
|
20529
21238
|
// ../../packages/data-core/src/policy-expr.ts
|
|
20530
21239
|
var MAX_EXPRESSION_BYTES = 3 * 16384;
|
|
@@ -20626,25 +21335,25 @@ function nested(state, token, read) {
|
|
|
20626
21335
|
}
|
|
20627
21336
|
}
|
|
20628
21337
|
function truthProblem(operand) {
|
|
20629
|
-
const
|
|
21338
|
+
const text2 = "is text, where 'true' reads as false";
|
|
20630
21339
|
switch (operand.kind) {
|
|
20631
21340
|
case "column":
|
|
20632
21341
|
return void 0;
|
|
20633
21342
|
case "literal":
|
|
20634
|
-
return typeof operand.value === "string" ? `a '\u2026' string ${
|
|
21343
|
+
return typeof operand.value === "string" ? `a '\u2026' string ${text2}: write TRUE or FALSE, or compare the string with =` : void 0;
|
|
20635
21344
|
case "jwt": {
|
|
20636
21345
|
const claim = printValue(operand);
|
|
20637
|
-
return `${claim} ${
|
|
21346
|
+
return `${claim} ${text2}: compare it with text, ${claim} = 'true'`;
|
|
20638
21347
|
}
|
|
20639
21348
|
case "auth":
|
|
20640
21349
|
return operand.fn === "uid" ? "auth.uid() is text: compare it, auth.uid() = user_id, or test auth.uid() IS NOT NULL" : "auth.role() is text: compare it, auth.role() = 'authenticated'";
|
|
20641
21350
|
case "cast":
|
|
20642
|
-
return `a ::text cast ${
|
|
21351
|
+
return `a ::text cast ${text2}: compare it with text, (\u2026)::text = 'true'`;
|
|
20643
21352
|
case "func":
|
|
20644
21353
|
switch (operand.name) {
|
|
20645
21354
|
case "lower":
|
|
20646
21355
|
case "upper":
|
|
20647
|
-
return `${operand.name}(\u2026) ${
|
|
21356
|
+
return `${operand.name}(\u2026) ${text2}: compare it with text, ${operand.name}(\u2026) = 'true'`;
|
|
20648
21357
|
case "coalesce":
|
|
20649
21358
|
return "coalesce(\u2026) is not a boolean: compare it, coalesce(\u2026) = 'true'";
|
|
20650
21359
|
case "length":
|
|
@@ -20703,9 +21412,9 @@ function booleanComparisonProblem(expr) {
|
|
|
20703
21412
|
const met = [...operandValues(left), ...operandValues(right)];
|
|
20704
21413
|
if (!met.some(isBooleanConstant)) continue;
|
|
20705
21414
|
const texts = met.filter(isTextValue);
|
|
20706
|
-
const
|
|
20707
|
-
if (
|
|
20708
|
-
const problem =
|
|
21415
|
+
const text2 = texts.find((value) => value.kind !== "literal") ?? texts[0];
|
|
21416
|
+
if (text2 === void 0) continue;
|
|
21417
|
+
const problem = text2.kind === "literal" ? "a '\u2026' string is text, where 'true' reads as false: write TRUE or FALSE on both sides, or a '\u2026' string on both" : truthProblem(text2);
|
|
20709
21418
|
return `a comparison with TRUE or FALSE compares a boolean, and ${String(problem)}`;
|
|
20710
21419
|
}
|
|
20711
21420
|
return void 0;
|
|
@@ -21261,17 +21970,17 @@ function readCondition(stream) {
|
|
|
21261
21970
|
return requireCondition(expr, token);
|
|
21262
21971
|
}
|
|
21263
21972
|
function checkExpressionSize(expr, position) {
|
|
21264
|
-
const
|
|
21265
|
-
if (utf8ByteLength(
|
|
21973
|
+
const text2 = printExpr(expr);
|
|
21974
|
+
if (utf8ByteLength(text2) > MAX_EXPRESSION_BYTES)
|
|
21266
21975
|
throw new PolicySyntaxError(
|
|
21267
21976
|
`the expression is longer than ${String(MAX_EXPRESSION_BYTES)} bytes`,
|
|
21268
21977
|
position
|
|
21269
21978
|
);
|
|
21270
|
-
return
|
|
21979
|
+
return text2;
|
|
21271
21980
|
}
|
|
21272
|
-
function parseExpression(
|
|
21981
|
+
function parseExpression(text2) {
|
|
21273
21982
|
try {
|
|
21274
|
-
const stream = new TokenStream(tokenize(
|
|
21983
|
+
const stream = new TokenStream(tokenize(text2));
|
|
21275
21984
|
if (stream.peek().kind === "eof")
|
|
21276
21985
|
throw new PolicySyntaxError("the expression is empty", 1);
|
|
21277
21986
|
const expr = readCondition(stream);
|
|
@@ -21286,22 +21995,22 @@ function parseExpression(text) {
|
|
|
21286
21995
|
function quoteIdent(name) {
|
|
21287
21996
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !SPECIAL_WORDS.has(asciiLower(name)) ? name : `"${name.replaceAll('"', '""')}"`;
|
|
21288
21997
|
}
|
|
21289
|
-
function commentText(
|
|
21998
|
+
function commentText(text2) {
|
|
21290
21999
|
let out = "";
|
|
21291
|
-
for (const char of
|
|
22000
|
+
for (const char of text2) {
|
|
21292
22001
|
const code = char.charCodeAt(0);
|
|
21293
22002
|
const control = code < 32 || code >= 127 && code <= 159 || code === 8232 || code === 8233;
|
|
21294
22003
|
out += control ? `\\u${code.toString(16).padStart(4, "0")}` : char;
|
|
21295
22004
|
}
|
|
21296
22005
|
return out;
|
|
21297
22006
|
}
|
|
21298
|
-
function quoteString(
|
|
21299
|
-
return `'${
|
|
22007
|
+
function quoteString(text2) {
|
|
22008
|
+
return `'${text2.replaceAll("'", "''")}'`;
|
|
21300
22009
|
}
|
|
21301
22010
|
function printNumber(value) {
|
|
21302
22011
|
if (Object.is(value, -0)) return "-0.0";
|
|
21303
|
-
const
|
|
21304
|
-
return Number.isSafeInteger(value) || /[.e]/.test(
|
|
22012
|
+
const text2 = String(value);
|
|
22013
|
+
return Number.isSafeInteger(value) || /[.e]/.test(text2) ? text2 : `${text2}.0`;
|
|
21305
22014
|
}
|
|
21306
22015
|
function printLiteral(value) {
|
|
21307
22016
|
if (value === null) return "NULL";
|
|
@@ -21433,6 +22142,30 @@ function unqualifiedProblem(ref) {
|
|
|
21433
22142
|
const name = quoteIdent(ref.name);
|
|
21434
22143
|
return `column ${name} has no table or alias in front of it inside a subquery: naro policy create writes one on every column there, so that a column added to a table later cannot change which table the policy reads it from \u2014 drop the policy and create it again`;
|
|
21435
22144
|
}
|
|
22145
|
+
function subqueriesOf(expr) {
|
|
22146
|
+
const found = [];
|
|
22147
|
+
const visit = (node) => {
|
|
22148
|
+
if (isValueExpr(node)) return;
|
|
22149
|
+
switch (node.kind) {
|
|
22150
|
+
case "and":
|
|
22151
|
+
case "or":
|
|
22152
|
+
node.args.forEach(visit);
|
|
22153
|
+
return;
|
|
22154
|
+
case "not":
|
|
22155
|
+
visit(node.arg);
|
|
22156
|
+
return;
|
|
22157
|
+
case "exists":
|
|
22158
|
+
case "inSelect":
|
|
22159
|
+
found.push(node.subquery);
|
|
22160
|
+
if (node.subquery.where) visit(node.subquery.where);
|
|
22161
|
+
return;
|
|
22162
|
+
default:
|
|
22163
|
+
return;
|
|
22164
|
+
}
|
|
22165
|
+
};
|
|
22166
|
+
visit(expr);
|
|
22167
|
+
return found;
|
|
22168
|
+
}
|
|
21436
22169
|
|
|
21437
22170
|
// ../../packages/data-core/src/named-policy.ts
|
|
21438
22171
|
var POLICY_COMMANDS = [
|
|
@@ -21487,17 +22220,33 @@ function policyShapeProblem(policy) {
|
|
|
21487
22220
|
return `${command === "all" ? "An ALL" : "An UPDATE"} policy needs USING (\u2026), WITH CHECK (\u2026) or both.`;
|
|
21488
22221
|
return void 0;
|
|
21489
22222
|
}
|
|
21490
|
-
function
|
|
22223
|
+
function policySql(policy) {
|
|
22224
|
+
const parts = [
|
|
22225
|
+
`CREATE POLICY ${quotePolicyName(policy.name)} ON ${quoteIdent(policy.table)}`
|
|
22226
|
+
];
|
|
22227
|
+
if (!policy.permissive) parts.push("AS RESTRICTIVE");
|
|
22228
|
+
if (policy.command !== "all")
|
|
22229
|
+
parts.push(`FOR ${policy.command.toUpperCase()}`);
|
|
22230
|
+
const roles = rolesText(policy.roles);
|
|
22231
|
+
if (roles !== "public") parts.push(`TO ${roles}`);
|
|
22232
|
+
if (policy.using !== null) parts.push(`USING (${policy.using})`);
|
|
22233
|
+
if (policy.check !== null) parts.push(`WITH CHECK (${policy.check})`);
|
|
22234
|
+
const sql = `${parts.join(" ")};`;
|
|
22235
|
+
if (policy.owner === null) return sql;
|
|
22236
|
+
const owner = commentText(quoteIdent(policy.owner));
|
|
22237
|
+
return `${sql} -- naro: owner ${owner}${policy.command === "insert" || policy.command === "all" ? ` (an insert that leaves ${owner} out gets auth.uid())` : ""}`;
|
|
22238
|
+
}
|
|
22239
|
+
function ownerExpression(column2) {
|
|
21491
22240
|
return printExpr({
|
|
21492
22241
|
kind: "compare",
|
|
21493
22242
|
op: "=",
|
|
21494
|
-
left: { kind: "column", qualifier: null, name:
|
|
22243
|
+
left: { kind: "column", qualifier: null, name: column2 },
|
|
21495
22244
|
right: { kind: "auth", fn: "uid" }
|
|
21496
22245
|
});
|
|
21497
22246
|
}
|
|
21498
|
-
function ownerClauses(
|
|
21499
|
-
const
|
|
21500
|
-
return command === "insert" ? { using: null, check:
|
|
22247
|
+
function ownerClauses(column2, command) {
|
|
22248
|
+
const text2 = ownerExpression(column2);
|
|
22249
|
+
return command === "insert" ? { using: null, check: text2 } : { using: text2, check: null };
|
|
21501
22250
|
}
|
|
21502
22251
|
var NAMED_POLICY_TABLE = "_naro_policies_v2";
|
|
21503
22252
|
var NAMED_POLICY_TABLE_SQL = `CREATE TABLE IF NOT EXISTS ${NAMED_POLICY_TABLE} (table_name TEXT NOT NULL, name TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'permissive', command TEXT NOT NULL DEFAULT 'all', roles TEXT NOT NULL DEFAULT 'public', using_expr TEXT, check_expr TEXT, owner_column TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (table_name, name))`;
|
|
@@ -21543,21 +22292,21 @@ function storedExpression(value, field) {
|
|
|
21543
22292
|
function loadPolicy(row, log2) {
|
|
21544
22293
|
const table = row.table_name;
|
|
21545
22294
|
if (typeof table !== "string" || table === "") return void 0;
|
|
21546
|
-
const
|
|
22295
|
+
const problems2 = [];
|
|
21547
22296
|
const name = typeof row.name === "string" && row.name !== "" ? row.name : "(unnamed)";
|
|
21548
|
-
if (name === "(unnamed)")
|
|
22297
|
+
if (name === "(unnamed)") problems2.push("the name is missing");
|
|
21549
22298
|
const kind = row.kind;
|
|
21550
22299
|
if (kind !== "permissive" && kind !== "restrictive")
|
|
21551
|
-
|
|
22300
|
+
problems2.push("kind is not permissive or restrictive");
|
|
21552
22301
|
const command = POLICY_COMMANDS.includes(row.command) ? row.command : void 0;
|
|
21553
22302
|
if (!command)
|
|
21554
|
-
|
|
22303
|
+
problems2.push("command is not one of all, select, insert, update, delete");
|
|
21555
22304
|
const roles = typeof row.roles === "string" && ["public", "anon", "authenticated"].includes(row.roles) ? readRoles([row.roles]) : void 0;
|
|
21556
|
-
if (!roles?.ok)
|
|
22305
|
+
if (!roles?.ok) problems2.push("roles is not public, anon or authenticated");
|
|
21557
22306
|
const using = storedExpression(row.using_expr, "using");
|
|
21558
22307
|
const check2 = storedExpression(row.check_expr, "check");
|
|
21559
|
-
if (using.problem)
|
|
21560
|
-
if (check2.problem)
|
|
22308
|
+
if (using.problem) problems2.push(using.problem);
|
|
22309
|
+
if (check2.problem) problems2.push(check2.problem);
|
|
21561
22310
|
const owner = row.owner_column;
|
|
21562
22311
|
const policy = {
|
|
21563
22312
|
table,
|
|
@@ -21573,30 +22322,47 @@ function loadPolicy(row, log2) {
|
|
|
21573
22322
|
usingExpr: using.expr,
|
|
21574
22323
|
checkExpr: check2.expr
|
|
21575
22324
|
};
|
|
21576
|
-
if (command &&
|
|
22325
|
+
if (command && problems2.length === 0) {
|
|
21577
22326
|
const shape = policyShapeProblem(policy);
|
|
21578
|
-
if (shape)
|
|
22327
|
+
if (shape) problems2.push(shape);
|
|
21579
22328
|
}
|
|
21580
|
-
if (policy.owner !== null && policy.owner !== "" && command &&
|
|
22329
|
+
if (policy.owner !== null && policy.owner !== "" && command && problems2.length === 0) {
|
|
21581
22330
|
const expected = ownerClauses(policy.owner, command);
|
|
21582
22331
|
if (policy.using !== expected.using || policy.check !== expected.check)
|
|
21583
|
-
|
|
22332
|
+
problems2.push(
|
|
21584
22333
|
"an owner policy's expressions are not its owner condition"
|
|
21585
22334
|
);
|
|
21586
22335
|
}
|
|
21587
|
-
if (
|
|
21588
|
-
policy.invalid =
|
|
22336
|
+
if (problems2.length > 0) {
|
|
22337
|
+
policy.invalid = problems2.join("; ");
|
|
21589
22338
|
log2?.(
|
|
21590
22339
|
`naro data-core: policy ${name} on ${table} cannot be read: ${policy.invalid}`
|
|
21591
22340
|
);
|
|
21592
22341
|
}
|
|
21593
22342
|
return policy;
|
|
21594
22343
|
}
|
|
21595
|
-
function
|
|
21596
|
-
|
|
22344
|
+
function applicablePolicies(policies, table, command, role) {
|
|
22345
|
+
const found = policiesOn(policies, table).filter(
|
|
22346
|
+
(policy) => (policy.command === "all" || policy.command === command) && policy.roles.includes(role)
|
|
22347
|
+
);
|
|
22348
|
+
return {
|
|
22349
|
+
permissive: found.filter((policy) => policy.permissive),
|
|
22350
|
+
restrictive: found.filter((policy) => !policy.permissive),
|
|
22351
|
+
invalid: found.find((policy) => policy.invalid !== null)
|
|
22352
|
+
};
|
|
22353
|
+
}
|
|
22354
|
+
function policiesOn(policies, table) {
|
|
22355
|
+
const folded = asciiLower(table);
|
|
22356
|
+
return policies.filter((policy) => asciiLower(policy.table) === folded);
|
|
22357
|
+
}
|
|
22358
|
+
function foldPolicyName(name, quoted2) {
|
|
22359
|
+
return quoted2 ? name : asciiLower(name);
|
|
22360
|
+
}
|
|
22361
|
+
function sqlStringLiteral(text2) {
|
|
22362
|
+
return text2 === null ? "NULL" : quoteString(text2);
|
|
21597
22363
|
}
|
|
21598
|
-
function policySqlTooLong(
|
|
21599
|
-
return utf8ByteLength(
|
|
22364
|
+
function policySqlTooLong(text2) {
|
|
22365
|
+
return utf8ByteLength(text2) > MAX_POLICY_SQL_BYTES;
|
|
21600
22366
|
}
|
|
21601
22367
|
|
|
21602
22368
|
// ../../packages/data-core/src/query.ts
|
|
@@ -21620,6 +22386,7 @@ function isAuthUid(value) {
|
|
|
21620
22386
|
return isPlainObject2(value) && Object.keys(value).length === 1 && Object.hasOwn(value, "auth") && value.auth === "uid";
|
|
21621
22387
|
}
|
|
21622
22388
|
var MAX_BODY_BYTES = 1024 * 1024;
|
|
22389
|
+
var MAX_LIMIT = 1e3;
|
|
21623
22390
|
var MAX_FILTERS = 100;
|
|
21624
22391
|
var MAX_FILTER_DEPTH = 4;
|
|
21625
22392
|
var MAX_ARRAY_VALUES = 100;
|
|
@@ -21682,11 +22449,11 @@ function readValue2(value, path, auth = false) {
|
|
|
21682
22449
|
);
|
|
21683
22450
|
return value;
|
|
21684
22451
|
}
|
|
21685
|
-
function longerThan(
|
|
21686
|
-
if (
|
|
21687
|
-
if (
|
|
22452
|
+
function longerThan(text2, max) {
|
|
22453
|
+
if (text2.length <= max) return false;
|
|
22454
|
+
if (text2.length > max * 2) return true;
|
|
21688
22455
|
let count2 = 0;
|
|
21689
|
-
for (const _ of
|
|
22456
|
+
for (const _ of text2) if (++count2 > max) return true;
|
|
21690
22457
|
return false;
|
|
21691
22458
|
}
|
|
21692
22459
|
function endsWithLoneEscape(pattern) {
|
|
@@ -21706,7 +22473,7 @@ function readFilter(value, path, auth) {
|
|
|
21706
22473
|
`${path}.operator`,
|
|
21707
22474
|
`must be one of ${FILTER_OPERATORS.join(", ")}`
|
|
21708
22475
|
);
|
|
21709
|
-
const
|
|
22476
|
+
const column2 = readName(own(object2, "column"), `${path}.column`);
|
|
21710
22477
|
if (!Object.hasOwn(object2, "value")) invalid(`${path}.value`, "is required");
|
|
21711
22478
|
const raw = own(object2, "value");
|
|
21712
22479
|
const valuePath = `${path}.value`;
|
|
@@ -21716,12 +22483,12 @@ function readFilter(value, path, auth) {
|
|
|
21716
22483
|
const values = raw.map(
|
|
21717
22484
|
(item, index) => readValue2(item, `${valuePath}[${index}]`, auth)
|
|
21718
22485
|
);
|
|
21719
|
-
return { operator, column, value: values };
|
|
22486
|
+
return { operator, column: column2, value: values };
|
|
21720
22487
|
}
|
|
21721
22488
|
case "is":
|
|
21722
22489
|
if (raw !== null && raw !== true && raw !== false)
|
|
21723
22490
|
invalid(valuePath, "must be null, true or false");
|
|
21724
|
-
return { operator, column, value: raw };
|
|
22491
|
+
return { operator, column: column2, value: raw };
|
|
21725
22492
|
case "like":
|
|
21726
22493
|
case "ilike": {
|
|
21727
22494
|
if (typeof raw !== "string") invalid(valuePath, "must be a string");
|
|
@@ -21731,7 +22498,7 @@ function readFilter(value, path, auth) {
|
|
|
21731
22498
|
invalid(valuePath, "must not contain a NUL character");
|
|
21732
22499
|
if (endsWithLoneEscape(raw))
|
|
21733
22500
|
invalid(valuePath, "must not end with an unescaped backslash");
|
|
21734
|
-
return { operator, column, value: raw };
|
|
22501
|
+
return { operator, column: column2, value: raw };
|
|
21735
22502
|
}
|
|
21736
22503
|
case "contains":
|
|
21737
22504
|
case "containedBy":
|
|
@@ -21745,13 +22512,13 @@ function readFilter(value, path, auth) {
|
|
|
21745
22512
|
);
|
|
21746
22513
|
return {
|
|
21747
22514
|
operator,
|
|
21748
|
-
column,
|
|
22515
|
+
column: column2,
|
|
21749
22516
|
value: raw.map(
|
|
21750
22517
|
(item) => isAuthUid(item) ? { auth: "uid" } : item
|
|
21751
22518
|
)
|
|
21752
22519
|
};
|
|
21753
22520
|
default:
|
|
21754
|
-
return { operator, column, value: readValue2(raw, valuePath, auth) };
|
|
22521
|
+
return { operator, column: column2, value: readValue2(raw, valuePath, auth) };
|
|
21755
22522
|
}
|
|
21756
22523
|
}
|
|
21757
22524
|
var NODE_KEYS = ["operator", "or", "and", "not"];
|
|
@@ -21843,8 +22610,8 @@ function parseCondition(action, value) {
|
|
|
21843
22610
|
}
|
|
21844
22611
|
function conditionsTooLarge(texts) {
|
|
21845
22612
|
let total = 0;
|
|
21846
|
-
for (const
|
|
21847
|
-
if (typeof
|
|
22613
|
+
for (const text2 of texts)
|
|
22614
|
+
if (typeof text2 === "string") total += utf8ByteLength(text2);
|
|
21848
22615
|
return total > MAX_CONDITION_BYTES;
|
|
21849
22616
|
}
|
|
21850
22617
|
function isInvalidCondition(value) {
|
|
@@ -21904,8 +22671,8 @@ var LEGACY_ROW_COLUMNS = [
|
|
|
21904
22671
|
];
|
|
21905
22672
|
function legacyRowAsRead(row) {
|
|
21906
22673
|
const values = [];
|
|
21907
|
-
for (const
|
|
21908
|
-
const value = row[
|
|
22674
|
+
for (const column2 of LEGACY_ROW_COLUMNS) {
|
|
22675
|
+
const value = row[column2];
|
|
21909
22676
|
if (value === null || value === void 0) values.push(null);
|
|
21910
22677
|
else if (typeof value === "string") values.push(value);
|
|
21911
22678
|
else return void 0;
|
|
@@ -21930,14 +22697,14 @@ var POLICY_SQL = {
|
|
|
21930
22697
|
*/
|
|
21931
22698
|
insertConverted: `INSERT OR IGNORE INTO _naro_policies_v2 (table_name, name, kind, command, roles, using_expr, check_expr, owner_column) SELECT ?, ?, ?, ?, ?, ?, ?, ? WHERE EXISTS (SELECT 1 FROM _naro_policies WHERE ${ROW_AS_READ})`
|
|
21932
22699
|
};
|
|
21933
|
-
function readStoredCondition(action,
|
|
21934
|
-
if (
|
|
21935
|
-
if (typeof
|
|
22700
|
+
function readStoredCondition(action, text2, tooLarge) {
|
|
22701
|
+
if (text2 === void 0 || text2 === null) return "is missing";
|
|
22702
|
+
if (typeof text2 !== "string") return "is not JSON";
|
|
21936
22703
|
if (tooLarge)
|
|
21937
22704
|
return `the conditions of the policy are larger than ${String(MAX_CONDITION_BYTES)} bytes`;
|
|
21938
22705
|
let value;
|
|
21939
22706
|
try {
|
|
21940
|
-
value = JSON.parse(
|
|
22707
|
+
value = JSON.parse(text2);
|
|
21941
22708
|
} catch {
|
|
21942
22709
|
return "is not JSON";
|
|
21943
22710
|
}
|
|
@@ -21952,7 +22719,7 @@ function policyFromRow(row, log2) {
|
|
|
21952
22719
|
(action) => row[`${action}_rule`] === CONDITION_RULE ? row[`${action}_condition`] : void 0
|
|
21953
22720
|
);
|
|
21954
22721
|
const tooLarge = conditionsTooLarge(
|
|
21955
|
-
texts.filter((
|
|
22722
|
+
texts.filter((text2) => typeof text2 === "string")
|
|
21956
22723
|
);
|
|
21957
22724
|
const read = POLICY_ACTIONS2.map(
|
|
21958
22725
|
(action, index) => {
|
|
@@ -22042,7 +22809,7 @@ function filterNodeToExpr(node, negated = false) {
|
|
|
22042
22809
|
return negated ? { kind: "or", args: [leaf, uidIsNull(false)] } : { kind: "and", args: [leaf, uidIsNull(true)] };
|
|
22043
22810
|
}
|
|
22044
22811
|
function leafToExpr(node) {
|
|
22045
|
-
const
|
|
22812
|
+
const column2 = {
|
|
22046
22813
|
kind: "column",
|
|
22047
22814
|
qualifier: null,
|
|
22048
22815
|
name: node.column
|
|
@@ -22058,7 +22825,7 @@ function leafToExpr(node) {
|
|
|
22058
22825
|
return {
|
|
22059
22826
|
kind: "compare",
|
|
22060
22827
|
op: COMPARISONS[node.operator],
|
|
22061
|
-
left:
|
|
22828
|
+
left: column2,
|
|
22062
22829
|
right: valueExpr(Array.isArray(value) ? null : value)
|
|
22063
22830
|
};
|
|
22064
22831
|
case "in": {
|
|
@@ -22067,7 +22834,7 @@ function leafToExpr(node) {
|
|
|
22067
22834
|
return {
|
|
22068
22835
|
kind: "in",
|
|
22069
22836
|
not: false,
|
|
22070
|
-
operand:
|
|
22837
|
+
operand: column2,
|
|
22071
22838
|
list: list.map(valueExpr)
|
|
22072
22839
|
};
|
|
22073
22840
|
}
|
|
@@ -22075,7 +22842,7 @@ function leafToExpr(node) {
|
|
|
22075
22842
|
return {
|
|
22076
22843
|
kind: "is",
|
|
22077
22844
|
not: false,
|
|
22078
|
-
operand:
|
|
22845
|
+
operand: column2,
|
|
22079
22846
|
value: value === true || value === false ? value : null
|
|
22080
22847
|
};
|
|
22081
22848
|
case "like":
|
|
@@ -22084,7 +22851,7 @@ function leafToExpr(node) {
|
|
|
22084
22851
|
kind: "like",
|
|
22085
22852
|
not: false,
|
|
22086
22853
|
ci: node.operator === "ilike",
|
|
22087
|
-
operand:
|
|
22854
|
+
operand: column2,
|
|
22088
22855
|
pattern: typeof value === "string" ? value : ""
|
|
22089
22856
|
};
|
|
22090
22857
|
case "contains":
|
|
@@ -22123,12 +22890,12 @@ function namedFromRule(table, action, rule, ownerColumn) {
|
|
|
22123
22890
|
check: action === "insert" ? TRUE_TEXT : null
|
|
22124
22891
|
};
|
|
22125
22892
|
if (rule === "owner") {
|
|
22126
|
-
const
|
|
22893
|
+
const column2 = ownerColumn ?? "";
|
|
22127
22894
|
return {
|
|
22128
22895
|
...base2,
|
|
22129
22896
|
roles: ["authenticated"],
|
|
22130
|
-
...
|
|
22131
|
-
owner:
|
|
22897
|
+
...column2 === "" ? { using: null, check: null } : ownerClauses(column2, action),
|
|
22898
|
+
owner: column2
|
|
22132
22899
|
};
|
|
22133
22900
|
}
|
|
22134
22901
|
if (isInvalidCondition(rule) || (action === "insert" ? rule.check : rule.using) === void 0)
|
|
@@ -22166,7 +22933,7 @@ function loadNamed(policies, log2) {
|
|
|
22166
22933
|
const values = policyValues(policy);
|
|
22167
22934
|
const loaded = loadPolicy(
|
|
22168
22935
|
Object.fromEntries(
|
|
22169
|
-
columns.map((
|
|
22936
|
+
columns.map((column2, index) => [column2, values[index]])
|
|
22170
22937
|
),
|
|
22171
22938
|
log2
|
|
22172
22939
|
);
|
|
@@ -22190,64 +22957,2112 @@ function unreadableConversions(policy) {
|
|
|
22190
22957
|
return found;
|
|
22191
22958
|
}
|
|
22192
22959
|
|
|
22193
|
-
// ../../packages/data-core/src/
|
|
22194
|
-
var
|
|
22195
|
-
|
|
22196
|
-
|
|
22197
|
-
|
|
22198
|
-
|
|
22199
|
-
|
|
22200
|
-
|
|
22201
|
-
|
|
22202
|
-
|
|
22203
|
-
|
|
22204
|
-
stream.next();
|
|
22205
|
-
return foldPolicyName(token.value, token.quoted);
|
|
22960
|
+
// ../../packages/data-core/src/rls-compile.ts
|
|
22961
|
+
var MAX_INLINE_DEPTH = 4;
|
|
22962
|
+
var MAX_STATEMENT_SUBQUERIES = 16;
|
|
22963
|
+
var MAX_POLICY_VALUES = 50;
|
|
22964
|
+
var MAX_POLICY_STATEMENT_BYTES = 5e4;
|
|
22965
|
+
function policyInvalid(policy, action) {
|
|
22966
|
+
return policy.owner !== null ? dataError("POLICY_INVALID", { action, table: policy.table }) : dataError("POLICY_INVALID", {
|
|
22967
|
+
action,
|
|
22968
|
+
table: policy.table,
|
|
22969
|
+
policy: policy.name
|
|
22970
|
+
});
|
|
22206
22971
|
}
|
|
22207
|
-
function
|
|
22208
|
-
|
|
22209
|
-
throw new PolicySyntaxError(
|
|
22210
|
-
`expected a role, found ${describeToken(token)}`,
|
|
22211
|
-
token.position
|
|
22212
|
-
);
|
|
22213
|
-
return token.quoted ? token.value : asciiLower(token.value);
|
|
22972
|
+
function newState2() {
|
|
22973
|
+
return { aliases: 0, subqueries: 0, depth: 0, active: [] };
|
|
22214
22974
|
}
|
|
22215
|
-
function
|
|
22216
|
-
|
|
22217
|
-
|
|
22218
|
-
"all",
|
|
22219
|
-
"select",
|
|
22220
|
-
"insert",
|
|
22221
|
-
"update",
|
|
22222
|
-
"delete"
|
|
22223
|
-
])
|
|
22224
|
-
if (isKeyword(token, command)) return command;
|
|
22225
|
-
throw new PolicySyntaxError(
|
|
22226
|
-
`expected ALL, SELECT, INSERT, UPDATE or DELETE, found ${describeToken(token)}`,
|
|
22227
|
-
token.position
|
|
22228
|
-
);
|
|
22975
|
+
function bind(env, value) {
|
|
22976
|
+
env.params.push(value);
|
|
22977
|
+
return "?";
|
|
22229
22978
|
}
|
|
22230
|
-
function
|
|
22231
|
-
|
|
22232
|
-
const expr = readCondition(stream);
|
|
22233
|
-
stream.expectOperator(")");
|
|
22234
|
-
checkExpressionSize(expr, open.position);
|
|
22235
|
-
return { text: printExpr(expr), position: open.position };
|
|
22979
|
+
function blamed(env, reason) {
|
|
22980
|
+
return fail(policyInvalid(env.blame.policy, env.blame.action), reason);
|
|
22236
22981
|
}
|
|
22237
|
-
function
|
|
22238
|
-
|
|
22239
|
-
|
|
22240
|
-
|
|
22241
|
-
|
|
22242
|
-
|
|
22243
|
-
|
|
22244
|
-
|
|
22245
|
-
|
|
22246
|
-
|
|
22247
|
-
|
|
22248
|
-
|
|
22249
|
-
|
|
22250
|
-
|
|
22982
|
+
function storedSource(table, names, alias) {
|
|
22983
|
+
return {
|
|
22984
|
+
table,
|
|
22985
|
+
names,
|
|
22986
|
+
read: (column2, _env, how) => alias !== null ? `${quoteIdentifier(alias)}.${quoteIdentifier(column2.name)}` : how.qualified ? `${quoteIdentifier(table.name)}.${quoteIdentifier(column2.name)}` : quoteIdentifier(column2.name),
|
|
22987
|
+
operand: (column2) => ({
|
|
22988
|
+
stable: true,
|
|
22989
|
+
newColumnish: true,
|
|
22990
|
+
newCollation: alias === null ? column2.collation : null
|
|
22991
|
+
})
|
|
22992
|
+
};
|
|
22993
|
+
}
|
|
22994
|
+
function missingColumnReason(name, tables) {
|
|
22995
|
+
return `column ${name} does not exist on ${tables.join(" or ")}`;
|
|
22996
|
+
}
|
|
22997
|
+
function missingRelationReason(table) {
|
|
22998
|
+
return `relation ${table} does not exist (a system table cannot be read either)`;
|
|
22999
|
+
}
|
|
23000
|
+
function resolveColumn(env, ref) {
|
|
23001
|
+
const name = asciiLower(ref.name);
|
|
23002
|
+
if (ref.qualifier !== null) {
|
|
23003
|
+
const qualifier = asciiLower(ref.qualifier);
|
|
23004
|
+
for (let index = env.scopes.length - 1; index >= 0; index--) {
|
|
23005
|
+
const source = env.scopes[index];
|
|
23006
|
+
if (!source?.names.includes(qualifier)) continue;
|
|
23007
|
+
const column2 = source.table.byName.get(name);
|
|
23008
|
+
if (!column2)
|
|
23009
|
+
return blamed(
|
|
23010
|
+
env,
|
|
23011
|
+
`column ${ref.qualifier}.${ref.name} does not exist: ${source.table.name} has no column ${ref.name}`
|
|
23012
|
+
);
|
|
23013
|
+
return { source, column: column2 };
|
|
23014
|
+
}
|
|
23015
|
+
return blamed(
|
|
23016
|
+
env,
|
|
23017
|
+
`${ref.qualifier}.${ref.name} names no table here: use the policy's table or a subquery's table or alias`
|
|
23018
|
+
);
|
|
23019
|
+
}
|
|
23020
|
+
for (let index = env.scopes.length - 1; index >= 0; index--) {
|
|
23021
|
+
const source = env.scopes[index];
|
|
23022
|
+
const column2 = source?.table.byName.get(name);
|
|
23023
|
+
if (!source || !column2) continue;
|
|
23024
|
+
if (env.scopes.length > 1) return blamed(env, unqualifiedProblem(ref));
|
|
23025
|
+
return { source, column: column2 };
|
|
23026
|
+
}
|
|
23027
|
+
return blamed(
|
|
23028
|
+
env,
|
|
23029
|
+
missingColumnReason(
|
|
23030
|
+
ref.name,
|
|
23031
|
+
env.scopes.map((source) => source.table.name)
|
|
23032
|
+
)
|
|
23033
|
+
);
|
|
23034
|
+
}
|
|
23035
|
+
function jwtValue(env, expr) {
|
|
23036
|
+
const value = env.req.claims[expr.claim];
|
|
23037
|
+
if (value === null) return "NULL";
|
|
23038
|
+
if (expr.op === "->") return bind(env, JSON.stringify(value));
|
|
23039
|
+
return bind(env, typeof value === "boolean" ? String(value) : value);
|
|
23040
|
+
}
|
|
23041
|
+
function valueSql(env, expr, textual = null) {
|
|
23042
|
+
switch (expr.kind) {
|
|
23043
|
+
case "literal":
|
|
23044
|
+
if (expr.value === null) return "NULL";
|
|
23045
|
+
if (typeof expr.value === "boolean") return expr.value ? "1" : "0";
|
|
23046
|
+
return bind(env, expr.value);
|
|
23047
|
+
case "column": {
|
|
23048
|
+
const { source, column: column2 } = resolveColumn(env, expr);
|
|
23049
|
+
return source.read(column2, env, {
|
|
23050
|
+
qualified: env.scopes.length > 1,
|
|
23051
|
+
textual
|
|
23052
|
+
});
|
|
23053
|
+
}
|
|
23054
|
+
case "auth":
|
|
23055
|
+
if (expr.fn === "role") return bind(env, env.req.role);
|
|
23056
|
+
return env.req.uid === null ? "NULL" : bind(env, env.req.uid);
|
|
23057
|
+
case "jwt":
|
|
23058
|
+
return jwtValue(env, expr);
|
|
23059
|
+
case "func": {
|
|
23060
|
+
if (expr.name === "now") return "CURRENT_TIMESTAMP";
|
|
23061
|
+
if (expr.name === "coalesce") {
|
|
23062
|
+
const args = expr.args.map((arg2) => valueSql(env, arg2, textual));
|
|
23063
|
+
return args.length === 1 ? `(${String(args[0])})` : `coalesce(${args.join(", ")})`;
|
|
23064
|
+
}
|
|
23065
|
+
const [arg] = expr.args;
|
|
23066
|
+
if (!arg) return blamed(env, `${expr.name}() needs an argument`);
|
|
23067
|
+
return `${expr.name}(${valueSql(env, arg, "textual")})`;
|
|
23068
|
+
}
|
|
23069
|
+
case "cast":
|
|
23070
|
+
return `CAST(${valueSql(env, expr.operand, "textual")} AS TEXT)`;
|
|
23071
|
+
}
|
|
23072
|
+
}
|
|
23073
|
+
function operandSide(env, expr) {
|
|
23074
|
+
if (expr.kind === "cast") return operandSide(env, expr.operand);
|
|
23075
|
+
if (expr.kind !== "column")
|
|
23076
|
+
return {
|
|
23077
|
+
stored: null,
|
|
23078
|
+
stable: true,
|
|
23079
|
+
newColumnish: false,
|
|
23080
|
+
newCollation: null
|
|
23081
|
+
};
|
|
23082
|
+
const { source, column: column2 } = resolveColumn(env, expr);
|
|
23083
|
+
const operand = source.operand(column2);
|
|
23084
|
+
return {
|
|
23085
|
+
stored: column2,
|
|
23086
|
+
stable: operand.stable,
|
|
23087
|
+
newColumnish: operand.newColumnish,
|
|
23088
|
+
newCollation: operand.newCollation
|
|
23089
|
+
};
|
|
23090
|
+
}
|
|
23091
|
+
function checkCollation(env, left, right) {
|
|
23092
|
+
if (!env.check) return "";
|
|
23093
|
+
const sides = right === null ? [left] : [left, right];
|
|
23094
|
+
const stored = sides.find((side) => side.stored !== null);
|
|
23095
|
+
const fresh = sides.find((side) => side.newColumnish);
|
|
23096
|
+
if (stored && stored === fresh && stored.stable) return "";
|
|
23097
|
+
if (!stored && !fresh) return "";
|
|
23098
|
+
const want = stored?.stored ? stored.stored.collation ?? env.check.problem(stored.stored.name, "collation", env.blame.policy) : "BINARY";
|
|
23099
|
+
const have = fresh ? fresh.newCollation : "BINARY";
|
|
23100
|
+
return have === want ? "" : ` COLLATE ${want}`;
|
|
23101
|
+
}
|
|
23102
|
+
var ARRAY_OPERATORS3 = {
|
|
23103
|
+
"@>": "contains",
|
|
23104
|
+
"<@": "containedBy",
|
|
23105
|
+
"&&": "overlaps"
|
|
23106
|
+
};
|
|
23107
|
+
function comparison(env, left, op, right) {
|
|
23108
|
+
const l = valueSql(env, left);
|
|
23109
|
+
const collate = env.check ? checkCollation(env, operandSide(env, left), operandSide(env, right)) : "";
|
|
23110
|
+
const r = valueSql(env, right);
|
|
23111
|
+
return `(${l}${collate} ${op} ${r})`;
|
|
23112
|
+
}
|
|
23113
|
+
function knownNullTest(env, expr) {
|
|
23114
|
+
if (expr.value !== null) return void 0;
|
|
23115
|
+
const { operand } = expr;
|
|
23116
|
+
let value;
|
|
23117
|
+
if (operand.kind === "auth" && operand.fn === "uid") value = env.req.uid;
|
|
23118
|
+
else if (operand.kind === "jwt") {
|
|
23119
|
+
const claim = env.req.claims[operand.claim];
|
|
23120
|
+
value = claim === null ? null : String(claim);
|
|
23121
|
+
} else return void 0;
|
|
23122
|
+
return value === null !== expr.not ? "1" : "0";
|
|
23123
|
+
}
|
|
23124
|
+
function conditionSql(env, expr) {
|
|
23125
|
+
if (isValueExpr(expr)) {
|
|
23126
|
+
if (expr.kind === "literal")
|
|
23127
|
+
return expr.value === null ? "NULL" : expr.value === true ? "1" : "0";
|
|
23128
|
+
if (expr.kind === "column") return `(${valueSql(env, expr)})`;
|
|
23129
|
+
return blamed(env, "a value alone is not a condition");
|
|
23130
|
+
}
|
|
23131
|
+
switch (expr.kind) {
|
|
23132
|
+
case "and":
|
|
23133
|
+
case "or":
|
|
23134
|
+
return `(${expr.args.map((arg) => conditionSql(env, arg)).join(expr.kind === "and" ? " AND " : " OR ")})`;
|
|
23135
|
+
case "not":
|
|
23136
|
+
return `(NOT ${conditionSql(env, expr.arg)})`;
|
|
23137
|
+
default:
|
|
23138
|
+
break;
|
|
23139
|
+
}
|
|
23140
|
+
const mismatched = booleanComparisonProblem(expr);
|
|
23141
|
+
if (mismatched !== void 0) return blamed(env, mismatched);
|
|
23142
|
+
switch (expr.kind) {
|
|
23143
|
+
case "compare":
|
|
23144
|
+
return comparison(env, expr.left, expr.op, expr.right);
|
|
23145
|
+
case "distinct":
|
|
23146
|
+
return comparison(env, expr.left, expr.not ? "IS" : "IS NOT", expr.right);
|
|
23147
|
+
case "is": {
|
|
23148
|
+
const untestable = expr.value === null ? void 0 : truthTestProblem(expr.operand);
|
|
23149
|
+
if (untestable !== void 0) return blamed(env, untestable);
|
|
23150
|
+
const known = knownNullTest(env, expr);
|
|
23151
|
+
if (known !== void 0) return known;
|
|
23152
|
+
const target = valueSql(env, expr.operand);
|
|
23153
|
+
if (expr.value === null)
|
|
23154
|
+
return `(${target} IS ${expr.not ? "NOT " : ""}NULL)`;
|
|
23155
|
+
const test = isBoolean(target, expr.value);
|
|
23156
|
+
return expr.not ? `(NOT (${test}))` : `(${test})`;
|
|
23157
|
+
}
|
|
23158
|
+
case "in": {
|
|
23159
|
+
const left = valueSql(env, expr.operand);
|
|
23160
|
+
const collate = env.check ? checkCollation(env, operandSide(env, expr.operand), null) : "";
|
|
23161
|
+
const list = expr.list.map((item) => valueSql(env, item));
|
|
23162
|
+
return `(${left}${collate} ${expr.not ? "NOT " : ""}IN (${list.join(", ")}))`;
|
|
23163
|
+
}
|
|
23164
|
+
case "inSelect": {
|
|
23165
|
+
const left = valueSql(env, expr.operand);
|
|
23166
|
+
const leftSide = env.check ? operandSide(env, expr.operand) : null;
|
|
23167
|
+
const sub = subquerySql(env, expr.subquery, "value");
|
|
23168
|
+
const collate = leftSide && sub.item ? checkCollation(env, leftSide, sub.item) : "";
|
|
23169
|
+
return `(${left}${collate} ${expr.not ? "NOT " : ""}IN (${sub.sql}))`;
|
|
23170
|
+
}
|
|
23171
|
+
case "like": {
|
|
23172
|
+
const problem = policyPatternProblem(expr.pattern, expr.ci);
|
|
23173
|
+
if (problem !== void 0) return blamed(env, problem);
|
|
23174
|
+
const target = valueSql(env, expr.operand, "pattern");
|
|
23175
|
+
const not = expr.not ? "NOT " : "";
|
|
23176
|
+
const pattern = bind(env, boundPattern(expr.pattern, expr.ci));
|
|
23177
|
+
return expr.ci ? `(${target} ${not}LIKE ${pattern} ESCAPE '\\')` : `(${target} ${not}GLOB ${pattern})`;
|
|
23178
|
+
}
|
|
23179
|
+
case "between": {
|
|
23180
|
+
if (env.check) {
|
|
23181
|
+
const low2 = comparison(env, expr.operand, ">=", expr.low);
|
|
23182
|
+
const high2 = comparison(env, expr.operand, "<=", expr.high);
|
|
23183
|
+
const both = `(${low2} AND ${high2})`;
|
|
23184
|
+
return expr.not ? `(NOT ${both})` : both;
|
|
23185
|
+
}
|
|
23186
|
+
const target = valueSql(env, expr.operand);
|
|
23187
|
+
const low = valueSql(env, expr.low);
|
|
23188
|
+
const high = valueSql(env, expr.high);
|
|
23189
|
+
return `(${target} ${expr.not ? "NOT " : ""}BETWEEN ${low} AND ${high})`;
|
|
23190
|
+
}
|
|
23191
|
+
case "array": {
|
|
23192
|
+
const values = distinctArrayValues(
|
|
23193
|
+
expr.values.flatMap(
|
|
23194
|
+
(value) => typeof value !== "object" ? [value] : env.req.uid === null ? [] : [env.req.uid]
|
|
23195
|
+
)
|
|
23196
|
+
);
|
|
23197
|
+
const nullElement = expr.values.some(
|
|
23198
|
+
(value) => typeof value === "object" && env.req.uid === null
|
|
23199
|
+
);
|
|
23200
|
+
let next = 0;
|
|
23201
|
+
return arraySql(
|
|
23202
|
+
() => (() => {
|
|
23203
|
+
const { source, column: column2 } = resolveColumn(env, expr.operand);
|
|
23204
|
+
return source.read(column2, env, { qualified: true, textual: null });
|
|
23205
|
+
})(),
|
|
23206
|
+
ARRAY_OPERATORS3[expr.op],
|
|
23207
|
+
values.length,
|
|
23208
|
+
() => bind(env, values[next++] ?? null),
|
|
23209
|
+
nullElement
|
|
23210
|
+
);
|
|
23211
|
+
}
|
|
23212
|
+
case "exists":
|
|
23213
|
+
return `(EXISTS (${subquerySql(env, expr.subquery, "one").sql}))`;
|
|
23214
|
+
}
|
|
23215
|
+
}
|
|
23216
|
+
function subquerySql(env, subquery, select) {
|
|
23217
|
+
const table = resolveTable(env.snapshot, subquery.table);
|
|
23218
|
+
if (!table) return blamed(env, missingRelationReason(subquery.table));
|
|
23219
|
+
const { state } = env;
|
|
23220
|
+
if (++state.subqueries > MAX_STATEMENT_SUBQUERIES)
|
|
23221
|
+
return blamed(
|
|
23222
|
+
env,
|
|
23223
|
+
`the policies read more than ${String(MAX_STATEMENT_SUBQUERIES)} subqueries in one statement, counting the policies of the tables they read`
|
|
23224
|
+
);
|
|
23225
|
+
if (state.depth + 1 > MAX_INLINE_DEPTH)
|
|
23226
|
+
return blamed(
|
|
23227
|
+
env,
|
|
23228
|
+
`subqueries nest more than ${String(MAX_INLINE_DEPTH)} deep, counting the policies of the tables they read`
|
|
23229
|
+
);
|
|
23230
|
+
state.depth++;
|
|
23231
|
+
try {
|
|
23232
|
+
const alias = `_naro_s${String(++state.aliases)}`;
|
|
23233
|
+
const source = storedSource(
|
|
23234
|
+
table,
|
|
23235
|
+
[asciiLower(subquery.alias ?? table.name)],
|
|
23236
|
+
alias
|
|
23237
|
+
);
|
|
23238
|
+
const inner = { ...env, scopes: [...env.scopes, source] };
|
|
23239
|
+
let item = null;
|
|
23240
|
+
let selected = "1";
|
|
23241
|
+
if (select === "value") {
|
|
23242
|
+
if (subquery.select === "*")
|
|
23243
|
+
return blamed(env, "IN (SELECT \u2026) selects one value, not *");
|
|
23244
|
+
selected = valueSql(inner, subquery.select);
|
|
23245
|
+
if (env.check) item = operandSide(inner, subquery.select);
|
|
23246
|
+
}
|
|
23247
|
+
const own2 = storedSource(table, [asciiLower(table.name)], alias);
|
|
23248
|
+
const rls = inlinedPolicies(env, table, own2);
|
|
23249
|
+
const where = subquery.where === null ? "" : ` AND ${conditionSql(inner, subquery.where)}`;
|
|
23250
|
+
return {
|
|
23251
|
+
sql: `SELECT ${selected} FROM ${quoteIdentifier(table.name)} AS ${quoteIdentifier(alias)} WHERE ${rls}${where}`,
|
|
23252
|
+
item
|
|
23253
|
+
};
|
|
23254
|
+
} finally {
|
|
23255
|
+
state.depth--;
|
|
23256
|
+
}
|
|
23257
|
+
}
|
|
23258
|
+
function hasSubqueries(policies) {
|
|
23259
|
+
return policies.some(
|
|
23260
|
+
(policy) => [policy.usingExpr, policy.checkExpr].some(
|
|
23261
|
+
(expr) => expr !== null && subqueriesOf(expr).length > 0
|
|
23262
|
+
)
|
|
23263
|
+
);
|
|
23264
|
+
}
|
|
23265
|
+
function withTableActive(state, table, policies, compile2) {
|
|
23266
|
+
if (!hasSubqueries(policies)) return compile2();
|
|
23267
|
+
const key = asciiLower(table);
|
|
23268
|
+
if (state.active.includes(key))
|
|
23269
|
+
fail(
|
|
23270
|
+
dataError("POLICY_INVALID", { action: "select", table, recursion: true }),
|
|
23271
|
+
`a policy's subquery reads ${table}, whose own policies are still being expanded`
|
|
23272
|
+
);
|
|
23273
|
+
state.active.push(key);
|
|
23274
|
+
try {
|
|
23275
|
+
return compile2();
|
|
23276
|
+
} finally {
|
|
23277
|
+
state.active.pop();
|
|
23278
|
+
}
|
|
23279
|
+
}
|
|
23280
|
+
function groupTerms(env, sources, action, permissive, restrictive, pick2) {
|
|
23281
|
+
const compileOne = (policy, expr) => {
|
|
23282
|
+
const params = [];
|
|
23283
|
+
const sql = conditionSql(
|
|
23284
|
+
{ ...env, scopes: sources, params, blame: { policy, action } },
|
|
23285
|
+
expr
|
|
23286
|
+
);
|
|
23287
|
+
const [source] = sources;
|
|
23288
|
+
const problem = source === void 0 ? void 0 : textTruthProblem(env.snapshot, source.table, expr);
|
|
23289
|
+
if (problem !== void 0) fail(policyInvalid(policy, action), problem);
|
|
23290
|
+
return { sql, params };
|
|
23291
|
+
};
|
|
23292
|
+
const terms = [];
|
|
23293
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23294
|
+
for (const policy of permissive) {
|
|
23295
|
+
const expr = pick2(policy);
|
|
23296
|
+
if (expr === null) continue;
|
|
23297
|
+
const term = compileOne(policy, expr);
|
|
23298
|
+
const key = JSON.stringify([term.sql, term.params]);
|
|
23299
|
+
if (seen.has(key)) continue;
|
|
23300
|
+
seen.add(key);
|
|
23301
|
+
terms.push(term);
|
|
23302
|
+
}
|
|
23303
|
+
const group = terms.length === 0 ? { sql: "0", params: [] } : terms.length === 1 ? terms[0] : {
|
|
23304
|
+
sql: `(${terms.map((term) => term.sql).join(" OR ")})`,
|
|
23305
|
+
params: terms.flatMap((term) => term.params)
|
|
23306
|
+
};
|
|
23307
|
+
const restricted = restrictive.flatMap((policy) => {
|
|
23308
|
+
const expr = pick2(policy);
|
|
23309
|
+
return expr === null ? [] : [{ policy, term: compileOne(policy, expr) }];
|
|
23310
|
+
});
|
|
23311
|
+
return { permissive: group, restrictive: restricted };
|
|
23312
|
+
}
|
|
23313
|
+
function inlinedPolicies(env, table, source) {
|
|
23314
|
+
const found = applicablePolicies(
|
|
23315
|
+
env.req.policies,
|
|
23316
|
+
table.name,
|
|
23317
|
+
"select",
|
|
23318
|
+
env.req.role
|
|
23319
|
+
);
|
|
23320
|
+
if (found.invalid)
|
|
23321
|
+
fail(
|
|
23322
|
+
policyInvalid(found.invalid, "select"),
|
|
23323
|
+
found.invalid.invalid ?? void 0
|
|
23324
|
+
);
|
|
23325
|
+
if (found.permissive.length === 0) return "0";
|
|
23326
|
+
return withTableActive(
|
|
23327
|
+
env.state,
|
|
23328
|
+
table.name,
|
|
23329
|
+
[...found.permissive, ...found.restrictive],
|
|
23330
|
+
() => {
|
|
23331
|
+
const inner = { ...env, check: null, params: [] };
|
|
23332
|
+
const terms = groupTerms(
|
|
23333
|
+
inner,
|
|
23334
|
+
[source],
|
|
23335
|
+
"select",
|
|
23336
|
+
found.permissive,
|
|
23337
|
+
found.restrictive,
|
|
23338
|
+
(policy) => policy.usingExpr
|
|
23339
|
+
);
|
|
23340
|
+
const parts = uniqueTerms([
|
|
23341
|
+
terms.permissive,
|
|
23342
|
+
...terms.restrictive.map((entry) => entry.term)
|
|
23343
|
+
]);
|
|
23344
|
+
for (const part of parts) env.params.push(...part.params);
|
|
23345
|
+
const [only] = parts;
|
|
23346
|
+
if (parts.length === 1 && only) return only.sql;
|
|
23347
|
+
return `(${parts.map((part) => part.sql).join(" AND ")})`;
|
|
23348
|
+
}
|
|
23349
|
+
);
|
|
23350
|
+
}
|
|
23351
|
+
function uniqueTerms(terms) {
|
|
23352
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23353
|
+
return terms.filter((term) => {
|
|
23354
|
+
const key = JSON.stringify([term.sql, term.params]);
|
|
23355
|
+
if (seen.has(key)) return false;
|
|
23356
|
+
seen.add(key);
|
|
23357
|
+
return true;
|
|
23358
|
+
});
|
|
23359
|
+
}
|
|
23360
|
+
function outerColumns(table, expr) {
|
|
23361
|
+
if (subqueriesOf(expr).length > 0) return void 0;
|
|
23362
|
+
const found = [];
|
|
23363
|
+
let missing = false;
|
|
23364
|
+
const visitValue = (value) => {
|
|
23365
|
+
switch (value.kind) {
|
|
23366
|
+
case "column": {
|
|
23367
|
+
const column2 = table.byName.get(asciiLower(value.name));
|
|
23368
|
+
if (!column2) missing = true;
|
|
23369
|
+
else found.push(column2);
|
|
23370
|
+
return;
|
|
23371
|
+
}
|
|
23372
|
+
case "func":
|
|
23373
|
+
value.args.forEach(visitValue);
|
|
23374
|
+
return;
|
|
23375
|
+
case "cast":
|
|
23376
|
+
visitValue(value.operand);
|
|
23377
|
+
return;
|
|
23378
|
+
default:
|
|
23379
|
+
return;
|
|
23380
|
+
}
|
|
23381
|
+
};
|
|
23382
|
+
const visit = (node) => {
|
|
23383
|
+
if (isValueExpr(node)) {
|
|
23384
|
+
visitValue(node);
|
|
23385
|
+
return;
|
|
23386
|
+
}
|
|
23387
|
+
switch (node.kind) {
|
|
23388
|
+
case "and":
|
|
23389
|
+
case "or":
|
|
23390
|
+
node.args.forEach(visit);
|
|
23391
|
+
return;
|
|
23392
|
+
case "not":
|
|
23393
|
+
visit(node.arg);
|
|
23394
|
+
return;
|
|
23395
|
+
case "compare":
|
|
23396
|
+
case "distinct":
|
|
23397
|
+
visitValue(node.left);
|
|
23398
|
+
visitValue(node.right);
|
|
23399
|
+
return;
|
|
23400
|
+
case "is":
|
|
23401
|
+
case "like":
|
|
23402
|
+
visitValue(node.operand);
|
|
23403
|
+
return;
|
|
23404
|
+
case "in":
|
|
23405
|
+
visitValue(node.operand);
|
|
23406
|
+
node.list.forEach(visitValue);
|
|
23407
|
+
return;
|
|
23408
|
+
case "between":
|
|
23409
|
+
[node.operand, node.low, node.high].forEach(visitValue);
|
|
23410
|
+
return;
|
|
23411
|
+
case "array":
|
|
23412
|
+
visitValue(node.operand);
|
|
23413
|
+
return;
|
|
23414
|
+
default:
|
|
23415
|
+
missing = true;
|
|
23416
|
+
}
|
|
23417
|
+
};
|
|
23418
|
+
visit(expr);
|
|
23419
|
+
return missing ? void 0 : found;
|
|
23420
|
+
}
|
|
23421
|
+
function resolveRef2(ref, scopes) {
|
|
23422
|
+
const name = asciiLower(ref.name);
|
|
23423
|
+
const qualifier = ref.qualifier === null ? null : asciiLower(ref.qualifier);
|
|
23424
|
+
for (const scope of scopes.toReversed()) {
|
|
23425
|
+
if (qualifier !== null) {
|
|
23426
|
+
if (!scope.names.includes(qualifier)) continue;
|
|
23427
|
+
const column3 = scope.table.byName.get(name);
|
|
23428
|
+
return column3 ? { table: scope.table, column: column3 } : void 0;
|
|
23429
|
+
}
|
|
23430
|
+
const column2 = scope.table.byName.get(name);
|
|
23431
|
+
if (column2) return { table: scope.table, column: column2 };
|
|
23432
|
+
}
|
|
23433
|
+
return void 0;
|
|
23434
|
+
}
|
|
23435
|
+
function scopedValues(operand, scopes) {
|
|
23436
|
+
return operandValues(operand).map((value) => ({ value, scopes }));
|
|
23437
|
+
}
|
|
23438
|
+
function subqueryScopes(snapshot, subquery, scopes) {
|
|
23439
|
+
const inner = resolveTable(snapshot, subquery.table);
|
|
23440
|
+
return inner ? [
|
|
23441
|
+
...scopes,
|
|
23442
|
+
{ names: [asciiLower(subquery.alias ?? inner.name)], table: inner }
|
|
23443
|
+
] : void 0;
|
|
23444
|
+
}
|
|
23445
|
+
function columnsReadOn(snapshot, table, expr, written) {
|
|
23446
|
+
const found = [];
|
|
23447
|
+
const visitValue = (value, scopes) => {
|
|
23448
|
+
switch (value.kind) {
|
|
23449
|
+
case "column": {
|
|
23450
|
+
const resolved = resolveRef2(value, scopes);
|
|
23451
|
+
if (resolved?.table === written) found.push(resolved.column);
|
|
23452
|
+
return;
|
|
23453
|
+
}
|
|
23454
|
+
case "func":
|
|
23455
|
+
for (const arg of value.args) visitValue(arg, scopes);
|
|
23456
|
+
return;
|
|
23457
|
+
case "cast":
|
|
23458
|
+
visitValue(value.operand, scopes);
|
|
23459
|
+
return;
|
|
23460
|
+
default:
|
|
23461
|
+
return;
|
|
23462
|
+
}
|
|
23463
|
+
};
|
|
23464
|
+
const visitSubquery = (subquery, scopes) => {
|
|
23465
|
+
const within = subqueryScopes(snapshot, subquery, scopes);
|
|
23466
|
+
if (!within) return;
|
|
23467
|
+
if (subquery.select !== "*") visitValue(subquery.select, within);
|
|
23468
|
+
if (subquery.where) visit(subquery.where, within);
|
|
23469
|
+
};
|
|
23470
|
+
const visit = (node, scopes) => {
|
|
23471
|
+
if (isValueExpr(node)) {
|
|
23472
|
+
visitValue(node, scopes);
|
|
23473
|
+
return;
|
|
23474
|
+
}
|
|
23475
|
+
switch (node.kind) {
|
|
23476
|
+
case "and":
|
|
23477
|
+
case "or":
|
|
23478
|
+
for (const arg of node.args) visit(arg, scopes);
|
|
23479
|
+
return;
|
|
23480
|
+
case "not":
|
|
23481
|
+
visit(node.arg, scopes);
|
|
23482
|
+
return;
|
|
23483
|
+
case "compare":
|
|
23484
|
+
case "distinct":
|
|
23485
|
+
visitValue(node.left, scopes);
|
|
23486
|
+
visitValue(node.right, scopes);
|
|
23487
|
+
return;
|
|
23488
|
+
case "is":
|
|
23489
|
+
case "like":
|
|
23490
|
+
case "array":
|
|
23491
|
+
visitValue(node.operand, scopes);
|
|
23492
|
+
return;
|
|
23493
|
+
case "in":
|
|
23494
|
+
visitValue(node.operand, scopes);
|
|
23495
|
+
for (const item of node.list) visitValue(item, scopes);
|
|
23496
|
+
return;
|
|
23497
|
+
case "between":
|
|
23498
|
+
for (const item of [node.operand, node.low, node.high])
|
|
23499
|
+
visitValue(item, scopes);
|
|
23500
|
+
return;
|
|
23501
|
+
case "exists":
|
|
23502
|
+
visitSubquery(node.subquery, scopes);
|
|
23503
|
+
return;
|
|
23504
|
+
case "inSelect":
|
|
23505
|
+
visitValue(node.operand, scopes);
|
|
23506
|
+
visitSubquery(node.subquery, scopes);
|
|
23507
|
+
return;
|
|
23508
|
+
}
|
|
23509
|
+
};
|
|
23510
|
+
visit(expr, [{ names: [asciiLower(table.name)], table }]);
|
|
23511
|
+
return found;
|
|
23512
|
+
}
|
|
23513
|
+
function truthColumns(snapshot, table, expr) {
|
|
23514
|
+
return comparedColumns(snapshot, table, expr, isBooleanConstant, true);
|
|
23515
|
+
}
|
|
23516
|
+
function numberComparedColumns(snapshot, table, expr) {
|
|
23517
|
+
return comparedColumns(
|
|
23518
|
+
snapshot,
|
|
23519
|
+
table,
|
|
23520
|
+
expr,
|
|
23521
|
+
(value) => value.kind === "literal" && typeof value.value === "number",
|
|
23522
|
+
false
|
|
23523
|
+
);
|
|
23524
|
+
}
|
|
23525
|
+
function comparedColumns(snapshot, table, expr, constant, truth) {
|
|
23526
|
+
const found = [];
|
|
23527
|
+
const read = (ref, scopes) => {
|
|
23528
|
+
const resolved = resolveRef2(ref, scopes);
|
|
23529
|
+
if (resolved) found.push({ written: printExpr(ref), ...resolved });
|
|
23530
|
+
};
|
|
23531
|
+
const compared = (node, scopes) => {
|
|
23532
|
+
for (const { left, right, inner } of comparedPairs(node)) {
|
|
23533
|
+
const within = inner && node.kind === "inSelect" ? subqueryScopes(snapshot, node.subquery, scopes) : scopes;
|
|
23534
|
+
const met = [
|
|
23535
|
+
...scopedValues(left, scopes),
|
|
23536
|
+
...within ? scopedValues(right, within) : []
|
|
23537
|
+
];
|
|
23538
|
+
if (!met.some(({ value }) => constant(value))) continue;
|
|
23539
|
+
for (const { value, scopes: where } of met)
|
|
23540
|
+
if (value.kind === "column") read(value, where);
|
|
23541
|
+
}
|
|
23542
|
+
};
|
|
23543
|
+
const visit = (node, scopes) => {
|
|
23544
|
+
if (isValueExpr(node)) {
|
|
23545
|
+
if (truth && node.kind === "column") read(node, scopes);
|
|
23546
|
+
return;
|
|
23547
|
+
}
|
|
23548
|
+
compared(node, scopes);
|
|
23549
|
+
switch (node.kind) {
|
|
23550
|
+
case "and":
|
|
23551
|
+
case "or":
|
|
23552
|
+
for (const arg of node.args) visit(arg, scopes);
|
|
23553
|
+
return;
|
|
23554
|
+
case "not":
|
|
23555
|
+
visit(node.arg, scopes);
|
|
23556
|
+
return;
|
|
23557
|
+
case "is":
|
|
23558
|
+
if (truth && node.value !== null && node.operand.kind === "column")
|
|
23559
|
+
read(node.operand, scopes);
|
|
23560
|
+
return;
|
|
23561
|
+
case "exists":
|
|
23562
|
+
case "inSelect": {
|
|
23563
|
+
const within = subqueryScopes(snapshot, node.subquery, scopes);
|
|
23564
|
+
if (within && node.subquery.where) visit(node.subquery.where, within);
|
|
23565
|
+
return;
|
|
23566
|
+
}
|
|
23567
|
+
default:
|
|
23568
|
+
return;
|
|
23569
|
+
}
|
|
23570
|
+
};
|
|
23571
|
+
visit(expr, [{ names: [asciiLower(table.name)], table }]);
|
|
23572
|
+
return found;
|
|
23573
|
+
}
|
|
23574
|
+
function textTruthProblem(snapshot, table, expr) {
|
|
23575
|
+
for (const { written, column: column2 } of truthColumns(snapshot, table, expr))
|
|
23576
|
+
if (!isNumeric(column2))
|
|
23577
|
+
return `${written} is a ${columnAffinity(column2)} column, whose truth value is not what its text says ('true' reads as false): compare it, ${written} = 'true'`;
|
|
23578
|
+
return void 0;
|
|
23579
|
+
}
|
|
23580
|
+
function selectCheckRefusal(path, column2, problem) {
|
|
23581
|
+
const reason = {
|
|
23582
|
+
generated: `is not available: the select policy reads the generated column ${column2}, which a new row cannot be checked against`,
|
|
23583
|
+
collation: `is not available: the select policy reads ${column2}, whose collation naro cannot read, so a new row cannot be checked against it`,
|
|
23584
|
+
pattern: `is not available: the select policy compares the number column ${column2} with like or ilike, which a new row cannot be checked against`,
|
|
23585
|
+
textual: `is not available: the select policy turns the number column ${column2} into text, which a new row cannot be checked against`
|
|
23586
|
+
}[problem];
|
|
23587
|
+
return invalidQuery(path, reason);
|
|
23588
|
+
}
|
|
23589
|
+
|
|
23590
|
+
// ../../packages/data-core/src/policy.ts
|
|
23591
|
+
function commandsOf(query) {
|
|
23592
|
+
switch (query.action) {
|
|
23593
|
+
case "select":
|
|
23594
|
+
return ["select"];
|
|
23595
|
+
case "insert":
|
|
23596
|
+
return query.returning === void 0 ? ["insert"] : ["insert", "select"];
|
|
23597
|
+
default:
|
|
23598
|
+
return [query.action, "select"];
|
|
23599
|
+
}
|
|
23600
|
+
}
|
|
23601
|
+
var ANON_CLAIMS = {
|
|
23602
|
+
sub: null,
|
|
23603
|
+
role: "anon",
|
|
23604
|
+
email: null,
|
|
23605
|
+
email_verified: null
|
|
23606
|
+
};
|
|
23607
|
+
function rlsIdentity(context) {
|
|
23608
|
+
if (context.role !== "authenticated" || context.userId === "")
|
|
23609
|
+
return { role: "anon", uid: null, claims: ANON_CLAIMS };
|
|
23610
|
+
const verified = context.emailVerified === true;
|
|
23611
|
+
return {
|
|
23612
|
+
role: "authenticated",
|
|
23613
|
+
uid: context.userId,
|
|
23614
|
+
claims: {
|
|
23615
|
+
sub: context.userId,
|
|
23616
|
+
role: "authenticated",
|
|
23617
|
+
email: verified ? context.email ?? null : null,
|
|
23618
|
+
email_verified: context.emailVerified ?? null
|
|
23619
|
+
}
|
|
23620
|
+
};
|
|
23621
|
+
}
|
|
23622
|
+
function decideQuery(context, query, policies, table) {
|
|
23623
|
+
if (context.role === "service") return { allow: true };
|
|
23624
|
+
const identity = rlsIdentity(context);
|
|
23625
|
+
const all = policies ?? [];
|
|
23626
|
+
for (const command of commandsOf(query)) {
|
|
23627
|
+
const found = applicablePolicies(all, table, command, identity.role);
|
|
23628
|
+
if (found.invalid)
|
|
23629
|
+
return {
|
|
23630
|
+
allow: false,
|
|
23631
|
+
error: policyInvalid(found.invalid, command),
|
|
23632
|
+
reason: found.invalid.invalid ?? "it cannot be read"
|
|
23633
|
+
};
|
|
23634
|
+
if (found.permissive.length === 0)
|
|
23635
|
+
return {
|
|
23636
|
+
allow: false,
|
|
23637
|
+
error: dataError("PERMISSION_DENIED", { action: command, table })
|
|
23638
|
+
};
|
|
23639
|
+
}
|
|
23640
|
+
return { allow: true, rls: { ...identity, policies: all } };
|
|
23641
|
+
}
|
|
23642
|
+
|
|
23643
|
+
// ../../packages/data-core/src/compiler.ts
|
|
23644
|
+
var MAX_PARAMETERS = 100;
|
|
23645
|
+
var MAX_STATEMENT_BYTES = 1e5;
|
|
23646
|
+
var MAX_INSERT_STATEMENTS = 100;
|
|
23647
|
+
var OR_ABORT = "OR ABORT";
|
|
23648
|
+
function column(table, name) {
|
|
23649
|
+
return table.byName.get(asciiLower(name)) ?? fail(dataError("COLUMN_NOT_FOUND", { column: name, table: table.name }));
|
|
23650
|
+
}
|
|
23651
|
+
function columnList(table, names) {
|
|
23652
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23653
|
+
const columns = [];
|
|
23654
|
+
for (const name of names) {
|
|
23655
|
+
const resolved = column(table, name);
|
|
23656
|
+
if (seen.has(resolved.name)) continue;
|
|
23657
|
+
seen.add(resolved.name);
|
|
23658
|
+
columns.push(resolved);
|
|
23659
|
+
}
|
|
23660
|
+
return columns;
|
|
23661
|
+
}
|
|
23662
|
+
function selectList(columns) {
|
|
23663
|
+
return columns.map((item) => quoteIdentifier(item.name)).join(", ");
|
|
23664
|
+
}
|
|
23665
|
+
function returningClause(table, returning, action) {
|
|
23666
|
+
if (returning === void 0) return "";
|
|
23667
|
+
if (returning !== "*" && returning.length === 0)
|
|
23668
|
+
fail(invalidQuery("returning", "must name at least one column"));
|
|
23669
|
+
if (table.kind === "virtual" && action !== "insert")
|
|
23670
|
+
fail(
|
|
23671
|
+
invalidQuery(
|
|
23672
|
+
"returning",
|
|
23673
|
+
`is not available for ${action} on the virtual table ${table.name}`
|
|
23674
|
+
)
|
|
23675
|
+
);
|
|
23676
|
+
const columns = returning === "*" ? table.columns : columnList(table, returning);
|
|
23677
|
+
return ` RETURNING ${selectList(columns)}`;
|
|
23678
|
+
}
|
|
23679
|
+
function scalar(value, path) {
|
|
23680
|
+
if (Array.isArray(value) || !isValue(value))
|
|
23681
|
+
fail(invalidQuery(path, "must be a single value"));
|
|
23682
|
+
return value;
|
|
23683
|
+
}
|
|
23684
|
+
function arrayValues(value, path) {
|
|
23685
|
+
const reason = `must be a non-empty array of strings and numbers (at most ${String(MAX_ARRAY_VALUES)})`;
|
|
23686
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_ARRAY_VALUES)
|
|
23687
|
+
fail(invalidQuery(path, reason));
|
|
23688
|
+
const items = [];
|
|
23689
|
+
for (const item of value) {
|
|
23690
|
+
if (typeof item !== "string" && !(typeof item === "number" && Number.isFinite(item)))
|
|
23691
|
+
fail(invalidQuery(path, reason));
|
|
23692
|
+
items.push(item);
|
|
23693
|
+
}
|
|
23694
|
+
return distinctArrayValues(items);
|
|
23695
|
+
}
|
|
23696
|
+
function filterSql(table, filter, params, path) {
|
|
23697
|
+
const resolved = column(table, filter.column);
|
|
23698
|
+
const target = quoteIdentifier(resolved.name);
|
|
23699
|
+
const valuePath = `${path}.value`;
|
|
23700
|
+
switch (filter.operator) {
|
|
23701
|
+
case "eq":
|
|
23702
|
+
case "neq":
|
|
23703
|
+
case "gt":
|
|
23704
|
+
case "gte":
|
|
23705
|
+
case "lt":
|
|
23706
|
+
case "lte": {
|
|
23707
|
+
const operator = {
|
|
23708
|
+
eq: "=",
|
|
23709
|
+
neq: "<>",
|
|
23710
|
+
gt: ">",
|
|
23711
|
+
gte: ">=",
|
|
23712
|
+
lt: "<",
|
|
23713
|
+
lte: "<="
|
|
23714
|
+
}[filter.operator];
|
|
23715
|
+
params.push(toBoundValue(scalar(filter.value, valuePath)));
|
|
23716
|
+
return `${target} ${operator} ?`;
|
|
23717
|
+
}
|
|
23718
|
+
case "in": {
|
|
23719
|
+
if (!Array.isArray(filter.value))
|
|
23720
|
+
fail(invalidQuery(valuePath, "must be an array"));
|
|
23721
|
+
if (filter.value.length === 0) return "0 = 1";
|
|
23722
|
+
for (const [index, item] of filter.value.entries())
|
|
23723
|
+
params.push(toBoundValue(scalar(item, `${valuePath}[${index}]`)));
|
|
23724
|
+
return `${target} IN (${filter.value.map(() => "?").join(", ")})`;
|
|
23725
|
+
}
|
|
23726
|
+
case "is": {
|
|
23727
|
+
const value = filter.value;
|
|
23728
|
+
if (value !== null && value !== true && value !== false)
|
|
23729
|
+
fail(invalidQuery(valuePath, "must be null, true or false"));
|
|
23730
|
+
if (value === null) {
|
|
23731
|
+
params.push(null);
|
|
23732
|
+
return `${target} IS ?`;
|
|
23733
|
+
}
|
|
23734
|
+
return isBoolean(target, value);
|
|
23735
|
+
}
|
|
23736
|
+
case "ilike":
|
|
23737
|
+
case "like": {
|
|
23738
|
+
if (typeof filter.value !== "string")
|
|
23739
|
+
fail(invalidQuery(valuePath, "must be a string"));
|
|
23740
|
+
const problem = patternProblem(filter.operator, filter.value);
|
|
23741
|
+
if (problem !== void 0) fail(invalidQuery(valuePath, problem));
|
|
23742
|
+
const ilike = filter.operator === "ilike";
|
|
23743
|
+
params.push(boundPattern(filter.value, ilike));
|
|
23744
|
+
return ilike ? `${target} LIKE ? ESCAPE '\\'` : `${target} GLOB ?`;
|
|
23745
|
+
}
|
|
23746
|
+
case "contains":
|
|
23747
|
+
case "containedBy":
|
|
23748
|
+
case "overlaps": {
|
|
23749
|
+
const values = arrayValues(filter.value, valuePath);
|
|
23750
|
+
const qualified = `${quoteIdentifier(table.name)}.${target}`;
|
|
23751
|
+
let next = 0;
|
|
23752
|
+
return arraySql(
|
|
23753
|
+
() => qualified,
|
|
23754
|
+
filter.operator,
|
|
23755
|
+
values.length,
|
|
23756
|
+
() => {
|
|
23757
|
+
params.push(values[next++] ?? null);
|
|
23758
|
+
return "?";
|
|
23759
|
+
}
|
|
23760
|
+
);
|
|
23761
|
+
}
|
|
23762
|
+
}
|
|
23763
|
+
}
|
|
23764
|
+
function nodeSql(table, node, params, path) {
|
|
23765
|
+
if ("operator" in node) return filterSql(table, node, params, path);
|
|
23766
|
+
if ("not" in node)
|
|
23767
|
+
return `NOT (${nodeSql(table, node.not, params, `${path}.not`)})`;
|
|
23768
|
+
const [key, children] = "or" in node ? ["or", node.or] : ["and", node.and];
|
|
23769
|
+
if (children.length === 0)
|
|
23770
|
+
fail(invalidQuery(`${path}.${key}`, "must not be empty"));
|
|
23771
|
+
const terms = children.map(
|
|
23772
|
+
(child, index) => nodeSql(table, child, params, `${path}.${key}[${index}]`)
|
|
23773
|
+
);
|
|
23774
|
+
return `(${terms.join(key === "or" ? " OR " : " AND ")})`;
|
|
23775
|
+
}
|
|
23776
|
+
function whereClause(table, filters, policy, params) {
|
|
23777
|
+
const parts = [];
|
|
23778
|
+
if (policy) {
|
|
23779
|
+
parts.push(policy.sql);
|
|
23780
|
+
params.push(...policy.params);
|
|
23781
|
+
}
|
|
23782
|
+
const client = filters.map(
|
|
23783
|
+
(node, index) => nodeSql(table, node, params, `filters[${index}]`)
|
|
23784
|
+
);
|
|
23785
|
+
if (client.length > 0) parts.push(`(${client.join(" AND ")})`);
|
|
23786
|
+
return parts.length === 0 ? "" : ` WHERE ${parts.join(" AND ")}`;
|
|
23787
|
+
}
|
|
23788
|
+
function orderClause(table, order) {
|
|
23789
|
+
if (order.length === 0) return "";
|
|
23790
|
+
const terms = order.map((term) => {
|
|
23791
|
+
const direction = term.ascending ? "ASC" : "DESC";
|
|
23792
|
+
const nulls = term.nullsFirst === void 0 ? "" : term.nullsFirst ? " NULLS FIRST" : " NULLS LAST";
|
|
23793
|
+
return `${quoteIdentifier(column(table, term.column).name)} ${direction}${nulls}`;
|
|
23794
|
+
});
|
|
23795
|
+
return ` ORDER BY ${terms.join(", ")}`;
|
|
23796
|
+
}
|
|
23797
|
+
function statement(sql, params) {
|
|
23798
|
+
if (params.length > MAX_PARAMETERS) fail(dataError("TOO_MANY_PARAMETERS"));
|
|
23799
|
+
if (utf8ByteLength(sql) > MAX_STATEMENT_BYTES)
|
|
23800
|
+
fail(
|
|
23801
|
+
invalidQuery(
|
|
23802
|
+
"",
|
|
23803
|
+
`compiles to more than ${String(MAX_STATEMENT_BYTES)} bytes of SQL, D1's limit for one statement`
|
|
23804
|
+
)
|
|
23805
|
+
);
|
|
23806
|
+
return { sql, params };
|
|
23807
|
+
}
|
|
23808
|
+
function assignments(table, row, path) {
|
|
23809
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
23810
|
+
for (const [key, value] of Object.entries(row)) {
|
|
23811
|
+
const target = column(table, key);
|
|
23812
|
+
if (target.generated)
|
|
23813
|
+
fail(
|
|
23814
|
+
invalidQuery(
|
|
23815
|
+
`${path}.${key}`,
|
|
23816
|
+
`Column ${target.name} is generated and cannot be written.`
|
|
23817
|
+
)
|
|
23818
|
+
);
|
|
23819
|
+
if (resolved.has(target.name))
|
|
23820
|
+
fail(
|
|
23821
|
+
invalidQuery(
|
|
23822
|
+
`${path}.${key}`,
|
|
23823
|
+
`Column ${target.name} is given more than once.`
|
|
23824
|
+
)
|
|
23825
|
+
);
|
|
23826
|
+
resolved.set(target.name, { column: target, value });
|
|
23827
|
+
}
|
|
23828
|
+
return resolved;
|
|
23829
|
+
}
|
|
23830
|
+
function hasCondition(nodes) {
|
|
23831
|
+
return (nodes ?? []).some((node) => {
|
|
23832
|
+
if ("operator" in node) return true;
|
|
23833
|
+
if ("not" in node) return hasCondition([node.not]);
|
|
23834
|
+
return hasCondition("or" in node ? node.or : node.and);
|
|
23835
|
+
});
|
|
23836
|
+
}
|
|
23837
|
+
function requireFilter(query, context) {
|
|
23838
|
+
if (hasCondition(query.filters)) return;
|
|
23839
|
+
if (query.allowUnfiltered === true && context.role === "service") return;
|
|
23840
|
+
fail(dataError("FILTER_REQUIRED"));
|
|
23841
|
+
}
|
|
23842
|
+
function mustBeNumber(action) {
|
|
23843
|
+
return `must be a number, a boolean or null, since the ${action} policy checks it`;
|
|
23844
|
+
}
|
|
23845
|
+
function mustBeNumberFor(type) {
|
|
23846
|
+
return `must be a number, a boolean or null, since the column is declared ${type}`;
|
|
23847
|
+
}
|
|
23848
|
+
function mustIncludeFor(name, command) {
|
|
23849
|
+
return `must include ${name}, which the ${command} policy checks`;
|
|
23850
|
+
}
|
|
23851
|
+
function mustInclude(name, action) {
|
|
23852
|
+
return action === "select" ? `${mustIncludeFor(name, action)} before a row is returned` : mustIncludeFor(name, action);
|
|
23853
|
+
}
|
|
23854
|
+
function mustNotBeNull(action) {
|
|
23855
|
+
return action === "select" ? "must not be null, since the select policy checks this key column before a row is returned" : `must not be null, since the ${action} policy checks this key column`;
|
|
23856
|
+
}
|
|
23857
|
+
function keepFirst(map2, item, policy) {
|
|
23858
|
+
if (isNumeric(item) && !map2.has(item.name)) map2.set(item.name, policy);
|
|
23859
|
+
}
|
|
23860
|
+
function insertSource(table, group) {
|
|
23861
|
+
const checked = /* @__PURE__ */ new Set();
|
|
23862
|
+
const source = {
|
|
23863
|
+
table,
|
|
23864
|
+
names: [asciiLower(table.name)],
|
|
23865
|
+
used: false,
|
|
23866
|
+
read: (item, env, how) => {
|
|
23867
|
+
const check2 = env.check;
|
|
23868
|
+
if (!check2) return fail(dataError("INTERNAL_ERROR"));
|
|
23869
|
+
source.used = true;
|
|
23870
|
+
if (item.generated)
|
|
23871
|
+
check2.problem(item.name, "generated", env.blame.policy);
|
|
23872
|
+
if (how.textual && isNumeric(item))
|
|
23873
|
+
check2.problem(item.name, how.textual, env.blame.policy);
|
|
23874
|
+
const index = group.columns.indexOf(item);
|
|
23875
|
+
if (index < 0) {
|
|
23876
|
+
const omitted = omittedSql(item);
|
|
23877
|
+
if (omitted === void 0)
|
|
23878
|
+
fail(
|
|
23879
|
+
invalidQuery(
|
|
23880
|
+
`rows[${String(group.indexes[0] ?? 0)}]`,
|
|
23881
|
+
mustInclude(item.name, check2.action)
|
|
23882
|
+
)
|
|
23883
|
+
);
|
|
23884
|
+
return omitted;
|
|
23885
|
+
}
|
|
23886
|
+
if (!checked.has(item.name)) {
|
|
23887
|
+
checked.add(item.name);
|
|
23888
|
+
for (const [row, values] of group.rows.entries()) {
|
|
23889
|
+
const path = `rows[${String(group.indexes[row] ?? row)}].${item.name}`;
|
|
23890
|
+
if (item.primaryKey > 0 && values[index] === null)
|
|
23891
|
+
fail(invalidQuery(path, mustNotBeNull(check2.action)));
|
|
23892
|
+
const value2 = values[index];
|
|
23893
|
+
if (typeof value2 === "string" && refusesText(item, value2))
|
|
23894
|
+
fail(invalidQuery(path, mustBeNumber(check2.action)));
|
|
23895
|
+
}
|
|
23896
|
+
}
|
|
23897
|
+
const value = `_naro_new.column${String(index + 1)}`;
|
|
23898
|
+
return group.rows.every(
|
|
23899
|
+
(values) => readsAsIs(item, values[index] ?? null)
|
|
23900
|
+
) ? value : asStored(item, value);
|
|
23901
|
+
},
|
|
23902
|
+
operand: (item) => group.columns.includes(item) ? { stable: false, newColumnish: true, newCollation: "BINARY" } : { stable: false, newColumnish: false, newCollation: null }
|
|
23903
|
+
};
|
|
23904
|
+
return source;
|
|
23905
|
+
}
|
|
23906
|
+
function updateSource(table, values) {
|
|
23907
|
+
const plain = storedSource(table, [asciiLower(table.name)], null);
|
|
23908
|
+
const source = {
|
|
23909
|
+
table,
|
|
23910
|
+
names: [asciiLower(table.name)],
|
|
23911
|
+
used: false,
|
|
23912
|
+
read: (item, env, how) => {
|
|
23913
|
+
const check2 = env.check;
|
|
23914
|
+
if (!check2) return fail(dataError("INTERNAL_ERROR"));
|
|
23915
|
+
source.used = true;
|
|
23916
|
+
if (item.generated)
|
|
23917
|
+
check2.problem(item.name, "generated", env.blame.policy);
|
|
23918
|
+
const set2 = values.get(item.name);
|
|
23919
|
+
if (!set2) return plain.read(item, env, how);
|
|
23920
|
+
if (how.textual && isNumeric(item))
|
|
23921
|
+
check2.problem(item.name, how.textual, env.blame.policy);
|
|
23922
|
+
const bound = toBoundValue(set2.value);
|
|
23923
|
+
if (typeof bound === "string" && refusesText(item, bound))
|
|
23924
|
+
fail(invalidQuery(`values.${item.name}`, mustBeNumber(check2.action)));
|
|
23925
|
+
env.params.push(bound);
|
|
23926
|
+
return readsAsIs(item, bound) ? "?" : asStored(item, "?");
|
|
23927
|
+
},
|
|
23928
|
+
operand: (item) => values.has(item.name) ? { stable: false, newColumnish: false, newCollation: null } : { stable: true, newColumnish: true, newCollation: item.collation }
|
|
23929
|
+
};
|
|
23930
|
+
return source;
|
|
23931
|
+
}
|
|
23932
|
+
function checkProblemReason(column2, problem) {
|
|
23933
|
+
switch (problem) {
|
|
23934
|
+
case "generated":
|
|
23935
|
+
return `${column2} is a generated column, which a WITH CHECK cannot read (an update policy without WITH CHECK checks its USING)`;
|
|
23936
|
+
case "collation":
|
|
23937
|
+
return `${column2} has a collation naro cannot read, which a WITH CHECK needs to compare it`;
|
|
23938
|
+
case "pattern":
|
|
23939
|
+
return `${column2} is a number column, which a WITH CHECK cannot compare with LIKE or ILIKE`;
|
|
23940
|
+
case "textual":
|
|
23941
|
+
return `${column2} is a number column, which a WITH CHECK cannot turn into text (lower, upper, length, ::text)`;
|
|
23942
|
+
}
|
|
23943
|
+
}
|
|
23944
|
+
function isTrue(term) {
|
|
23945
|
+
return term.sql === "1" && term.params.length === 0;
|
|
23946
|
+
}
|
|
23947
|
+
var Rls = class {
|
|
23948
|
+
constructor(req, snapshot, table, nonce, commands) {
|
|
23949
|
+
this.req = req;
|
|
23950
|
+
this.snapshot = snapshot;
|
|
23951
|
+
this.table = table;
|
|
23952
|
+
this.nonce = nonce;
|
|
23953
|
+
this.outer = storedSource(table, [asciiLower(table.name)], null);
|
|
23954
|
+
const all = commands.flatMap((command) => {
|
|
23955
|
+
const found = this.found(command);
|
|
23956
|
+
return [...found.permissive, ...found.restrictive];
|
|
23957
|
+
});
|
|
23958
|
+
if (hasSubqueries(all)) this.state.active.push(asciiLower(table.name));
|
|
23959
|
+
for (const command of commands) {
|
|
23960
|
+
const found = this.found(command);
|
|
23961
|
+
for (const policy of [...found.permissive, ...found.restrictive]) {
|
|
23962
|
+
if (policy.owner === null) continue;
|
|
23963
|
+
const owner = table.byName.get(asciiLower(policy.owner));
|
|
23964
|
+
if (!owner || owner.generated)
|
|
23965
|
+
fail(
|
|
23966
|
+
policyInvalid(policy, command),
|
|
23967
|
+
owner ? `the owner column ${owner.name} is a generated column` : `the owner column ${policy.owner} is not on ${table.name}`
|
|
23968
|
+
);
|
|
23969
|
+
}
|
|
23970
|
+
}
|
|
23971
|
+
}
|
|
23972
|
+
state = newState2();
|
|
23973
|
+
/** Restrictive policies named by the checks' markers. */
|
|
23974
|
+
restrictive = [];
|
|
23975
|
+
outer;
|
|
23976
|
+
/**
|
|
23977
|
+
* A new statement of the same query — an insert's next group of rows:
|
|
23978
|
+
* MAX_STATEMENT_SUBQUERIES is a cap on one statement's SQL, so its count
|
|
23979
|
+
* starts again (P2-7 적대적 리뷰 반영: counted across an insert's groups,
|
|
23980
|
+
* rows naming different columns let any caller get the owner's 500).
|
|
23981
|
+
*/
|
|
23982
|
+
nextStatement() {
|
|
23983
|
+
this.state.subqueries = 0;
|
|
23984
|
+
this.state.aliases = 0;
|
|
23985
|
+
}
|
|
23986
|
+
found(command) {
|
|
23987
|
+
const found = applicablePolicies(
|
|
23988
|
+
this.req.policies,
|
|
23989
|
+
this.table.name,
|
|
23990
|
+
command,
|
|
23991
|
+
this.req.role
|
|
23992
|
+
);
|
|
23993
|
+
if (found.invalid)
|
|
23994
|
+
fail(
|
|
23995
|
+
policyInvalid(found.invalid, command),
|
|
23996
|
+
found.invalid.invalid ?? void 0
|
|
23997
|
+
);
|
|
23998
|
+
return found;
|
|
23999
|
+
}
|
|
24000
|
+
env(check2, blame, action) {
|
|
24001
|
+
return {
|
|
24002
|
+
req: this.req,
|
|
24003
|
+
snapshot: this.snapshot,
|
|
24004
|
+
scopes: [this.outer],
|
|
24005
|
+
params: [],
|
|
24006
|
+
state: this.state,
|
|
24007
|
+
blame: { policy: blame, action },
|
|
24008
|
+
check: check2
|
|
24009
|
+
};
|
|
24010
|
+
}
|
|
24011
|
+
/**
|
|
24012
|
+
* The owner column when every permissive policy of the command is an
|
|
24013
|
+
* owner policy of that one column (spec §10) — then a row that does not
|
|
24014
|
+
* hold the caller's id there fails the policy anyway, and is refused with
|
|
24015
|
+
* P1's POLICY_VIOLATION.
|
|
24016
|
+
*/
|
|
24017
|
+
ownerOnly(command) {
|
|
24018
|
+
const { permissive } = this.found(command);
|
|
24019
|
+
const owners = new Set(
|
|
24020
|
+
permissive.map(
|
|
24021
|
+
(policy) => policy.owner === null ? null : asciiLower(policy.owner)
|
|
24022
|
+
)
|
|
24023
|
+
);
|
|
24024
|
+
if (owners.size !== 1) return void 0;
|
|
24025
|
+
const [owner] = owners;
|
|
24026
|
+
return owner ? this.table.byName.get(owner) : void 0;
|
|
24027
|
+
}
|
|
24028
|
+
/** The columns an insert fills with the caller's id (spec §10). */
|
|
24029
|
+
fills() {
|
|
24030
|
+
const columns = /* @__PURE__ */ new Map();
|
|
24031
|
+
for (const policy of this.found("insert").permissive) {
|
|
24032
|
+
if (policy.owner === null) continue;
|
|
24033
|
+
const owner = this.table.byName.get(asciiLower(policy.owner));
|
|
24034
|
+
if (owner) columns.set(owner.name, owner);
|
|
24035
|
+
}
|
|
24036
|
+
return [...columns.values()];
|
|
24037
|
+
}
|
|
24038
|
+
/** WHERE's policy part (spec §5): the command's USING, and SELECT's. */
|
|
24039
|
+
where(action) {
|
|
24040
|
+
const commands = action === "select" ? ["select"] : [action, "select"];
|
|
24041
|
+
const terms = [];
|
|
24042
|
+
for (const command of commands) {
|
|
24043
|
+
const found = this.found(command);
|
|
24044
|
+
const first = found.permissive[0];
|
|
24045
|
+
if (!first) {
|
|
24046
|
+
terms.push({ sql: "0", params: [] });
|
|
24047
|
+
continue;
|
|
24048
|
+
}
|
|
24049
|
+
const group = groupTerms(
|
|
24050
|
+
this.env(null, first, command),
|
|
24051
|
+
[this.outer],
|
|
24052
|
+
command,
|
|
24053
|
+
found.permissive,
|
|
24054
|
+
found.restrictive,
|
|
24055
|
+
(policy) => policy.usingExpr
|
|
24056
|
+
);
|
|
24057
|
+
terms.push(group.permissive, ...group.restrictive.map((e) => e.term));
|
|
24058
|
+
}
|
|
24059
|
+
const kept = uniqueTerms(terms).filter((term) => !isTrue(term));
|
|
24060
|
+
const [only] = kept;
|
|
24061
|
+
if (!only) return null;
|
|
24062
|
+
if (kept.length === 1) return only;
|
|
24063
|
+
return {
|
|
24064
|
+
sql: `(${kept.map((term) => term.sql).join(" AND ")})`,
|
|
24065
|
+
params: kept.flatMap((term) => term.params)
|
|
24066
|
+
};
|
|
24067
|
+
}
|
|
24068
|
+
/**
|
|
24069
|
+
* The checks on a new row (spec §5, §8.2), in Postgres's order: the
|
|
24070
|
+
* command's permissive policies together, its restrictive ones each, then
|
|
24071
|
+
* — for an insert with `returning`, and every update — SELECT's. `skip`
|
|
24072
|
+
* leaves out a group a WHERE already guarantees (§8.5).
|
|
24073
|
+
*/
|
|
24074
|
+
checks(action, source, withSelect, skip, holdsCaller) {
|
|
24075
|
+
const groups = [
|
|
24076
|
+
{
|
|
24077
|
+
command: action,
|
|
24078
|
+
kind: action,
|
|
24079
|
+
pick: (policy) => policy.checkExpr ?? policy.usingExpr
|
|
24080
|
+
}
|
|
24081
|
+
];
|
|
24082
|
+
if (withSelect)
|
|
24083
|
+
groups.push({
|
|
24084
|
+
command: "select",
|
|
24085
|
+
kind: "select",
|
|
24086
|
+
pick: (policy) => policy.usingExpr
|
|
24087
|
+
});
|
|
24088
|
+
const terms = [];
|
|
24089
|
+
for (const { command, kind, pick: pick2 } of groups) {
|
|
24090
|
+
const found = this.found(command);
|
|
24091
|
+
const first = found.permissive[0];
|
|
24092
|
+
if (!first) {
|
|
24093
|
+
terms.push({
|
|
24094
|
+
sql: "0",
|
|
24095
|
+
params: [],
|
|
24096
|
+
marker: checkMarker(kind, this.nonce),
|
|
24097
|
+
kind,
|
|
24098
|
+
policy: void 0
|
|
24099
|
+
});
|
|
24100
|
+
continue;
|
|
24101
|
+
}
|
|
24102
|
+
if (skip(command, [...found.permissive, ...found.restrictive])) continue;
|
|
24103
|
+
const mode = {
|
|
24104
|
+
action: kind,
|
|
24105
|
+
problem: (name, problem, policy) => kind === "select" ? fail(
|
|
24106
|
+
selectCheckRefusal(
|
|
24107
|
+
action === "insert" ? "returning" : "values",
|
|
24108
|
+
name,
|
|
24109
|
+
problem
|
|
24110
|
+
)
|
|
24111
|
+
) : fail(
|
|
24112
|
+
policyInvalid(policy, action),
|
|
24113
|
+
checkProblemReason(name, problem)
|
|
24114
|
+
)
|
|
24115
|
+
};
|
|
24116
|
+
const ownerColumn = this.ownerOnly(command);
|
|
24117
|
+
const owned = this.req.uid !== null && ownerColumn !== void 0 && holdsCaller(ownerColumn);
|
|
24118
|
+
const group = groupTerms(
|
|
24119
|
+
{ ...this.env(mode, first, command), scopes: [source] },
|
|
24120
|
+
[source],
|
|
24121
|
+
command,
|
|
24122
|
+
owned ? [] : found.permissive,
|
|
24123
|
+
found.restrictive,
|
|
24124
|
+
pick2
|
|
24125
|
+
);
|
|
24126
|
+
if (!owned && !isTrue(group.permissive))
|
|
24127
|
+
terms.push({
|
|
24128
|
+
...group.permissive,
|
|
24129
|
+
marker: checkMarker(kind, this.nonce),
|
|
24130
|
+
kind,
|
|
24131
|
+
policy: first
|
|
24132
|
+
});
|
|
24133
|
+
for (const { policy, term } of group.restrictive) {
|
|
24134
|
+
if (isTrue(term)) continue;
|
|
24135
|
+
const index = this.restrictive.push(policy.name) - 1;
|
|
24136
|
+
terms.push({
|
|
24137
|
+
...term,
|
|
24138
|
+
marker: checkMarker(kind, this.nonce, index),
|
|
24139
|
+
kind,
|
|
24140
|
+
policy
|
|
24141
|
+
});
|
|
24142
|
+
}
|
|
24143
|
+
}
|
|
24144
|
+
return terms;
|
|
24145
|
+
}
|
|
24146
|
+
/**
|
|
24147
|
+
* Whether an update's check group is guaranteed by its WHERE (spec §8.5):
|
|
24148
|
+
* no subquery, no generated column, no column the update sets, and — for
|
|
24149
|
+
* UPDATE's own — every policy's check the same as its USING.
|
|
24150
|
+
*/
|
|
24151
|
+
implied(command, policies, set2) {
|
|
24152
|
+
for (const policy of policies) {
|
|
24153
|
+
if (command === "update") {
|
|
24154
|
+
if (policy.usingExpr === null) return false;
|
|
24155
|
+
if (policy.check !== null && policy.check !== policy.using)
|
|
24156
|
+
return false;
|
|
24157
|
+
}
|
|
24158
|
+
const expr = policy.usingExpr;
|
|
24159
|
+
if (expr === null) continue;
|
|
24160
|
+
const read = outerColumns(this.table, expr);
|
|
24161
|
+
if (!read) return false;
|
|
24162
|
+
if (read.some((item) => item.generated || set2.has(item.name)))
|
|
24163
|
+
return false;
|
|
24164
|
+
}
|
|
24165
|
+
return true;
|
|
24166
|
+
}
|
|
24167
|
+
/**
|
|
24168
|
+
* The number columns policies read, and those they read as true or false
|
|
24169
|
+
* or compare with a number, each with the first policy that does; read
|
|
24170
|
+
* once, on the first string.
|
|
24171
|
+
*/
|
|
24172
|
+
textReaders;
|
|
24173
|
+
/**
|
|
24174
|
+
* Refuses text written to a number column that a policy reads — any
|
|
24175
|
+
* table's policy, whatever its command or role, and whether or not this
|
|
24176
|
+
* statement's checks read it (known-issues #88). Stored, `'true'` is
|
|
24177
|
+
* false to `NOT is_private` and turns the policy inside out; Postgres
|
|
24178
|
+
* would read it as a boolean. A uuid-like column (`numberClass`
|
|
24179
|
+
* "fallback") keeps text that is no number, so only text it would store
|
|
24180
|
+
* as a number is refused — unless a policy reads it as true or false
|
|
24181
|
+
* (`truthColumns`) or compares it with a number (`numberComparedColumns`,
|
|
24182
|
+
* PR #80 리뷰: `hidden <> 1` let the text 'true' through): then it takes
|
|
24183
|
+
* no text at all. The refusal is named after the first policy that reads
|
|
24184
|
+
* the column, in the order they are read (table, then name), `all` after
|
|
24185
|
+
* the statement.
|
|
24186
|
+
*
|
|
24187
|
+
* And text a number or boolean column (INT, REAL, FLOA, DOUB, NUM, DEC,
|
|
24188
|
+
* BOOL, BIT) would keep as text, whether or not a policy reads it now
|
|
24189
|
+
* (PR #80 리뷰): a policy written later reads what is already stored.
|
|
24190
|
+
* Text it stores as a number ("7") is that number, and kept; so is the
|
|
24191
|
+
* caller's own id in an owner policy's column, which an owner insert
|
|
24192
|
+
* writes there itself and the owner condition compares as text (spec
|
|
24193
|
+
* §10 — owner policies are left out of the refusal above too).
|
|
24194
|
+
*/
|
|
24195
|
+
refuseText(action, path, target, value) {
|
|
24196
|
+
if (typeof value !== "string") return;
|
|
24197
|
+
const { numbers, truths } = this.textReaders ??= this.readersOfText();
|
|
24198
|
+
const reader = refusesText(target, value) ? numbers.get(target.name) : truths.get(target.name);
|
|
24199
|
+
if (reader)
|
|
24200
|
+
fail(
|
|
24201
|
+
invalidQuery(
|
|
24202
|
+
path,
|
|
24203
|
+
mustBeNumber(reader.command === "all" ? action : reader.command)
|
|
24204
|
+
)
|
|
24205
|
+
);
|
|
24206
|
+
if (numberClass(target.type) === "number" && !storesAsNumber(value) && !this.ownId(target, value))
|
|
24207
|
+
fail(invalidQuery(path, mustBeNumberFor(target.type)));
|
|
24208
|
+
}
|
|
24209
|
+
/** The caller's id, in a column an owner policy of this table names. */
|
|
24210
|
+
ownId(target, value) {
|
|
24211
|
+
return value === this.req.uid && this.req.policies.some(
|
|
24212
|
+
(policy) => policy.owner !== null && asciiLower(policy.owner) === asciiLower(target.name) && resolveTable(this.snapshot, policy.table) === this.table
|
|
24213
|
+
);
|
|
24214
|
+
}
|
|
24215
|
+
/**
|
|
24216
|
+
* The same refusal for a column an insert leaves out, whose DEFAULT is a
|
|
24217
|
+
* string (P2-7b 최종 리뷰 I2): SQLite stores it as it would store that
|
|
24218
|
+
* string sent, so `is_private BOOLEAN DEFAULT 'true'` holds the text
|
|
24219
|
+
* `'true'`, which `NOT is_private` reads as false. Only text SQLite keeps
|
|
24220
|
+
* as text — a DEFAULT `'0'` is stored as 0 — and then by the rule for a
|
|
24221
|
+
* sent string: a number or boolean column any policy reads, a fallback
|
|
24222
|
+
* one a policy reads as true or false. The client must send the value.
|
|
24223
|
+
* A name SQLite makes a string (`DEFAULT "true"`, `DEFAULT yes`) is that
|
|
24224
|
+
* text, and a blob `x'…'` — never stored as a number — is refused as text
|
|
24225
|
+
* that is no number (P2-7b 최종 리뷰 후속 2).
|
|
24226
|
+
*/
|
|
24227
|
+
refuseOmittedText(path, target) {
|
|
24228
|
+
const stored = stringDefault(target);
|
|
24229
|
+
if (stored === void 0) return;
|
|
24230
|
+
if (stored.kind === "text" && storesAsNumber(stored.text)) return;
|
|
24231
|
+
const { numbers, truths } = this.textReaders ??= this.readersOfText();
|
|
24232
|
+
const refused = stored.kind === "text" ? refusesText(target, stored.text) : numberClass(target.type) === "number";
|
|
24233
|
+
const reader = refused ? numbers.get(target.name) : truths.get(target.name);
|
|
24234
|
+
if (reader)
|
|
24235
|
+
fail(
|
|
24236
|
+
invalidQuery(
|
|
24237
|
+
path,
|
|
24238
|
+
mustIncludeFor(
|
|
24239
|
+
target.name,
|
|
24240
|
+
reader.command === "all" ? "insert" : reader.command
|
|
24241
|
+
)
|
|
24242
|
+
)
|
|
24243
|
+
);
|
|
24244
|
+
}
|
|
24245
|
+
readersOfText() {
|
|
24246
|
+
const numbers = /* @__PURE__ */ new Map();
|
|
24247
|
+
const truths = /* @__PURE__ */ new Map();
|
|
24248
|
+
for (const policy of this.req.policies) {
|
|
24249
|
+
if (policy.owner !== null) continue;
|
|
24250
|
+
const on = resolveTable(this.snapshot, policy.table);
|
|
24251
|
+
if (!on) continue;
|
|
24252
|
+
for (const expr of [policy.usingExpr, policy.checkExpr]) {
|
|
24253
|
+
if (expr === null) continue;
|
|
24254
|
+
for (const read of columnsReadOn(this.snapshot, on, expr, this.table))
|
|
24255
|
+
keepFirst(numbers, read, policy);
|
|
24256
|
+
for (const read of [
|
|
24257
|
+
...truthColumns(this.snapshot, on, expr),
|
|
24258
|
+
...numberComparedColumns(this.snapshot, on, expr)
|
|
24259
|
+
])
|
|
24260
|
+
if (read.table === this.table) keepFirst(truths, read.column, policy);
|
|
24261
|
+
}
|
|
24262
|
+
}
|
|
24263
|
+
return { numbers, truths };
|
|
24264
|
+
}
|
|
24265
|
+
/**
|
|
24266
|
+
* The policy part's values and SQL must leave D1's other half of each to
|
|
24267
|
+
* the request: 50 of 100 values, 50,000 of 100,000 bytes.
|
|
24268
|
+
*/
|
|
24269
|
+
budget(parts) {
|
|
24270
|
+
const count2 = parts.reduce((sum, part) => sum + part.params.length, 0);
|
|
24271
|
+
if (count2 > MAX_POLICY_VALUES)
|
|
24272
|
+
fail(
|
|
24273
|
+
dataError("POLICY_INVALID", {
|
|
24274
|
+
action: "select",
|
|
24275
|
+
table: this.table.name,
|
|
24276
|
+
values: true
|
|
24277
|
+
}),
|
|
24278
|
+
`they bind ${String(count2)}`
|
|
24279
|
+
);
|
|
24280
|
+
const bytes = parts.reduce(
|
|
24281
|
+
(sum, part) => sum + utf8ByteLength(part.sql),
|
|
24282
|
+
0
|
|
24283
|
+
);
|
|
24284
|
+
if (bytes > MAX_POLICY_STATEMENT_BYTES)
|
|
24285
|
+
fail(
|
|
24286
|
+
dataError("POLICY_INVALID", {
|
|
24287
|
+
action: "select",
|
|
24288
|
+
table: this.table.name,
|
|
24289
|
+
bytes: true
|
|
24290
|
+
}),
|
|
24291
|
+
`they write ${String(bytes)}`
|
|
24292
|
+
);
|
|
24293
|
+
}
|
|
24294
|
+
};
|
|
24295
|
+
function checkCase(table, terms, guard, pass, action, nonce) {
|
|
24296
|
+
if (terms.length === 0) return pass;
|
|
24297
|
+
let sql = pass.sql;
|
|
24298
|
+
let params = pass.params;
|
|
24299
|
+
for (const term of terms.toReversed()) {
|
|
24300
|
+
sql = `CASE WHEN ${term.sql} THEN ${sql} ELSE ${term.marker} END`;
|
|
24301
|
+
params = [...term.params, ...params];
|
|
24302
|
+
}
|
|
24303
|
+
if (!guard) return { sql, params };
|
|
24304
|
+
if (table.sql === null) {
|
|
24305
|
+
const first = terms.find((term) => term.policy !== void 0);
|
|
24306
|
+
if (first?.kind === "select" || !first?.policy)
|
|
24307
|
+
fail(
|
|
24308
|
+
invalidQuery(
|
|
24309
|
+
action === "insert" ? "returning" : "values",
|
|
24310
|
+
`is not available: naro cannot read the schema of ${table.name}, so a new row cannot be checked against its select policy`
|
|
24311
|
+
)
|
|
24312
|
+
);
|
|
24313
|
+
fail(policyInvalid(first.policy, action));
|
|
24314
|
+
}
|
|
24315
|
+
return {
|
|
24316
|
+
sql: `CASE WHEN (SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?) = ? THEN ${sql} ELSE ${schemaMarker(nonce)} END`,
|
|
24317
|
+
params: [table.name, table.sql, ...params]
|
|
24318
|
+
};
|
|
24319
|
+
}
|
|
24320
|
+
function compileSelect(query, table, rls) {
|
|
24321
|
+
if (query.columns !== "*" && query.columns.length === 0)
|
|
24322
|
+
fail(invalidQuery("columns", "must name at least one column"));
|
|
24323
|
+
const columns = query.columns === "*" ? table.columns : columnList(table, query.columns);
|
|
24324
|
+
const params = [];
|
|
24325
|
+
const policy = rls?.where("select") ?? null;
|
|
24326
|
+
if (rls && policy) rls.budget([policy]);
|
|
24327
|
+
const where = whereClause(table, query.filters ?? [], policy, params);
|
|
24328
|
+
const order = orderClause(table, query.order ?? []);
|
|
24329
|
+
const limit = query.limit ?? MAX_LIMIT;
|
|
24330
|
+
params.push(query.single ? Math.min(limit, 2) : limit, query.offset ?? 0);
|
|
24331
|
+
return [
|
|
24332
|
+
statement(
|
|
24333
|
+
`SELECT ${selectList(columns)} FROM ${quoteIdentifier(table.name)}${where}${order} LIMIT ? OFFSET ?`,
|
|
24334
|
+
params
|
|
24335
|
+
)
|
|
24336
|
+
];
|
|
24337
|
+
}
|
|
24338
|
+
function compileInsert(query, table, rls, nonce) {
|
|
24339
|
+
const uid = rls?.req.uid ?? null;
|
|
24340
|
+
const ownerInsert = rls?.ownerOnly("insert");
|
|
24341
|
+
const ownerRead = rls && query.returning !== void 0 ? rls.ownerOnly("select") : void 0;
|
|
24342
|
+
const fills = rls && uid !== null ? rls.fills() : [];
|
|
24343
|
+
const groups = /* @__PURE__ */ new Map();
|
|
24344
|
+
for (const [index, row] of query.rows.entries()) {
|
|
24345
|
+
const values = assignments(table, row, `rows[${index}]`);
|
|
24346
|
+
if (ownerInsert) {
|
|
24347
|
+
const given = values.get(ownerInsert.name);
|
|
24348
|
+
if (given && given.value !== uid)
|
|
24349
|
+
fail(dataError("POLICY_VIOLATION", { column: ownerInsert.name }));
|
|
24350
|
+
}
|
|
24351
|
+
for (const owner of fills)
|
|
24352
|
+
if (!values.has(owner.name) && uid !== null)
|
|
24353
|
+
values.set(owner.name, { column: owner, value: uid });
|
|
24354
|
+
if (ownerRead && values.get(ownerRead.name)?.value !== uid)
|
|
24355
|
+
fail(dataError("POLICY_VIOLATION", { column: ownerRead.name }));
|
|
24356
|
+
const ordered = [...values.values()].toSorted(
|
|
24357
|
+
(left, right) => left.column.cid - right.column.cid
|
|
24358
|
+
);
|
|
24359
|
+
const bound = ordered.map((item) => toBoundValue(item.value));
|
|
24360
|
+
const key = ordered.map(
|
|
24361
|
+
(item, at) => rls && readsAsIs(item.column, bound[at] ?? null) ? `${String(item.column.cid)}t` : String(item.column.cid)
|
|
24362
|
+
).join(",");
|
|
24363
|
+
let group = groups.get(key);
|
|
24364
|
+
if (!group) {
|
|
24365
|
+
group = {
|
|
24366
|
+
columns: ordered.map((item) => item.column),
|
|
24367
|
+
rows: [],
|
|
24368
|
+
indexes: []
|
|
24369
|
+
};
|
|
24370
|
+
groups.set(key, group);
|
|
24371
|
+
}
|
|
24372
|
+
group.rows.push(bound);
|
|
24373
|
+
group.indexes.push(index);
|
|
24374
|
+
}
|
|
24375
|
+
const plans = [...groups.values()].map((group) => {
|
|
24376
|
+
const width = group.columns.length;
|
|
24377
|
+
if (width > MAX_PARAMETERS) fail(dataError("TOO_MANY_PARAMETERS"));
|
|
24378
|
+
let where;
|
|
24379
|
+
if (rls) {
|
|
24380
|
+
rls.nextStatement();
|
|
24381
|
+
const source = insertSource(table, group);
|
|
24382
|
+
const terms = rls.checks(
|
|
24383
|
+
"insert",
|
|
24384
|
+
source,
|
|
24385
|
+
query.returning !== void 0,
|
|
24386
|
+
() => false,
|
|
24387
|
+
// Every row sends the caller's id there (filled in or checked).
|
|
24388
|
+
(owner) => {
|
|
24389
|
+
const index = group.columns.indexOf(owner);
|
|
24390
|
+
return index >= 0 && group.rows.every((row) => row[index] === uid);
|
|
24391
|
+
}
|
|
24392
|
+
);
|
|
24393
|
+
if (terms.length > 0) {
|
|
24394
|
+
where = checkCase(
|
|
24395
|
+
table,
|
|
24396
|
+
terms,
|
|
24397
|
+
source.used,
|
|
24398
|
+
{ sql: width === 0 ? "0" : "1", params: [] },
|
|
24399
|
+
"insert",
|
|
24400
|
+
nonce
|
|
24401
|
+
);
|
|
24402
|
+
rls.budget([
|
|
24403
|
+
{ sql: where.sql, params: terms.flatMap((term) => term.params) }
|
|
24404
|
+
]);
|
|
24405
|
+
}
|
|
24406
|
+
}
|
|
24407
|
+
const checkParams = where?.params ?? [];
|
|
24408
|
+
const room = MAX_PARAMETERS - checkParams.length;
|
|
24409
|
+
if (width > 0 && room < width) fail(dataError("TOO_MANY_PARAMETERS"));
|
|
24410
|
+
const perStatement = width === 0 ? 1 : Math.floor(room / width);
|
|
24411
|
+
const needed2 = width === 0 ? group.rows.length + (where === void 0 ? 0 : 1) : Math.ceil(group.rows.length / perStatement);
|
|
24412
|
+
return { group, width, where, perStatement, needed: needed2 };
|
|
24413
|
+
});
|
|
24414
|
+
const needed = plans.reduce((sum, plan) => sum + plan.needed, 0);
|
|
24415
|
+
if (needed > MAX_INSERT_STATEMENTS)
|
|
24416
|
+
fail(
|
|
24417
|
+
invalidQuery(
|
|
24418
|
+
"rows",
|
|
24419
|
+
`would take ${String(needed)} statements; one insert may take at most ${String(MAX_INSERT_STATEMENTS)} (each binds at most ${String(MAX_PARAMETERS)} values, and a row with no values takes one of its own)`
|
|
24420
|
+
)
|
|
24421
|
+
);
|
|
24422
|
+
const into = `INSERT ${OR_ABORT} INTO ${quoteIdentifier(table.name)}`;
|
|
24423
|
+
const returning = returningClause(table, query.returning, "insert");
|
|
24424
|
+
if (rls) {
|
|
24425
|
+
for (const { group } of plans)
|
|
24426
|
+
for (const [row, values] of group.rows.entries())
|
|
24427
|
+
for (const [index, item] of group.columns.entries())
|
|
24428
|
+
rls.refuseText(
|
|
24429
|
+
"insert",
|
|
24430
|
+
`rows[${String(group.indexes[row] ?? row)}].${item.name}`,
|
|
24431
|
+
item,
|
|
24432
|
+
values[index] ?? null
|
|
24433
|
+
);
|
|
24434
|
+
for (const { group } of plans)
|
|
24435
|
+
for (const item of table.columns)
|
|
24436
|
+
if (!item.generated && !group.columns.includes(item))
|
|
24437
|
+
rls.refuseOmittedText(`rows[${String(group.indexes[0] ?? 0)}]`, item);
|
|
24438
|
+
}
|
|
24439
|
+
const statements = [];
|
|
24440
|
+
for (const { group, width, where, perStatement } of plans) {
|
|
24441
|
+
if (width === 0) {
|
|
24442
|
+
if (where !== void 0)
|
|
24443
|
+
statements.push(statement(`SELECT 1 WHERE ${where.sql}`, where.params));
|
|
24444
|
+
for (let count2 = 0; count2 < group.rows.length; count2++)
|
|
24445
|
+
statements.push(statement(`${into} DEFAULT VALUES${returning}`, []));
|
|
24446
|
+
continue;
|
|
24447
|
+
}
|
|
24448
|
+
const tuple2 = `(${group.columns.map(() => "?").join(", ")})`;
|
|
24449
|
+
const names = selectList(group.columns);
|
|
24450
|
+
for (let start = 0; start < group.rows.length; start += perStatement) {
|
|
24451
|
+
const chunk = group.rows.slice(start, start + perStatement);
|
|
24452
|
+
const values = chunk.map(() => tuple2).join(", ");
|
|
24453
|
+
statements.push(
|
|
24454
|
+
where === void 0 ? statement(
|
|
24455
|
+
`${into} (${names}) VALUES ${values}${returning}`,
|
|
24456
|
+
chunk.flat()
|
|
24457
|
+
) : statement(
|
|
24458
|
+
`${into} (${names}) SELECT ${group.columns.map((_, index) => `_naro_new.column${String(index + 1)}`).join(", ")} FROM (VALUES ${values}) AS _naro_new WHERE ${where.sql}${returning}`,
|
|
24459
|
+
[...chunk.flat(), ...where.params]
|
|
24460
|
+
)
|
|
24461
|
+
);
|
|
24462
|
+
}
|
|
24463
|
+
}
|
|
24464
|
+
return statements;
|
|
24465
|
+
}
|
|
24466
|
+
function compileUpdate(query, table, context, rls, nonce) {
|
|
24467
|
+
requireFilter(query, context);
|
|
24468
|
+
const values = assignments(table, query.values, "values");
|
|
24469
|
+
if (values.size === 0)
|
|
24470
|
+
fail(invalidQuery("values", "must set at least one column"));
|
|
24471
|
+
const ownerUpdate = rls?.ownerOnly("update");
|
|
24472
|
+
if (ownerUpdate) {
|
|
24473
|
+
const given = values.get(ownerUpdate.name);
|
|
24474
|
+
if (given && given.value !== rls?.req.uid)
|
|
24475
|
+
fail(dataError("POLICY_VIOLATION", { column: ownerUpdate.name }));
|
|
24476
|
+
}
|
|
24477
|
+
const set2 = new Set(values.keys());
|
|
24478
|
+
const policy = rls?.where("update") ?? null;
|
|
24479
|
+
const source = rls ? updateSource(table, values) : void 0;
|
|
24480
|
+
const terms = rls && source ? rls.checks(
|
|
24481
|
+
"update",
|
|
24482
|
+
source,
|
|
24483
|
+
true,
|
|
24484
|
+
(command, policies) => rls.implied(command, policies, set2),
|
|
24485
|
+
// Kept as WHERE found it, or set to the caller's id.
|
|
24486
|
+
(owner) => {
|
|
24487
|
+
const given = values.get(owner.name);
|
|
24488
|
+
return !given || given.value === rls.req.uid;
|
|
24489
|
+
}
|
|
24490
|
+
) : [];
|
|
24491
|
+
const params = [];
|
|
24492
|
+
let checkSql = "";
|
|
24493
|
+
const assigned = [...values.values()].map((item, index) => {
|
|
24494
|
+
const value = { sql: "?", params: [toBoundValue(item.value)] };
|
|
24495
|
+
const expression = index === 0 && terms.length > 0 ? checkCase(table, terms, source?.used ?? false, value, "update", nonce) : value;
|
|
24496
|
+
if (expression !== value) checkSql = expression.sql;
|
|
24497
|
+
params.push(...expression.params);
|
|
24498
|
+
return `${quoteIdentifier(item.column.name)} = ${expression.sql}`;
|
|
24499
|
+
});
|
|
24500
|
+
rls?.budget([
|
|
24501
|
+
{ sql: checkSql, params: terms.flatMap((term) => term.params) },
|
|
24502
|
+
...policy ? [policy] : []
|
|
24503
|
+
]);
|
|
24504
|
+
const where = whereClause(table, query.filters ?? [], policy, params);
|
|
24505
|
+
const returning = returningClause(table, query.returning, "update");
|
|
24506
|
+
for (const item of values.values())
|
|
24507
|
+
rls?.refuseText(
|
|
24508
|
+
"update",
|
|
24509
|
+
`values.${item.column.name}`,
|
|
24510
|
+
item.column,
|
|
24511
|
+
toBoundValue(item.value)
|
|
24512
|
+
);
|
|
24513
|
+
return [
|
|
24514
|
+
statement(
|
|
24515
|
+
`UPDATE ${OR_ABORT} ${quoteIdentifier(table.name)} SET ${assigned.join(", ")}${where}${returning}`,
|
|
24516
|
+
params
|
|
24517
|
+
)
|
|
24518
|
+
];
|
|
24519
|
+
}
|
|
24520
|
+
function compileDelete(query, table, context, rls) {
|
|
24521
|
+
requireFilter(query, context);
|
|
24522
|
+
const params = [];
|
|
24523
|
+
const policy = rls?.where("delete") ?? null;
|
|
24524
|
+
if (rls && policy) rls.budget([policy]);
|
|
24525
|
+
const where = whereClause(table, query.filters ?? [], policy, params);
|
|
24526
|
+
return [
|
|
24527
|
+
statement(
|
|
24528
|
+
`DELETE FROM ${quoteIdentifier(table.name)}${where}${returningClause(table, query.returning, "delete")}`,
|
|
24529
|
+
params
|
|
24530
|
+
)
|
|
24531
|
+
];
|
|
24532
|
+
}
|
|
24533
|
+
function compile(query, snapshot, decision, context, options = {}) {
|
|
24534
|
+
const nonce = options.checkNonce ?? DEFAULT_CHECK_NONCE;
|
|
24535
|
+
if (!/^[0-9a-f]{1,64}$/.test(nonce))
|
|
24536
|
+
throw new Error("compile: checkNonce must be 1 to 64 hex digits");
|
|
24537
|
+
if (!decision.allow)
|
|
24538
|
+
return decision.reason === void 0 ? { ok: false, error: decision.error } : { ok: false, error: decision.error, reason: decision.reason };
|
|
24539
|
+
const table = resolveTable(snapshot, query.table);
|
|
24540
|
+
if (!table)
|
|
24541
|
+
return {
|
|
24542
|
+
ok: false,
|
|
24543
|
+
error: dataError("TABLE_NOT_FOUND", { table: query.table })
|
|
24544
|
+
};
|
|
24545
|
+
try {
|
|
24546
|
+
const rls = decision.rls ? new Rls(decision.rls, snapshot, table, nonce, commandsOf(query)) : void 0;
|
|
24547
|
+
let statements;
|
|
24548
|
+
switch (query.action) {
|
|
24549
|
+
case "select":
|
|
24550
|
+
statements = compileSelect(query, table, rls);
|
|
24551
|
+
break;
|
|
24552
|
+
case "insert":
|
|
24553
|
+
statements = compileInsert(query, table, rls, nonce);
|
|
24554
|
+
break;
|
|
24555
|
+
case "update":
|
|
24556
|
+
statements = compileUpdate(query, table, context, rls, nonce);
|
|
24557
|
+
break;
|
|
24558
|
+
case "delete":
|
|
24559
|
+
statements = compileDelete(query, table, context, rls);
|
|
24560
|
+
break;
|
|
24561
|
+
}
|
|
24562
|
+
return { ok: true, statements, restrictive: rls?.restrictive ?? [] };
|
|
24563
|
+
} catch (error51) {
|
|
24564
|
+
if (error51 instanceof Failure)
|
|
24565
|
+
return error51.reason === void 0 ? { ok: false, error: error51.error } : { ok: false, error: error51.error, reason: error51.reason };
|
|
24566
|
+
throw error51;
|
|
24567
|
+
}
|
|
24568
|
+
}
|
|
24569
|
+
|
|
24570
|
+
// ../../packages/data-core/src/policy-index.ts
|
|
24571
|
+
var INDEX_SQL = `SELECT t.name AS table_name, il.name AS index_name, il."partial" AS partial, ix.name AS column_name, ix.coll AS collation FROM pragma_table_list AS t JOIN pragma_index_list(t.name, t.schema) AS il JOIN pragma_index_xinfo(il.name, t.schema) AS ix WHERE t.schema = 'main' AND t.type = 'table' AND ${systemTablePredicate("t.name")} AND ix.seqno = 0`;
|
|
24572
|
+
function indexesFromRows(rows) {
|
|
24573
|
+
const tables = /* @__PURE__ */ new Map();
|
|
24574
|
+
for (const row of rows) {
|
|
24575
|
+
const table = row.table_name;
|
|
24576
|
+
const column2 = row.column_name;
|
|
24577
|
+
if (typeof table !== "string" || typeof column2 !== "string") continue;
|
|
24578
|
+
if (Number(row.partial) !== 0) continue;
|
|
24579
|
+
const key = asciiLower(table);
|
|
24580
|
+
const columns = tables.get(key) ?? /* @__PURE__ */ new Map();
|
|
24581
|
+
tables.set(key, columns);
|
|
24582
|
+
const collations = columns.get(asciiLower(column2)) ?? [];
|
|
24583
|
+
collations.push(
|
|
24584
|
+
typeof row.collation === "string" ? row.collation.toUpperCase() : "BINARY"
|
|
24585
|
+
);
|
|
24586
|
+
columns.set(asciiLower(column2), collations);
|
|
24587
|
+
}
|
|
24588
|
+
return tables;
|
|
24589
|
+
}
|
|
24590
|
+
function resolve8(scopes, qualifier, name) {
|
|
24591
|
+
const column2 = asciiLower(name);
|
|
24592
|
+
for (let index = scopes.length - 1; index >= 0; index--) {
|
|
24593
|
+
const scope = scopes[index];
|
|
24594
|
+
if (qualifier !== null && !scope.names.includes(asciiLower(qualifier)))
|
|
24595
|
+
continue;
|
|
24596
|
+
const found = scope.table.byName.get(column2);
|
|
24597
|
+
if (found) return { column: found, scope: index };
|
|
24598
|
+
if (qualifier !== null) return void 0;
|
|
24599
|
+
}
|
|
24600
|
+
return void 0;
|
|
24601
|
+
}
|
|
24602
|
+
function valueScopes(scopes, value) {
|
|
24603
|
+
switch (value.kind) {
|
|
24604
|
+
case "column": {
|
|
24605
|
+
const found = resolve8(scopes, value.qualifier, value.name);
|
|
24606
|
+
return found ? [found.scope] : [];
|
|
24607
|
+
}
|
|
24608
|
+
case "func":
|
|
24609
|
+
return value.args.flatMap((arg) => valueScopes(scopes, arg));
|
|
24610
|
+
case "cast":
|
|
24611
|
+
return valueScopes(scopes, value.operand);
|
|
24612
|
+
default:
|
|
24613
|
+
return [];
|
|
24614
|
+
}
|
|
24615
|
+
}
|
|
24616
|
+
function readsOutside(scopes, expr, depth, snapshot) {
|
|
24617
|
+
if (expr === null) return false;
|
|
24618
|
+
if (isValueExpr(expr))
|
|
24619
|
+
return valueScopes(scopes, expr).some((scope) => scope < depth);
|
|
24620
|
+
const values = (...items) => items.some(
|
|
24621
|
+
(item) => valueScopes(scopes, item).some((scope) => scope < depth)
|
|
24622
|
+
);
|
|
24623
|
+
switch (expr.kind) {
|
|
24624
|
+
case "and":
|
|
24625
|
+
case "or":
|
|
24626
|
+
return expr.args.some(
|
|
24627
|
+
(arg) => readsOutside(scopes, arg, depth, snapshot)
|
|
24628
|
+
);
|
|
24629
|
+
case "not":
|
|
24630
|
+
return readsOutside(scopes, expr.arg, depth, snapshot);
|
|
24631
|
+
case "compare":
|
|
24632
|
+
case "distinct":
|
|
24633
|
+
return values(expr.left, expr.right);
|
|
24634
|
+
case "is":
|
|
24635
|
+
case "like":
|
|
24636
|
+
return values(expr.operand);
|
|
24637
|
+
case "in":
|
|
24638
|
+
return values(expr.operand, ...expr.list);
|
|
24639
|
+
case "between":
|
|
24640
|
+
return values(expr.operand, expr.low, expr.high);
|
|
24641
|
+
case "array":
|
|
24642
|
+
return values(expr.operand);
|
|
24643
|
+
case "exists":
|
|
24644
|
+
case "inSelect": {
|
|
24645
|
+
if (expr.kind === "inSelect" && values(expr.operand)) return true;
|
|
24646
|
+
const inner = subqueryScopes2(scopes, expr.subquery, snapshot);
|
|
24647
|
+
if (!inner) return false;
|
|
24648
|
+
return expr.subquery.select !== "*" && readsOutside(inner, expr.subquery.select, depth, snapshot) || readsOutside(inner, expr.subquery.where, depth, snapshot);
|
|
24649
|
+
}
|
|
24650
|
+
}
|
|
24651
|
+
}
|
|
24652
|
+
function subqueryScopes2(scopes, subquery, snapshot) {
|
|
24653
|
+
const table = resolveTable(snapshot, subquery.table);
|
|
24654
|
+
if (!table) return void 0;
|
|
24655
|
+
return [
|
|
24656
|
+
...scopes,
|
|
24657
|
+
{ table, names: [asciiLower(subquery.alias ?? table.name)] }
|
|
24658
|
+
];
|
|
24659
|
+
}
|
|
24660
|
+
function conjuncts(expr) {
|
|
24661
|
+
if (expr === null) return [];
|
|
24662
|
+
if (!isValueExpr(expr) && expr.kind === "and")
|
|
24663
|
+
return expr.args.flatMap(conjuncts);
|
|
24664
|
+
return [expr];
|
|
24665
|
+
}
|
|
24666
|
+
function columnAffinityOf(column2) {
|
|
24667
|
+
const affinity2 = columnAffinity(column2);
|
|
24668
|
+
if (affinity2 === "TEXT") return "text";
|
|
24669
|
+
if (affinity2 === "BLOB") return "blob";
|
|
24670
|
+
return "numeric";
|
|
24671
|
+
}
|
|
24672
|
+
function valueAffinity(scopes, value) {
|
|
24673
|
+
if (value.kind === "cast") return "text";
|
|
24674
|
+
if (value.kind !== "column") return "none";
|
|
24675
|
+
const found = resolve8(scopes, value.qualifier, value.name);
|
|
24676
|
+
return found ? columnAffinityOf(found.column) : "none";
|
|
24677
|
+
}
|
|
24678
|
+
function valueCollation(scopes, value) {
|
|
24679
|
+
if (value.kind === "cast") return valueCollation(scopes, value.operand);
|
|
24680
|
+
if (value.kind !== "column") return void 0;
|
|
24681
|
+
const found = resolve8(scopes, value.qualifier, value.name);
|
|
24682
|
+
return found ? found.column.collation : void 0;
|
|
24683
|
+
}
|
|
24684
|
+
function waysIn(scopes, where) {
|
|
24685
|
+
const inner = scopes.length - 1;
|
|
24686
|
+
const own2 = (value) => {
|
|
24687
|
+
if (value.kind !== "column") return void 0;
|
|
24688
|
+
const found = resolve8(scopes, value.qualifier, value.name);
|
|
24689
|
+
return found?.scope === inner ? found.column : void 0;
|
|
24690
|
+
};
|
|
24691
|
+
const outside = (value) => valueScopes(scopes, value).every((scope) => scope < inner);
|
|
24692
|
+
const ways = [];
|
|
24693
|
+
const pair2 = (left, right) => {
|
|
24694
|
+
for (const [mine, other, mineLeft] of [
|
|
24695
|
+
[left, right, true],
|
|
24696
|
+
[right, left, false]
|
|
24697
|
+
]) {
|
|
24698
|
+
const column2 = own2(mine);
|
|
24699
|
+
if (!column2 || !outside(other)) continue;
|
|
24700
|
+
const first = mineLeft ? mine : other;
|
|
24701
|
+
const second = mineLeft ? other : mine;
|
|
24702
|
+
const collation = valueCollation(scopes, first) !== void 0 ? valueCollation(scopes, first) : valueCollation(scopes, second);
|
|
24703
|
+
const mineAffinity = columnAffinityOf(column2);
|
|
24704
|
+
const otherAffinity = valueAffinity(scopes, other);
|
|
24705
|
+
const affinity2 = otherAffinity === "none" ? mineAffinity : mineAffinity === "numeric" || otherAffinity === "numeric" ? "numeric" : "blob";
|
|
24706
|
+
ways.push({ column: column2, collation, affinity: affinity2 });
|
|
24707
|
+
}
|
|
24708
|
+
};
|
|
24709
|
+
for (const condition of conjuncts(where)) {
|
|
24710
|
+
if (isValueExpr(condition)) continue;
|
|
24711
|
+
if (condition.kind === "compare" && condition.op === "=")
|
|
24712
|
+
pair2(condition.left, condition.right);
|
|
24713
|
+
else if (condition.kind === "distinct" && condition.not)
|
|
24714
|
+
pair2(condition.left, condition.right);
|
|
24715
|
+
else if (condition.kind === "in" && !condition.not) {
|
|
24716
|
+
const column2 = own2(condition.operand);
|
|
24717
|
+
if (column2 && condition.list.every(outside))
|
|
24718
|
+
ways.push({
|
|
24719
|
+
column: column2,
|
|
24720
|
+
collation: column2.collation,
|
|
24721
|
+
affinity: columnAffinityOf(column2)
|
|
24722
|
+
});
|
|
24723
|
+
}
|
|
24724
|
+
}
|
|
24725
|
+
return ways;
|
|
24726
|
+
}
|
|
24727
|
+
function isRowid(table, column2) {
|
|
24728
|
+
const keys = table.columns.filter((item) => item.primaryKey > 0);
|
|
24729
|
+
return keys.length === 1 && keys[0] === column2 && asciiLower(column2.type.trim()) === "integer";
|
|
24730
|
+
}
|
|
24731
|
+
function blocked(table, indexes, way) {
|
|
24732
|
+
if (isRowid(table, way.column))
|
|
24733
|
+
return way.affinity === "text" ? "affinity" : void 0;
|
|
24734
|
+
const collations = indexes.get(asciiLower(table.name))?.get(asciiLower(way.column.name));
|
|
24735
|
+
if (!collations || collations.length === 0) return "none";
|
|
24736
|
+
const wanted = way.collation === void 0 ? way.column.collation : way.collation;
|
|
24737
|
+
if (wanted !== null && !collations.includes(wanted)) return "collation";
|
|
24738
|
+
const own2 = columnAffinityOf(way.column);
|
|
24739
|
+
if (way.affinity === "text" && own2 !== "text") return "affinity";
|
|
24740
|
+
if (way.affinity === "numeric" && own2 !== "numeric") return "affinity";
|
|
24741
|
+
return void 0;
|
|
24742
|
+
}
|
|
24743
|
+
function problemMessage(policy, outer, table, ways, why) {
|
|
24744
|
+
const start = `Policy ${policy.name} on ${outer.name}: its subquery on ${table.name} runs once for every row of ${outer.name} it looks at`;
|
|
24745
|
+
const override = "or send it with allow_unindexed=1 (naro policy create --allow-unindexed) to accept a scan of every row";
|
|
24746
|
+
const [way] = ways;
|
|
24747
|
+
if (!way)
|
|
24748
|
+
return `${start}, and no condition of its WHERE compares a column of ${table.name} with = to a value from outside it, so each run reads all of ${table.name}. Compare a column of ${table.name} with = (or IN) and index that column \u2014 ${override}.`;
|
|
24749
|
+
const column2 = way.column.name;
|
|
24750
|
+
if (why === "affinity")
|
|
24751
|
+
return `${start}, and it compares ${table.name}.${column2} with a value of another type (a number and text), which no index can search, so each run reads all of ${table.name}. Compare it with a value of its own type \u2014 ${override}.`;
|
|
24752
|
+
const wanted = way.collation === void 0 || way.collation === null ? null : way.collation;
|
|
24753
|
+
const collate = why === "collation" && wanted !== null && wanted !== way.column.collation ? ` COLLATE ${wanted}` : "";
|
|
24754
|
+
const index = quoteIdent(`${table.name}_${column2}_idx`);
|
|
24755
|
+
return `${start}, and no index starts with ${table.name}.${column2}${collate === "" ? "" : ` in collation ${String(wanted)}`}, so each run reads all of ${table.name}. Create one first: CREATE INDEX ${index} ON ${quoteIdent(table.name)} (${quoteIdent(column2)}${collate}); \u2014 ${override}.`;
|
|
24756
|
+
}
|
|
24757
|
+
function unindexedSubqueries(snapshot, indexes, policies) {
|
|
24758
|
+
const found = [];
|
|
24759
|
+
for (const policy of policies) {
|
|
24760
|
+
if (policy.invalid !== null) continue;
|
|
24761
|
+
const outer = resolveTable(snapshot, policy.table);
|
|
24762
|
+
if (!outer) continue;
|
|
24763
|
+
const visit = (scopes, expr) => {
|
|
24764
|
+
if (expr === null || isValueExpr(expr)) return;
|
|
24765
|
+
switch (expr.kind) {
|
|
24766
|
+
case "and":
|
|
24767
|
+
case "or":
|
|
24768
|
+
expr.args.forEach((arg) => visit(scopes, arg));
|
|
24769
|
+
return;
|
|
24770
|
+
case "not":
|
|
24771
|
+
visit(scopes, expr.arg);
|
|
24772
|
+
return;
|
|
24773
|
+
case "exists":
|
|
24774
|
+
case "inSelect": {
|
|
24775
|
+
const inner = subqueryScopes2(scopes, expr.subquery, snapshot);
|
|
24776
|
+
if (!inner) return;
|
|
24777
|
+
const depth = inner.length - 1;
|
|
24778
|
+
const correlated = expr.subquery.select !== "*" && readsOutside(inner, expr.subquery.select, depth, snapshot) || readsOutside(inner, expr.subquery.where, depth, snapshot);
|
|
24779
|
+
if (correlated) {
|
|
24780
|
+
const table = inner[depth].table;
|
|
24781
|
+
const ways = waysIn(inner, expr.subquery.where);
|
|
24782
|
+
const reasons = ways.map((way) => blocked(table, indexes, way));
|
|
24783
|
+
if (!reasons.includes(void 0)) {
|
|
24784
|
+
const order = ["none", "collation", "affinity"];
|
|
24785
|
+
const best = order.map((reason) => reasons.indexOf(reason)).find((index) => index >= 0);
|
|
24786
|
+
const way = best === void 0 ? void 0 : ways[best];
|
|
24787
|
+
found.push({
|
|
24788
|
+
policy,
|
|
24789
|
+
table: table.name,
|
|
24790
|
+
message: problemMessage(
|
|
24791
|
+
policy,
|
|
24792
|
+
outer,
|
|
24793
|
+
table,
|
|
24794
|
+
way ? [way] : [],
|
|
24795
|
+
best === void 0 ? void 0 : reasons[best]
|
|
24796
|
+
)
|
|
24797
|
+
});
|
|
24798
|
+
}
|
|
24799
|
+
}
|
|
24800
|
+
visit(inner, expr.subquery.where);
|
|
24801
|
+
return;
|
|
24802
|
+
}
|
|
24803
|
+
default:
|
|
24804
|
+
return;
|
|
24805
|
+
}
|
|
24806
|
+
};
|
|
24807
|
+
const root = [{ table: outer, names: [asciiLower(outer.name)] }];
|
|
24808
|
+
visit(root, policy.usingExpr);
|
|
24809
|
+
visit(root, policy.checkExpr);
|
|
24810
|
+
}
|
|
24811
|
+
return found;
|
|
24812
|
+
}
|
|
24813
|
+
|
|
24814
|
+
// ../../packages/data-core/src/policy-check.ts
|
|
24815
|
+
var CONTEXTS = [
|
|
24816
|
+
{ role: "anon" },
|
|
24817
|
+
{
|
|
24818
|
+
role: "authenticated",
|
|
24819
|
+
userId: "naro-policy-check",
|
|
24820
|
+
email: "check@naro.invalid",
|
|
24821
|
+
emailVerified: true
|
|
24822
|
+
}
|
|
24823
|
+
];
|
|
24824
|
+
function probes(table, context, owners) {
|
|
24825
|
+
const writable = table.columns.filter((column2) => !column2.generated);
|
|
24826
|
+
const [first] = table.columns;
|
|
24827
|
+
const filters = first ? [{ operator: "is", column: first.name, value: null }] : [];
|
|
24828
|
+
const row = Object.fromEntries(
|
|
24829
|
+
writable.map((column2) => [
|
|
24830
|
+
column2.name,
|
|
24831
|
+
context.role === "authenticated" && owners.has(asciiLower(column2.name)) ? context.userId : isNumeric(column2) ? 0 : "naro-policy-check"
|
|
24832
|
+
])
|
|
24833
|
+
);
|
|
24834
|
+
const queries = [
|
|
24835
|
+
{ version: 1, action: "select", table: table.name, columns: "*" },
|
|
24836
|
+
{ version: 1, action: "insert", table: table.name, rows: [row] },
|
|
24837
|
+
{
|
|
24838
|
+
version: 1,
|
|
24839
|
+
action: "insert",
|
|
24840
|
+
table: table.name,
|
|
24841
|
+
rows: [row],
|
|
24842
|
+
returning: "*"
|
|
24843
|
+
},
|
|
24844
|
+
{ version: 1, action: "delete", table: table.name, filters }
|
|
24845
|
+
];
|
|
24846
|
+
if (writable.length > 0)
|
|
24847
|
+
queries.push({
|
|
24848
|
+
version: 1,
|
|
24849
|
+
action: "update",
|
|
24850
|
+
table: table.name,
|
|
24851
|
+
values: row,
|
|
24852
|
+
filters
|
|
24853
|
+
});
|
|
24854
|
+
return queries;
|
|
24855
|
+
}
|
|
24856
|
+
function namedInRefusal(message, policies) {
|
|
24857
|
+
let found;
|
|
24858
|
+
for (const policy of policies) {
|
|
24859
|
+
if (!message.startsWith(`Policy ${policy.name} on ${policy.table} cannot be`))
|
|
24860
|
+
continue;
|
|
24861
|
+
if (found === void 0 || policy.name.length > found.name.length)
|
|
24862
|
+
found = policy;
|
|
24863
|
+
}
|
|
24864
|
+
return found;
|
|
24865
|
+
}
|
|
24866
|
+
function problems(snapshot, policies, options) {
|
|
24867
|
+
const found = /* @__PURE__ */ new Map();
|
|
24868
|
+
const loaded = loadNamed(policies);
|
|
24869
|
+
for (const policy of loaded)
|
|
24870
|
+
if (policy.invalid !== null)
|
|
24871
|
+
found.set(`read:${policy.table}:${policy.name}`, {
|
|
24872
|
+
problem: {
|
|
24873
|
+
table: policy.table,
|
|
24874
|
+
message: `Policy ${policy.name} on ${policy.table} cannot be read: ${policy.invalid}.`,
|
|
24875
|
+
policy: policy.name,
|
|
24876
|
+
kind: "read"
|
|
24877
|
+
},
|
|
24878
|
+
policy: policy.name
|
|
24879
|
+
});
|
|
24880
|
+
const tables = /* @__PURE__ */ new Map();
|
|
24881
|
+
for (const policy of loaded) {
|
|
24882
|
+
const table = resolveTable(snapshot, policy.table);
|
|
24883
|
+
if (table) tables.set(table.name, table);
|
|
24884
|
+
}
|
|
24885
|
+
for (const policy of loaded) {
|
|
24886
|
+
const table = resolveTable(snapshot, policy.table);
|
|
24887
|
+
if (!table || policy.invalid !== null) continue;
|
|
24888
|
+
for (const expr of [policy.usingExpr, policy.checkExpr]) {
|
|
24889
|
+
if (expr === null) continue;
|
|
24890
|
+
for (const { written, column: column2 } of truthColumns(snapshot, table, expr)) {
|
|
24891
|
+
if (isNumeric(column2)) continue;
|
|
24892
|
+
const message = `Policy ${policy.name} on ${policy.table} cannot be applied: ${written} is a ${columnAffinity(column2)} column, whose truth value is not what its text says ('true' reads as false): compare it, ${written} = 'true'.`;
|
|
24893
|
+
found.set(`${table.name}:${message}`, {
|
|
24894
|
+
problem: {
|
|
24895
|
+
table: table.name,
|
|
24896
|
+
message,
|
|
24897
|
+
policy: policy.name,
|
|
24898
|
+
kind: "compile"
|
|
24899
|
+
},
|
|
24900
|
+
policy: policy.name
|
|
24901
|
+
});
|
|
24902
|
+
}
|
|
24903
|
+
}
|
|
24904
|
+
}
|
|
24905
|
+
for (const policy of loaded) {
|
|
24906
|
+
const table = resolveTable(snapshot, policy.table);
|
|
24907
|
+
if (!table || policy.invalid !== null) continue;
|
|
24908
|
+
for (const { written, column: column2 } of numberReadsOf(snapshot, table, policy)) {
|
|
24909
|
+
const stored = stringDefault(column2);
|
|
24910
|
+
if (stored === void 0) continue;
|
|
24911
|
+
if (stored.kind === "text" && storesAsNumber(stored.text)) continue;
|
|
24912
|
+
const kept = stored.kind === "text" ? "text" : "a blob";
|
|
24913
|
+
found.set(`default:${table.name}:${policy.name}:${written}`, {
|
|
24914
|
+
problem: {
|
|
24915
|
+
table: table.name,
|
|
24916
|
+
message: `Policy ${policy.name} on ${policy.table} reads ${written} as a boolean or a number, but its DEFAULT ${String(column2.defaultValue)} is stored as ${kept}, which the policy does not read as it says ('true' reads as false): the rows that took it \u2014 every row there when the column was added, and each service-key insert that leaves it out \u2014 hold that value. Write the default as a number (1, 0, TRUE or FALSE) and update those rows.`,
|
|
24917
|
+
policy: policy.name,
|
|
24918
|
+
kind: "default"
|
|
24919
|
+
},
|
|
24920
|
+
policy: policy.name
|
|
24921
|
+
});
|
|
24922
|
+
}
|
|
24923
|
+
}
|
|
24924
|
+
const probe = loadNamed(
|
|
24925
|
+
[...tables.values()].map((table) => ({
|
|
24926
|
+
table: table.name,
|
|
24927
|
+
name: "naro_policy_check",
|
|
24928
|
+
permissive: true,
|
|
24929
|
+
command: "select",
|
|
24930
|
+
roles: ["anon", "authenticated"],
|
|
24931
|
+
using: "true",
|
|
24932
|
+
check: null,
|
|
24933
|
+
owner: null
|
|
24934
|
+
}))
|
|
24935
|
+
);
|
|
24936
|
+
const compiled = [...loaded, ...probe];
|
|
24937
|
+
for (const table of tables.values()) {
|
|
24938
|
+
const owners = new Set(
|
|
24939
|
+
policiesOn(loaded, table.name).flatMap(
|
|
24940
|
+
(policy) => policy.owner ? [asciiLower(policy.owner)] : []
|
|
24941
|
+
)
|
|
24942
|
+
);
|
|
24943
|
+
for (const context of CONTEXTS)
|
|
24944
|
+
for (const query of probes(table, context, owners)) {
|
|
24945
|
+
const decision = decideQuery(context, query, compiled, table.name);
|
|
24946
|
+
const result = decision.allow ? compile(query, snapshot, decision, context) : {
|
|
24947
|
+
ok: false,
|
|
24948
|
+
error: decision.error,
|
|
24949
|
+
reason: decision.reason
|
|
24950
|
+
};
|
|
24951
|
+
if (result.ok || result.error.code !== "POLICY_INVALID") continue;
|
|
24952
|
+
const message = result.reason === void 0 ? result.error.message : `${result.error.message.replace(/\.$/, "")}: ${result.reason}.`;
|
|
24953
|
+
const named = namedInRefusal(result.error.message, loaded);
|
|
24954
|
+
const blamed2 = named === void 0 ? table.name : resolveTable(snapshot, named.table)?.name ?? named.table;
|
|
24955
|
+
const key = `${blamed2}:${message}`;
|
|
24956
|
+
if (!found.has(key))
|
|
24957
|
+
found.set(key, {
|
|
24958
|
+
problem: {
|
|
24959
|
+
table: blamed2,
|
|
24960
|
+
message,
|
|
24961
|
+
...named === void 0 ? {} : { policy: named.name },
|
|
24962
|
+
kind: "compile"
|
|
24963
|
+
},
|
|
24964
|
+
policy: ""
|
|
24965
|
+
});
|
|
24966
|
+
}
|
|
24967
|
+
}
|
|
24968
|
+
if (options.indexes && options.allowUnindexed !== true)
|
|
24969
|
+
for (const problem of unindexedSubqueries(
|
|
24970
|
+
snapshot,
|
|
24971
|
+
options.indexes,
|
|
24972
|
+
loaded
|
|
24973
|
+
)) {
|
|
24974
|
+
const key = `index:${problem.policy.table}:${problem.message}`;
|
|
24975
|
+
if (!found.has(key))
|
|
24976
|
+
found.set(key, {
|
|
24977
|
+
problem: {
|
|
24978
|
+
table: problem.policy.table,
|
|
24979
|
+
message: problem.message,
|
|
24980
|
+
policy: problem.policy.name,
|
|
24981
|
+
kind: "unindexed"
|
|
24982
|
+
},
|
|
24983
|
+
policy: problem.policy.name
|
|
24984
|
+
});
|
|
24985
|
+
}
|
|
24986
|
+
return found;
|
|
24987
|
+
}
|
|
24988
|
+
function byText(a, b) {
|
|
24989
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
24990
|
+
}
|
|
24991
|
+
function policySetProblems(snapshot, policies, options = {}) {
|
|
24992
|
+
return [...problems(snapshot, policies, options).values()].toSorted(
|
|
24993
|
+
(a, b) => byText(a.problem.table, b.problem.table) || byText(a.policy, b.policy) || byText(a.problem.message, b.problem.message)
|
|
24994
|
+
).map(({ problem }) => problem);
|
|
24995
|
+
}
|
|
24996
|
+
function numberReadsOf(snapshot, table, policy) {
|
|
24997
|
+
const reads = /* @__PURE__ */ new Map();
|
|
24998
|
+
for (const expr of [policy.usingExpr, policy.checkExpr]) {
|
|
24999
|
+
if (expr === null) continue;
|
|
25000
|
+
for (const read of [
|
|
25001
|
+
...truthColumns(snapshot, table, expr),
|
|
25002
|
+
...numberComparedColumns(snapshot, table, expr)
|
|
25003
|
+
]) {
|
|
25004
|
+
const key = JSON.stringify([read.table.name, read.column.name]);
|
|
25005
|
+
if (isNumeric(read.column) && !reads.has(key)) reads.set(key, read);
|
|
25006
|
+
}
|
|
25007
|
+
}
|
|
25008
|
+
return [...reads.values()];
|
|
25009
|
+
}
|
|
25010
|
+
|
|
25011
|
+
// ../../packages/data-core/src/policy-statement.ts
|
|
25012
|
+
function readPolicyName(stream) {
|
|
25013
|
+
const token = stream.peek();
|
|
25014
|
+
if (token.kind !== "ident")
|
|
25015
|
+
throw new PolicySyntaxError(
|
|
25016
|
+
`expected a policy name, found ${describeToken(token)}`,
|
|
25017
|
+
token.position
|
|
25018
|
+
);
|
|
25019
|
+
stream.next();
|
|
25020
|
+
return foldPolicyName(token.value, token.quoted);
|
|
25021
|
+
}
|
|
25022
|
+
function readRoleName(token) {
|
|
25023
|
+
if (token.kind !== "ident")
|
|
25024
|
+
throw new PolicySyntaxError(
|
|
25025
|
+
`expected a role, found ${describeToken(token)}`,
|
|
25026
|
+
token.position
|
|
25027
|
+
);
|
|
25028
|
+
return token.quoted ? token.value : asciiLower(token.value);
|
|
25029
|
+
}
|
|
25030
|
+
function readCommand(stream) {
|
|
25031
|
+
const token = stream.next();
|
|
25032
|
+
for (const command of [
|
|
25033
|
+
"all",
|
|
25034
|
+
"select",
|
|
25035
|
+
"insert",
|
|
25036
|
+
"update",
|
|
25037
|
+
"delete"
|
|
25038
|
+
])
|
|
25039
|
+
if (isKeyword(token, command)) return command;
|
|
25040
|
+
throw new PolicySyntaxError(
|
|
25041
|
+
`expected ALL, SELECT, INSERT, UPDATE or DELETE, found ${describeToken(token)}`,
|
|
25042
|
+
token.position
|
|
25043
|
+
);
|
|
25044
|
+
}
|
|
25045
|
+
function readClause(stream) {
|
|
25046
|
+
const open = stream.expectOperator("(");
|
|
25047
|
+
const expr = readCondition(stream);
|
|
25048
|
+
stream.expectOperator(")");
|
|
25049
|
+
checkExpressionSize(expr, open.position);
|
|
25050
|
+
return { text: printExpr(expr), position: open.position };
|
|
25051
|
+
}
|
|
25052
|
+
function readCreate(stream, start) {
|
|
25053
|
+
stream.expectKeyword("policy");
|
|
25054
|
+
const name = readPolicyName(stream);
|
|
25055
|
+
stream.expectKeyword("on");
|
|
25056
|
+
const table = readTableName(stream);
|
|
25057
|
+
let permissive = true;
|
|
25058
|
+
if (stream.acceptKeyword("as")) {
|
|
25059
|
+
const token = stream.next();
|
|
25060
|
+
if (isKeyword(token, "restrictive")) permissive = false;
|
|
25061
|
+
else if (!isKeyword(token, "permissive"))
|
|
25062
|
+
throw new PolicySyntaxError(
|
|
25063
|
+
`expected PERMISSIVE or RESTRICTIVE, found ${describeToken(token)}`,
|
|
25064
|
+
token.position
|
|
25065
|
+
);
|
|
22251
25066
|
}
|
|
22252
25067
|
let command = "all";
|
|
22253
25068
|
if (stream.acceptKeyword("for")) command = readCommand(stream);
|
|
@@ -22278,6 +25093,7 @@ function readCreate(stream, start) {
|
|
|
22278
25093
|
position = clause.position;
|
|
22279
25094
|
}
|
|
22280
25095
|
if (!roles.ok) throw new Error("unreachable");
|
|
25096
|
+
readEnd(stream);
|
|
22281
25097
|
const policy = {
|
|
22282
25098
|
table,
|
|
22283
25099
|
name,
|
|
@@ -22304,21 +25120,31 @@ function readDrop(stream) {
|
|
|
22304
25120
|
stream.expectKeyword("on");
|
|
22305
25121
|
const table = readTableName(stream);
|
|
22306
25122
|
if (!stream.acceptKeyword("cascade")) stream.acceptKeyword("restrict");
|
|
25123
|
+
readEnd(stream);
|
|
22307
25124
|
return { kind: "drop", name, table, ifExists };
|
|
22308
25125
|
}
|
|
22309
|
-
function
|
|
22310
|
-
|
|
25126
|
+
function readEnd(stream) {
|
|
25127
|
+
stream.acceptOperator(";");
|
|
25128
|
+
const rest = stream.peek();
|
|
25129
|
+
if (rest.kind !== "eof")
|
|
25130
|
+
throw new PolicySyntaxError(
|
|
25131
|
+
isKeyword(rest, "create") || isKeyword(rest, "drop") ? "one statement at a time" : `syntax error at or near ${describeToken(rest)}`,
|
|
25132
|
+
rest.position
|
|
25133
|
+
);
|
|
25134
|
+
}
|
|
25135
|
+
function parsePolicyStatement(text2) {
|
|
25136
|
+
if (policySqlTooLong(text2))
|
|
22311
25137
|
return {
|
|
22312
25138
|
ok: false,
|
|
22313
25139
|
reason: `the statement is longer than ${String(MAX_POLICY_SQL_BYTES)} bytes`,
|
|
22314
25140
|
position: 1
|
|
22315
25141
|
};
|
|
22316
25142
|
try {
|
|
22317
|
-
const stream = new TokenStream(tokenize(
|
|
25143
|
+
const stream = new TokenStream(tokenize(text2));
|
|
22318
25144
|
const start = stream.next();
|
|
22319
|
-
let
|
|
22320
|
-
if (isKeyword(start, "create"))
|
|
22321
|
-
else if (isKeyword(start, "drop"))
|
|
25145
|
+
let statement2;
|
|
25146
|
+
if (isKeyword(start, "create")) statement2 = readCreate(stream, start);
|
|
25147
|
+
else if (isKeyword(start, "drop")) statement2 = readDrop(stream);
|
|
22322
25148
|
else if (isKeyword(start, "alter"))
|
|
22323
25149
|
throw new PolicySyntaxError(
|
|
22324
25150
|
"ALTER POLICY is not supported here yet: DROP POLICY and CREATE POLICY again",
|
|
@@ -22331,24 +25157,17 @@ function parsePolicyStatement(text) {
|
|
|
22331
25157
|
"only CREATE POLICY and DROP POLICY are accepted here",
|
|
22332
25158
|
start.position
|
|
22333
25159
|
);
|
|
22334
|
-
|
|
22335
|
-
const rest = stream.peek();
|
|
22336
|
-
if (rest.kind !== "eof")
|
|
22337
|
-
throw new PolicySyntaxError(
|
|
22338
|
-
isKeyword(rest, "create") || isKeyword(rest, "drop") ? "one statement at a time" : `syntax error at or near ${describeToken(rest)}`,
|
|
22339
|
-
rest.position
|
|
22340
|
-
);
|
|
22341
|
-
return { ok: true, statement };
|
|
25160
|
+
return { ok: true, statement: statement2 };
|
|
22342
25161
|
} catch (error51) {
|
|
22343
25162
|
if (error51 instanceof PolicySyntaxError)
|
|
22344
25163
|
return { ok: false, reason: error51.reason, position: error51.position };
|
|
22345
25164
|
throw error51;
|
|
22346
25165
|
}
|
|
22347
25166
|
}
|
|
22348
|
-
function splitPolicyStatements(
|
|
25167
|
+
function splitPolicyStatements(text2) {
|
|
22349
25168
|
let tokens;
|
|
22350
25169
|
try {
|
|
22351
|
-
tokens = tokenize(
|
|
25170
|
+
tokens = tokenize(text2);
|
|
22352
25171
|
} catch (error51) {
|
|
22353
25172
|
if (error51 instanceof PolicySyntaxError)
|
|
22354
25173
|
return { ok: false, reason: error51.reason, position: error51.position };
|
|
@@ -22360,16 +25179,428 @@ function splitPolicyStatements(text) {
|
|
|
22360
25179
|
if (token.kind === "eof") break;
|
|
22361
25180
|
start ??= token.position - 1;
|
|
22362
25181
|
if (token.kind === "op" && token.value === ";") {
|
|
22363
|
-
statements.push(
|
|
25182
|
+
statements.push(text2.slice(start, token.position).trim());
|
|
22364
25183
|
start = void 0;
|
|
22365
25184
|
}
|
|
22366
25185
|
}
|
|
22367
25186
|
if (start !== void 0) {
|
|
22368
|
-
const last =
|
|
25187
|
+
const last = text2.slice(start).trim();
|
|
22369
25188
|
if (last !== "") statements.push(last);
|
|
22370
25189
|
}
|
|
22371
25190
|
return { ok: true, statements };
|
|
22372
25191
|
}
|
|
25192
|
+
var OWNER_PREFIX = "naro:owner";
|
|
25193
|
+
function ownerComment(text2, position) {
|
|
25194
|
+
if (!text2.startsWith(OWNER_PREFIX)) return void 0;
|
|
25195
|
+
const rest = text2.slice(OWNER_PREFIX.length);
|
|
25196
|
+
if (rest !== "" && !/^[ \t]/.test(rest)) return void 0;
|
|
25197
|
+
let column2 = rest.replace(/^[ \t]+|[ \t]+$/g, "");
|
|
25198
|
+
if (column2.startsWith('"')) {
|
|
25199
|
+
const quoted2 = /^"((?:[^"]|"")*)"$/.exec(column2);
|
|
25200
|
+
if (!quoted2)
|
|
25201
|
+
throw new PolicySyntaxError(
|
|
25202
|
+
"naro:owner needs one column name, bare or in double quotes",
|
|
25203
|
+
position
|
|
25204
|
+
);
|
|
25205
|
+
column2 = (quoted2[1] ?? "").replaceAll('""', '"');
|
|
25206
|
+
}
|
|
25207
|
+
if (column2 === "")
|
|
25208
|
+
throw new PolicySyntaxError("naro:owner needs a column name", position);
|
|
25209
|
+
return column2;
|
|
25210
|
+
}
|
|
25211
|
+
function readComment(stream) {
|
|
25212
|
+
stream.expectKeyword("on");
|
|
25213
|
+
stream.expectKeyword("policy");
|
|
25214
|
+
const name = readPolicyName(stream);
|
|
25215
|
+
stream.expectKeyword("on");
|
|
25216
|
+
const table = readTableName(stream);
|
|
25217
|
+
stream.expectKeyword("is");
|
|
25218
|
+
const token = stream.next();
|
|
25219
|
+
if (isKeyword(token, "null"))
|
|
25220
|
+
return { kind: "comment", name, table, owner: null, isOwner: true };
|
|
25221
|
+
if (token.kind !== "string")
|
|
25222
|
+
throw new PolicySyntaxError(
|
|
25223
|
+
`expected a string or NULL, found ${describeToken(token)}`,
|
|
25224
|
+
token.position
|
|
25225
|
+
);
|
|
25226
|
+
const owner = ownerComment(token.value, token.position);
|
|
25227
|
+
return owner === void 0 ? { kind: "comment", name, table, owner: null, isOwner: false } : { kind: "comment", name, table, owner, isOwner: true };
|
|
25228
|
+
}
|
|
25229
|
+
function readRowLevelSecurity(stream) {
|
|
25230
|
+
if (stream.acceptKeyword("if")) stream.expectKeyword("exists");
|
|
25231
|
+
stream.acceptKeyword("only");
|
|
25232
|
+
const table = readTableName(stream);
|
|
25233
|
+
const token = stream.next();
|
|
25234
|
+
let action;
|
|
25235
|
+
if (isKeyword(token, "enable")) action = "enable";
|
|
25236
|
+
else if (isKeyword(token, "disable")) action = "disable";
|
|
25237
|
+
else if (isKeyword(token, "force")) action = "force";
|
|
25238
|
+
else if (isKeyword(token, "no")) {
|
|
25239
|
+
stream.expectKeyword("force");
|
|
25240
|
+
action = "no-force";
|
|
25241
|
+
} else
|
|
25242
|
+
throw new PolicySyntaxError(
|
|
25243
|
+
`expected ENABLE, DISABLE, FORCE or NO FORCE, found ${describeToken(token)}`,
|
|
25244
|
+
token.position
|
|
25245
|
+
);
|
|
25246
|
+
stream.expectKeyword("row");
|
|
25247
|
+
stream.expectKeyword("level");
|
|
25248
|
+
stream.expectKeyword("security");
|
|
25249
|
+
return { kind: "rls", table, action };
|
|
25250
|
+
}
|
|
25251
|
+
function parseMigrationPolicyStatement(text2) {
|
|
25252
|
+
if (policySqlTooLong(text2))
|
|
25253
|
+
return {
|
|
25254
|
+
ok: false,
|
|
25255
|
+
reason: `the statement is longer than ${String(MAX_POLICY_SQL_BYTES)} bytes`,
|
|
25256
|
+
position: 1
|
|
25257
|
+
};
|
|
25258
|
+
try {
|
|
25259
|
+
const stream = new TokenStream(tokenize(text2));
|
|
25260
|
+
let statement2;
|
|
25261
|
+
if (stream.acceptKeyword("comment")) statement2 = readComment(stream);
|
|
25262
|
+
else if (isKeyword(stream.peek(), "alter") && isKeyword(stream.peek(1), "table")) {
|
|
25263
|
+
stream.next();
|
|
25264
|
+
stream.next();
|
|
25265
|
+
statement2 = readRowLevelSecurity(stream);
|
|
25266
|
+
} else return parsePolicyStatement(text2);
|
|
25267
|
+
readEnd(stream);
|
|
25268
|
+
return { ok: true, statement: statement2 };
|
|
25269
|
+
} catch (error51) {
|
|
25270
|
+
if (error51 instanceof PolicySyntaxError)
|
|
25271
|
+
return { ok: false, reason: error51.reason, position: error51.position };
|
|
25272
|
+
throw error51;
|
|
25273
|
+
}
|
|
25274
|
+
}
|
|
25275
|
+
|
|
25276
|
+
// ../../packages/data-core/src/sql-head.ts
|
|
25277
|
+
var HEAD_TOKENS = 12;
|
|
25278
|
+
var BOM2 = 65279;
|
|
25279
|
+
function isBlankStart(char) {
|
|
25280
|
+
return char === " " || char === " " || char === "\n" || char === "\f" || char === "\r";
|
|
25281
|
+
}
|
|
25282
|
+
function isBlank(char) {
|
|
25283
|
+
return isBlankStart(char) || char === "\v";
|
|
25284
|
+
}
|
|
25285
|
+
function isWordStart(char) {
|
|
25286
|
+
return /[A-Za-z_]/.test(char) || char.charCodeAt(0) >= 128;
|
|
25287
|
+
}
|
|
25288
|
+
function isWordPart(char) {
|
|
25289
|
+
return /[A-Za-z0-9_$]/.test(char) || char.charCodeAt(0) >= 128;
|
|
25290
|
+
}
|
|
25291
|
+
function skipBlanks(text2, from) {
|
|
25292
|
+
let index = from;
|
|
25293
|
+
for (; ; ) {
|
|
25294
|
+
const char = text2.charAt(index);
|
|
25295
|
+
if (isBlankStart(char)) {
|
|
25296
|
+
index++;
|
|
25297
|
+
while (index < text2.length && isBlank(text2.charAt(index))) index++;
|
|
25298
|
+
} else if (text2.charCodeAt(index) === BOM2) index++;
|
|
25299
|
+
else if (char === "-" && text2.charAt(index + 1) === "-") {
|
|
25300
|
+
const end = text2.indexOf("\n", index + 2);
|
|
25301
|
+
index = end < 0 ? text2.length : end;
|
|
25302
|
+
} else if (char === "/" && text2.charAt(index + 1) === "*") {
|
|
25303
|
+
const end = text2.indexOf("*/", index + 2);
|
|
25304
|
+
index = end < 0 ? text2.length : end + 2;
|
|
25305
|
+
} else return index;
|
|
25306
|
+
}
|
|
25307
|
+
}
|
|
25308
|
+
function readQuoted(text2, start, delimiter) {
|
|
25309
|
+
let value = "";
|
|
25310
|
+
let index = start + 1;
|
|
25311
|
+
for (; ; ) {
|
|
25312
|
+
const close = text2.indexOf(delimiter, index);
|
|
25313
|
+
if (close < 0) return void 0;
|
|
25314
|
+
value += text2.slice(index, close);
|
|
25315
|
+
if (text2.charAt(close + 1) === delimiter) {
|
|
25316
|
+
value += delimiter;
|
|
25317
|
+
index = close + 2;
|
|
25318
|
+
continue;
|
|
25319
|
+
}
|
|
25320
|
+
return { text: value, end: close + 1 };
|
|
25321
|
+
}
|
|
25322
|
+
}
|
|
25323
|
+
function headTokens(text2) {
|
|
25324
|
+
const tokens = [];
|
|
25325
|
+
let index = 0;
|
|
25326
|
+
while (tokens.length < HEAD_TOKENS) {
|
|
25327
|
+
index = skipBlanks(text2, index);
|
|
25328
|
+
const char = text2.charAt(index);
|
|
25329
|
+
if (index >= text2.length) break;
|
|
25330
|
+
if (char === '"' || char === "`" || char === "'") {
|
|
25331
|
+
const quoted2 = readQuoted(text2, index, char);
|
|
25332
|
+
if (!quoted2) break;
|
|
25333
|
+
tokens.push({ kind: "name", text: quoted2.text });
|
|
25334
|
+
index = quoted2.end;
|
|
25335
|
+
} else if (char === "[") {
|
|
25336
|
+
const close = text2.indexOf("]", index + 1);
|
|
25337
|
+
if (close < 0) break;
|
|
25338
|
+
tokens.push({ kind: "name", text: text2.slice(index + 1, close) });
|
|
25339
|
+
index = close + 1;
|
|
25340
|
+
} else if ((char === "x" || char === "X") && text2.charAt(index + 1) === "'") {
|
|
25341
|
+
break;
|
|
25342
|
+
} else if (isWordStart(char)) {
|
|
25343
|
+
let end = index + 1;
|
|
25344
|
+
while (end < text2.length && isWordPart(text2.charAt(end))) end++;
|
|
25345
|
+
tokens.push({ kind: "word", text: text2.slice(index, end) });
|
|
25346
|
+
index = end;
|
|
25347
|
+
} else if (char === "." && !/[0-9]/.test(text2.charAt(index + 1))) {
|
|
25348
|
+
tokens.push({ kind: "dot" });
|
|
25349
|
+
index++;
|
|
25350
|
+
} else break;
|
|
25351
|
+
}
|
|
25352
|
+
tokens.push({ kind: "stop" });
|
|
25353
|
+
return tokens;
|
|
25354
|
+
}
|
|
25355
|
+
var OTHER = { kind: "other" };
|
|
25356
|
+
var NO_NAME = {
|
|
25357
|
+
kind: "unreadable",
|
|
25358
|
+
reason: "the table name could not be read"
|
|
25359
|
+
};
|
|
25360
|
+
var OTHER_SCHEMA = {
|
|
25361
|
+
kind: "unreadable",
|
|
25362
|
+
reason: "a table in another schema"
|
|
25363
|
+
};
|
|
25364
|
+
var PUBLIC_SCHEMA = {
|
|
25365
|
+
kind: "unreadable",
|
|
25366
|
+
reason: 'SQLite has no schema "public": write the table name without "public."'
|
|
25367
|
+
};
|
|
25368
|
+
var Head = class {
|
|
25369
|
+
constructor(tokens) {
|
|
25370
|
+
this.tokens = tokens;
|
|
25371
|
+
}
|
|
25372
|
+
token(at) {
|
|
25373
|
+
return this.tokens[Math.min(at, this.tokens.length - 1)] ?? { kind: "stop" };
|
|
25374
|
+
}
|
|
25375
|
+
/** The bare word `word` (lower case), in any ASCII case. */
|
|
25376
|
+
is(at, word) {
|
|
25377
|
+
const token = this.token(at);
|
|
25378
|
+
return token.kind === "word" && asciiLower(token.text) === word;
|
|
25379
|
+
}
|
|
25380
|
+
nameAt(at) {
|
|
25381
|
+
const token = this.token(at);
|
|
25382
|
+
return token.kind === "word" || token.kind === "name" ? token.text : void 0;
|
|
25383
|
+
}
|
|
25384
|
+
/**
|
|
25385
|
+
* SQLite's `fullname`. `main.` is the database a migration writes and is
|
|
25386
|
+
* dropped; `temp.` (in any case, quoted or not, as sqlite3FindDbName
|
|
25387
|
+
* matches it) is a table that goes with the connection; `public.` is
|
|
25388
|
+
* what a Postgres migration writes, said in its own words; any other
|
|
25389
|
+
* schema is one D1 does not have.
|
|
25390
|
+
*/
|
|
25391
|
+
table(at) {
|
|
25392
|
+
const first = this.nameAt(at);
|
|
25393
|
+
if (first === void 0) return { ok: false, head: NO_NAME };
|
|
25394
|
+
if (this.token(at + 1).kind !== "dot")
|
|
25395
|
+
return { ok: true, name: first, next: at + 1 };
|
|
25396
|
+
const second = this.nameAt(at + 2);
|
|
25397
|
+
if (second === void 0) return { ok: false, head: NO_NAME };
|
|
25398
|
+
const schema = asciiLower(first);
|
|
25399
|
+
if (schema === "main") return { ok: true, name: second, next: at + 3 };
|
|
25400
|
+
let head = OTHER_SCHEMA;
|
|
25401
|
+
if (schema === "temp") head = OTHER;
|
|
25402
|
+
else if (schema === "public") head = PUBLIC_SCHEMA;
|
|
25403
|
+
return { ok: false, head, next: at + 3 };
|
|
25404
|
+
}
|
|
25405
|
+
};
|
|
25406
|
+
function readCreate2(head) {
|
|
25407
|
+
if (head.is(1, "policy")) return { kind: "create-policy" };
|
|
25408
|
+
if (head.is(1, "temp") || head.is(1, "temporary")) return OTHER;
|
|
25409
|
+
let at;
|
|
25410
|
+
if (head.is(1, "table")) at = 2;
|
|
25411
|
+
else if (head.is(1, "virtual") && head.is(2, "table")) at = 3;
|
|
25412
|
+
else return OTHER;
|
|
25413
|
+
if (head.is(at, "if")) {
|
|
25414
|
+
if (!head.is(at + 1, "not") || !head.is(at + 2, "exists")) return NO_NAME;
|
|
25415
|
+
at += 3;
|
|
25416
|
+
}
|
|
25417
|
+
const table = head.table(at);
|
|
25418
|
+
return table.ok ? { kind: "create-table", table: table.name } : table.head;
|
|
25419
|
+
}
|
|
25420
|
+
function readDrop2(head) {
|
|
25421
|
+
if (head.is(1, "policy")) return { kind: "drop-policy" };
|
|
25422
|
+
if (!head.is(1, "table")) return OTHER;
|
|
25423
|
+
let at = 2;
|
|
25424
|
+
if (head.is(at, "if")) {
|
|
25425
|
+
if (!head.is(at + 1, "exists")) return NO_NAME;
|
|
25426
|
+
at += 2;
|
|
25427
|
+
}
|
|
25428
|
+
const table = head.table(at);
|
|
25429
|
+
return table.ok ? { kind: "drop-table", table: table.name } : table.head;
|
|
25430
|
+
}
|
|
25431
|
+
var RLS_ACTIONS = ["enable", "disable", "force", "no"];
|
|
25432
|
+
function readAlter(head) {
|
|
25433
|
+
if (head.is(1, "policy")) return { kind: "alter-policy" };
|
|
25434
|
+
if (!head.is(1, "table")) return OTHER;
|
|
25435
|
+
const sqlite = head.table(2);
|
|
25436
|
+
const action = sqlite.next;
|
|
25437
|
+
if (action !== void 0) {
|
|
25438
|
+
if (head.is(action, "rename") && head.is(action + 1, "to")) {
|
|
25439
|
+
if (!sqlite.ok) return sqlite.head;
|
|
25440
|
+
const to = head.table(action + 2);
|
|
25441
|
+
return to.ok ? { kind: "rename-table", table: sqlite.name, to: to.name } : to.head;
|
|
25442
|
+
}
|
|
25443
|
+
if (head.is(action, "rename") || head.is(action, "add") || head.is(action, "drop"))
|
|
25444
|
+
return sqlite.ok ? { kind: "alter-table", table: sqlite.name } : sqlite.head;
|
|
25445
|
+
}
|
|
25446
|
+
let at = 2;
|
|
25447
|
+
if (head.is(at, "if") && head.is(at + 1, "exists")) at += 2;
|
|
25448
|
+
if (head.is(at, "only")) at++;
|
|
25449
|
+
const next = head.table(at).next;
|
|
25450
|
+
if (next !== void 0 && RLS_ACTIONS.some((word) => head.is(next, word)))
|
|
25451
|
+
return { kind: "rls" };
|
|
25452
|
+
return sqlite.ok ? OTHER : sqlite.head;
|
|
25453
|
+
}
|
|
25454
|
+
function readSqlHead(text2) {
|
|
25455
|
+
if (text2.includes("\0"))
|
|
25456
|
+
return { kind: "unreadable", reason: "the statement has a NUL character" };
|
|
25457
|
+
const head = new Head(headTokens(text2));
|
|
25458
|
+
if (head.is(0, "create")) return readCreate2(head);
|
|
25459
|
+
if (head.is(0, "drop")) return readDrop2(head);
|
|
25460
|
+
if (head.is(0, "alter")) return readAlter(head);
|
|
25461
|
+
if (head.is(0, "comment") && head.is(1, "on") && head.is(2, "policy"))
|
|
25462
|
+
return { kind: "comment-policy" };
|
|
25463
|
+
if (head.is(0, "grant")) return { kind: "grant" };
|
|
25464
|
+
if (head.is(0, "revoke")) return { kind: "revoke" };
|
|
25465
|
+
return OTHER;
|
|
25466
|
+
}
|
|
25467
|
+
|
|
25468
|
+
// ../../packages/data-core/src/policy-migration.ts
|
|
25469
|
+
var GRANT_REFUSAL = "naro has no GRANT or REVOKE: every table is behind row-level security, so write a policy instead (CREATE POLICY \u2026).";
|
|
25470
|
+
function disableRefusal(table) {
|
|
25471
|
+
return `naro keeps row-level security on for every table. To let everyone through, create a policy: CREATE POLICY "open" ON ${quoteIdent(table)} USING (true) WITH CHECK (true);`;
|
|
25472
|
+
}
|
|
25473
|
+
function relationMissing(table) {
|
|
25474
|
+
return `relation "${table}" does not exist`;
|
|
25475
|
+
}
|
|
25476
|
+
var LATER_TABLE_CHANGE = "a later statement changes a table, so name the table of every column inside this policy's subqueries (for example members.user_id).";
|
|
25477
|
+
function tablesChanged(head) {
|
|
25478
|
+
switch (head.kind) {
|
|
25479
|
+
case "create-table":
|
|
25480
|
+
case "drop-table":
|
|
25481
|
+
case "alter-table":
|
|
25482
|
+
return [asciiLower(head.table)];
|
|
25483
|
+
case "rename-table":
|
|
25484
|
+
return [asciiLower(head.table), asciiLower(head.to)];
|
|
25485
|
+
case "unreadable":
|
|
25486
|
+
return "any";
|
|
25487
|
+
default:
|
|
25488
|
+
return [];
|
|
25489
|
+
}
|
|
25490
|
+
}
|
|
25491
|
+
function readsBareSubqueryColumn(policy) {
|
|
25492
|
+
return [policy.using, policy.check].some((text2) => {
|
|
25493
|
+
if (text2 === null) return false;
|
|
25494
|
+
const parsed = parseExpression(text2);
|
|
25495
|
+
return parsed.ok && unqualifiedInSubquery(parsed.expr) !== void 0;
|
|
25496
|
+
});
|
|
25497
|
+
}
|
|
25498
|
+
function tablesInScope(policy) {
|
|
25499
|
+
const tables = /* @__PURE__ */ new Set([asciiLower(policy.table)]);
|
|
25500
|
+
for (const text2 of [policy.using, policy.check]) {
|
|
25501
|
+
if (text2 === null) continue;
|
|
25502
|
+
const parsed = parseExpression(text2);
|
|
25503
|
+
if (!parsed.ok) continue;
|
|
25504
|
+
for (const subquery of subqueriesOf(parsed.expr))
|
|
25505
|
+
tables.add(asciiLower(subquery.table));
|
|
25506
|
+
}
|
|
25507
|
+
return tables;
|
|
25508
|
+
}
|
|
25509
|
+
function planPolicyMigration(statements) {
|
|
25510
|
+
const count2 = statements.length;
|
|
25511
|
+
const planned = [];
|
|
25512
|
+
let hasPolicyStatements = false;
|
|
25513
|
+
let touchesTables = false;
|
|
25514
|
+
const heads = statements.map((text2) => readSqlHead(text2));
|
|
25515
|
+
const changes = heads.map(tablesChanged);
|
|
25516
|
+
const changedAfter = (index, tables) => changes.slice(index + 1).some(
|
|
25517
|
+
(changed) => changed === "any" || changed.some((table) => tables.has(table))
|
|
25518
|
+
);
|
|
25519
|
+
for (const [index, text2] of statements.entries()) {
|
|
25520
|
+
const refuse = (message) => ({
|
|
25521
|
+
ok: false,
|
|
25522
|
+
index,
|
|
25523
|
+
message: `Statement ${String(index + 1)} of ${String(count2)}: ${message}`
|
|
25524
|
+
});
|
|
25525
|
+
const head = heads[index] ?? readSqlHead(text2);
|
|
25526
|
+
switch (head.kind) {
|
|
25527
|
+
case "grant":
|
|
25528
|
+
case "revoke":
|
|
25529
|
+
return refuse(GRANT_REFUSAL);
|
|
25530
|
+
case "unreadable":
|
|
25531
|
+
return refuse(`${head.reason}.`);
|
|
25532
|
+
case "create-table":
|
|
25533
|
+
case "drop-table":
|
|
25534
|
+
case "rename-table":
|
|
25535
|
+
touchesTables = true;
|
|
25536
|
+
planned.push({ kind: "verbatim", text: text2, head });
|
|
25537
|
+
continue;
|
|
25538
|
+
case "alter-table":
|
|
25539
|
+
case "other":
|
|
25540
|
+
planned.push({ kind: "verbatim", text: text2, head });
|
|
25541
|
+
continue;
|
|
25542
|
+
default:
|
|
25543
|
+
break;
|
|
25544
|
+
}
|
|
25545
|
+
const parsed = parseMigrationPolicyStatement(text2);
|
|
25546
|
+
if (!parsed.ok)
|
|
25547
|
+
return refuse(
|
|
25548
|
+
`${parsed.reason} at character ${String(parsed.position)}.`
|
|
25549
|
+
);
|
|
25550
|
+
const statement2 = parsed.statement;
|
|
25551
|
+
const table = statement2.kind === "create" ? statement2.policy.table : statement2.table;
|
|
25552
|
+
if (isSystemTable(table)) return refuse(`${relationMissing(table)}.`);
|
|
25553
|
+
switch (statement2.kind) {
|
|
25554
|
+
case "create":
|
|
25555
|
+
if (readsBareSubqueryColumn(statement2.policy) && changedAfter(index, tablesInScope(statement2.policy)))
|
|
25556
|
+
return refuse(LATER_TABLE_CHANGE);
|
|
25557
|
+
hasPolicyStatements = true;
|
|
25558
|
+
planned.push({ kind: "create-policy", policy: statement2.policy });
|
|
25559
|
+
break;
|
|
25560
|
+
case "drop":
|
|
25561
|
+
hasPolicyStatements = true;
|
|
25562
|
+
planned.push({
|
|
25563
|
+
kind: "drop-policy",
|
|
25564
|
+
name: statement2.name,
|
|
25565
|
+
table,
|
|
25566
|
+
ifExists: statement2.ifExists
|
|
25567
|
+
});
|
|
25568
|
+
break;
|
|
25569
|
+
case "comment":
|
|
25570
|
+
if (!statement2.isOwner) {
|
|
25571
|
+
planned.push({ kind: "noop" });
|
|
25572
|
+
break;
|
|
25573
|
+
}
|
|
25574
|
+
hasPolicyStatements = true;
|
|
25575
|
+
planned.push({
|
|
25576
|
+
kind: "owner",
|
|
25577
|
+
name: statement2.name,
|
|
25578
|
+
table,
|
|
25579
|
+
column: statement2.owner
|
|
25580
|
+
});
|
|
25581
|
+
break;
|
|
25582
|
+
case "rls":
|
|
25583
|
+
if (statement2.action === "disable")
|
|
25584
|
+
return refuse(disableRefusal(table));
|
|
25585
|
+
planned.push({ kind: "noop" });
|
|
25586
|
+
break;
|
|
25587
|
+
}
|
|
25588
|
+
}
|
|
25589
|
+
return {
|
|
25590
|
+
ok: true,
|
|
25591
|
+
plan: { statements: planned, hasPolicyStatements, touchesTables }
|
|
25592
|
+
};
|
|
25593
|
+
}
|
|
25594
|
+
function policyMigrationSql(policy) {
|
|
25595
|
+
const create = policySql({ ...policy, owner: null });
|
|
25596
|
+
if (policy.owner === null) return create;
|
|
25597
|
+
return `${create}
|
|
25598
|
+
COMMENT ON POLICY ${quotePolicyName(policy.name)} ON ${quoteIdent(policy.table)} IS ${sqlStringLiteral(`naro:owner ${quoteIdent(policy.owner)}`)};`;
|
|
25599
|
+
}
|
|
25600
|
+
function unreadablePolicyComment(policy) {
|
|
25601
|
+
return `-- Policy ${commentText(quotePolicyName(policy.name))} on ${commentText(quoteIdent(policy.table))} cannot be read (${commentText(policy.invalid)}); the requests it applies to are refused until it is dropped and created again.`;
|
|
25602
|
+
}
|
|
25603
|
+
var SYSTEM_PREDICATE = systemTablePredicate("name");
|
|
22373
25604
|
|
|
22374
25605
|
// ../../packages/data-core/src/redirect-origins.ts
|
|
22375
25606
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
@@ -22386,19 +25617,19 @@ var TYPED_PATH = /^[a-z][a-z0-9+.-]*:\/\/[^/]*(\/.*)?$/i;
|
|
|
22386
25617
|
var SEGMENT = /^[A-Za-z0-9._~-]+$/;
|
|
22387
25618
|
function parseRedirectEntry(value) {
|
|
22388
25619
|
if (typeof value !== "string") return null;
|
|
22389
|
-
const
|
|
22390
|
-
if (
|
|
22391
|
-
if (/[\s?#*%\\]/.test(
|
|
25620
|
+
const text2 = value.trim();
|
|
25621
|
+
if (text2 === "" || text2.length > MAX_ENTRY_LENGTH) return null;
|
|
25622
|
+
if (/[\s?#*%\\]/.test(text2)) return null;
|
|
22392
25623
|
let url2;
|
|
22393
25624
|
try {
|
|
22394
|
-
url2 = new URL(
|
|
25625
|
+
url2 = new URL(text2);
|
|
22395
25626
|
} catch {
|
|
22396
25627
|
return null;
|
|
22397
25628
|
}
|
|
22398
25629
|
if (url2.username !== "" || url2.password !== "") return null;
|
|
22399
25630
|
if (url2.search !== "" || url2.hash !== "") return null;
|
|
22400
25631
|
if (!isSecureOrLoopback(url2)) return null;
|
|
22401
|
-
const typed = TYPED_PATH.exec(
|
|
25632
|
+
const typed = TYPED_PATH.exec(text2)?.[1] ?? "/";
|
|
22402
25633
|
if (typed !== url2.pathname) return null;
|
|
22403
25634
|
const path = url2.pathname;
|
|
22404
25635
|
if (path !== "/") {
|
|
@@ -22604,7 +25835,7 @@ function tableOptions(sql) {
|
|
|
22604
25835
|
}
|
|
22605
25836
|
function rowidAlias(table) {
|
|
22606
25837
|
if (tableOptions(table.sql).withoutRowid) return null;
|
|
22607
|
-
const keys = table.columns.filter((
|
|
25838
|
+
const keys = table.columns.filter((column2) => column2.primaryKey);
|
|
22608
25839
|
const only = keys.length === 1 ? keys[0] : void 0;
|
|
22609
25840
|
return only && only.type.trim().toUpperCase() === "INTEGER" ? only.name : null;
|
|
22610
25841
|
}
|
|
@@ -22633,16 +25864,16 @@ async function policiesForTypes(client, projectId) {
|
|
|
22633
25864
|
};
|
|
22634
25865
|
}
|
|
22635
25866
|
}
|
|
22636
|
-
function isNullable(
|
|
22637
|
-
return !
|
|
25867
|
+
function isNullable(column2, alias) {
|
|
25868
|
+
return !column2.notNull && column2.name !== alias;
|
|
22638
25869
|
}
|
|
22639
|
-
function rowProperty(
|
|
22640
|
-
const type = READ[affinity(
|
|
22641
|
-
return ` ${keyLiteral(
|
|
25870
|
+
function rowProperty(column2, { alias, strict }) {
|
|
25871
|
+
const type = READ[affinity(column2.type, strict)];
|
|
25872
|
+
return ` ${keyLiteral(column2.name)}: ${withNull(type, isNullable(column2, alias))};`;
|
|
22642
25873
|
}
|
|
22643
|
-
function writeProperty(
|
|
22644
|
-
const type = WRITE[affinity(
|
|
22645
|
-
return ` ${keyLiteral(
|
|
25874
|
+
function writeProperty(column2, { alias, strict }, optional2) {
|
|
25875
|
+
const type = WRITE[affinity(column2.type, strict)];
|
|
25876
|
+
return ` ${keyLiteral(column2.name)}${optional2 ? "?" : ""}: ${withNull(type, isNullable(column2, alias))};`;
|
|
22646
25877
|
}
|
|
22647
25878
|
function block(name, lines) {
|
|
22648
25879
|
return lines.length === 0 ? [`export interface ${name} {}`] : [`export interface ${name} {`, ...lines, "}"];
|
|
@@ -22652,8 +25883,8 @@ function member(indent, name, lines) {
|
|
|
22652
25883
|
}
|
|
22653
25884
|
function tableEntry(described) {
|
|
22654
25885
|
const { table, row, alias, owner } = described;
|
|
22655
|
-
const writable = table.columns.filter((
|
|
22656
|
-
const optionalOnInsert = (
|
|
25886
|
+
const writable = table.columns.filter((column2) => column2.generated !== true);
|
|
25887
|
+
const optionalOnInsert = (column2) => isNullable(column2, alias) || column2.defaultValue !== null || column2.name === alias || asciiLower(column2.name) === owner;
|
|
22657
25888
|
return [
|
|
22658
25889
|
` ${keyLiteral(table.name)}: {`,
|
|
22659
25890
|
` Row: ${row};`,
|
|
@@ -22661,13 +25892,13 @@ function tableEntry(described) {
|
|
|
22661
25892
|
" ",
|
|
22662
25893
|
"Insert",
|
|
22663
25894
|
writable.map(
|
|
22664
|
-
(
|
|
25895
|
+
(column2) => writeProperty(column2, described, optionalOnInsert(column2))
|
|
22665
25896
|
)
|
|
22666
25897
|
),
|
|
22667
25898
|
...member(
|
|
22668
25899
|
" ",
|
|
22669
25900
|
"Update",
|
|
22670
|
-
writable.map((
|
|
25901
|
+
writable.map((column2) => writeProperty(column2, described, true))
|
|
22671
25902
|
),
|
|
22672
25903
|
" };"
|
|
22673
25904
|
];
|
|
@@ -22706,7 +25937,7 @@ function generateTypes(tables, options = {}) {
|
|
|
22706
25937
|
"",
|
|
22707
25938
|
...block(
|
|
22708
25939
|
row,
|
|
22709
|
-
table.columns.map((
|
|
25940
|
+
table.columns.map((column2) => rowProperty(column2, entry))
|
|
22710
25941
|
)
|
|
22711
25942
|
);
|
|
22712
25943
|
}
|
|
@@ -22792,7 +26023,7 @@ function ensureGitignoreHasNaro(dir) {
|
|
|
22792
26023
|
}
|
|
22793
26024
|
|
|
22794
26025
|
// ../../packages/sdk/src/lint.ts
|
|
22795
|
-
function
|
|
26026
|
+
function byText2(left, right) {
|
|
22796
26027
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
22797
26028
|
}
|
|
22798
26029
|
function lintedTables(input) {
|
|
@@ -22820,8 +26051,8 @@ function brokenForeignKeys(input) {
|
|
|
22820
26051
|
parents.set(violation.parent, group);
|
|
22821
26052
|
groups.set(violation.table, parents);
|
|
22822
26053
|
}
|
|
22823
|
-
return [...groups.entries()].toSorted(([left], [right]) =>
|
|
22824
|
-
([table, parents]) => [...parents.entries()].toSorted(([left], [right]) =>
|
|
26054
|
+
return [...groups.entries()].toSorted(([left], [right]) => byText2(left, right)).flatMap(
|
|
26055
|
+
([table, parents]) => [...parents.entries()].toSorted(([left], [right]) => byText2(left, right)).map(([parent, group]) => ({
|
|
22825
26056
|
rule: "broken-foreign-key",
|
|
22826
26057
|
severity: "error",
|
|
22827
26058
|
object: table,
|
|
@@ -22846,7 +26077,7 @@ function tableIssues(input) {
|
|
|
22846
26077
|
(index) => index.columns[0] === void 0 ? [] : [index.columns[0].toLowerCase()]
|
|
22847
26078
|
)
|
|
22848
26079
|
);
|
|
22849
|
-
const keyColumns = table.columns.filter((
|
|
26080
|
+
const keyColumns = table.columns.filter((column2) => column2.primaryKey);
|
|
22850
26081
|
const declared = keyColumns.length === 1 && keyColumns[0]?.type.trim().toUpperCase() === "INTEGER" ? keyColumns[0]?.name.toLowerCase() ?? null : null;
|
|
22851
26082
|
const rowidColumn = declared !== null && !leading.has(declared) ? declared : null;
|
|
22852
26083
|
if (keyColumns.length === 0)
|
|
@@ -22856,12 +26087,12 @@ function tableIssues(input) {
|
|
|
22856
26087
|
object: table.name,
|
|
22857
26088
|
message: "The table has no primary key, so its rows are identified by a rowid that changes after a VACUUM or a restore."
|
|
22858
26089
|
});
|
|
22859
|
-
for (const
|
|
22860
|
-
if (rowidColumn !== null ||
|
|
26090
|
+
for (const column2 of keyColumns) {
|
|
26091
|
+
if (rowidColumn !== null || column2.notNull) continue;
|
|
22861
26092
|
issues.push({
|
|
22862
26093
|
rule: "nullable-primary-key",
|
|
22863
26094
|
severity: "warn",
|
|
22864
|
-
object: `${table.name}.${
|
|
26095
|
+
object: `${table.name}.${column2.name}`,
|
|
22865
26096
|
message: "The primary key column is not INTEGER and has no NOT NULL, so SQLite accepts NULL in it."
|
|
22866
26097
|
});
|
|
22867
26098
|
}
|
|
@@ -22878,11 +26109,73 @@ function tableIssues(input) {
|
|
|
22878
26109
|
}
|
|
22879
26110
|
return issues;
|
|
22880
26111
|
}
|
|
26112
|
+
var RULE_OF_KIND = {
|
|
26113
|
+
read: "rls_policy_invalid",
|
|
26114
|
+
compile: "rls_policy_invalid",
|
|
26115
|
+
default: "rls_policy_text_default",
|
|
26116
|
+
unindexed: "rls_policy_unindexed"
|
|
26117
|
+
};
|
|
26118
|
+
var POLICY_WARNING_RULES = [
|
|
26119
|
+
"rls_policy_text_default",
|
|
26120
|
+
"rls_policy_unindexed"
|
|
26121
|
+
];
|
|
26122
|
+
function policyIssues(input) {
|
|
26123
|
+
const present = new Set(input.tables.map(asciiLower));
|
|
26124
|
+
const found = [
|
|
26125
|
+
...policySetProblems(
|
|
26126
|
+
input.snapshot,
|
|
26127
|
+
input.policies,
|
|
26128
|
+
input.indexes === void 0 ? {} : { indexes: input.indexes }
|
|
26129
|
+
).map((problem) => ({
|
|
26130
|
+
rule: RULE_OF_KIND[problem.kind],
|
|
26131
|
+
table: problem.table,
|
|
26132
|
+
policy: problem.policy ?? null,
|
|
26133
|
+
message: problem.message
|
|
26134
|
+
})),
|
|
26135
|
+
...input.unreadable.filter((row) => present.has(asciiLower(row.table))).map((row) => ({
|
|
26136
|
+
rule: "rls_policy_invalid",
|
|
26137
|
+
table: row.table,
|
|
26138
|
+
policy: row.name,
|
|
26139
|
+
message: `Policy ${row.name} on ${row.table} cannot be read (${row.reason}); the requests it applies to are refused until it is dropped and created again.`
|
|
26140
|
+
}))
|
|
26141
|
+
];
|
|
26142
|
+
const issues = found.toSorted(
|
|
26143
|
+
(left, right) => byText2(left.table, right.table) || byText2(left.policy ?? "", right.policy ?? "") || byText2(left.message, right.message)
|
|
26144
|
+
).map((entry) => ({
|
|
26145
|
+
rule: entry.rule,
|
|
26146
|
+
severity: entry.rule === "rls_policy_invalid" ? "error" : "warn",
|
|
26147
|
+
object: entry.policy === null ? entry.table : `${entry.table}.${entry.policy}`,
|
|
26148
|
+
message: entry.message
|
|
26149
|
+
}));
|
|
26150
|
+
return {
|
|
26151
|
+
errors: issues.filter((issue2) => issue2.severity === "error"),
|
|
26152
|
+
warnings: issues.filter((issue2) => issue2.severity === "warn").toSorted(
|
|
26153
|
+
(left, right) => POLICY_WARNING_RULES.indexOf(left.rule) - POLICY_WARNING_RULES.indexOf(right.rule)
|
|
26154
|
+
)
|
|
26155
|
+
};
|
|
26156
|
+
}
|
|
26157
|
+
function tablesWithoutPolicy(input) {
|
|
26158
|
+
const covered = new Set(
|
|
26159
|
+
[...input.policies, ...input.unreadable].map(
|
|
26160
|
+
(policy) => asciiLower(policy.table)
|
|
26161
|
+
)
|
|
26162
|
+
);
|
|
26163
|
+
return input.tables.filter((name) => !isSystemTable(name) && !covered.has(asciiLower(name))).toSorted(byText2).map((name) => ({
|
|
26164
|
+
rule: "rls_no_policy",
|
|
26165
|
+
severity: "warn",
|
|
26166
|
+
object: name,
|
|
26167
|
+
message: `Table ${name} has no policy, so the Data API refuses every request to it. Add one in a migration: CREATE POLICY \u2026`
|
|
26168
|
+
}));
|
|
26169
|
+
}
|
|
22881
26170
|
function lintSchema(input) {
|
|
26171
|
+
const policies = input.policies === void 0 ? void 0 : policyIssues(input.policies);
|
|
22882
26172
|
return [
|
|
22883
26173
|
...brokenForeignKeys(input),
|
|
22884
26174
|
...integrity(input),
|
|
26175
|
+
...policies?.errors ?? [],
|
|
22885
26176
|
...tableIssues(input),
|
|
26177
|
+
...input.policies === void 0 ? [] : tablesWithoutPolicy(input.policies),
|
|
26178
|
+
...policies?.warnings ?? [],
|
|
22886
26179
|
...input.triggers.filter((name) => !isSystemTable(name)).map((name) => ({
|
|
22887
26180
|
rule: "trigger-not-reproducible",
|
|
22888
26181
|
severity: "warn",
|
|
@@ -22900,10 +26193,10 @@ function lintSchema(input) {
|
|
|
22900
26193
|
|
|
22901
26194
|
// ../../packages/sdk/src/migrations.ts
|
|
22902
26195
|
import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
|
|
22903
|
-
import { resolve as
|
|
26196
|
+
import { resolve as resolve9 } from "path";
|
|
22904
26197
|
function resolveMigrationsDir(input) {
|
|
22905
|
-
if (input.dir) return
|
|
22906
|
-
return
|
|
26198
|
+
if (input.dir) return resolve9(input.baseDir, input.dir);
|
|
26199
|
+
return resolve9(input.baseDir, input.rootDirectory ?? "", "migrations");
|
|
22907
26200
|
}
|
|
22908
26201
|
function sortMigrationNames(names) {
|
|
22909
26202
|
return names.toSorted((a, b) => {
|
|
@@ -22985,6 +26278,8 @@ function migrationNameMismatch(sent, recorded) {
|
|
|
22985
26278
|
// ../../packages/sdk/src/reset-plan.ts
|
|
22986
26279
|
var RESET_DEFER_FOREIGN_KEYS = "pragma defer_foreign_keys = true;";
|
|
22987
26280
|
var RESET_BATCH_STATEMENTS = 50;
|
|
26281
|
+
var RESET_CLEAR_POLICIES = `DELETE FROM ${NAMED_POLICY_TABLE};`;
|
|
26282
|
+
var RESET_BACKEND_OUTDATED = "The project backend is older than named policies. Run `naro backend enable` to update it, then try again.";
|
|
22988
26283
|
function clearInternal(internal) {
|
|
22989
26284
|
return [
|
|
22990
26285
|
...internal.sqliteSequence ? ["DELETE FROM sqlite_sequence;"] : [],
|
|
@@ -23087,9 +26382,9 @@ function tableGroups(names, edges) {
|
|
|
23087
26382
|
}
|
|
23088
26383
|
return order;
|
|
23089
26384
|
}
|
|
23090
|
-
function pack(groups) {
|
|
26385
|
+
function pack(groups, first) {
|
|
23091
26386
|
const batches = [];
|
|
23092
|
-
let current = [RESET_DEFER_FOREIGN_KEYS];
|
|
26387
|
+
let current = [RESET_DEFER_FOREIGN_KEYS, ...first];
|
|
23093
26388
|
for (const group of groups) {
|
|
23094
26389
|
if (current.length > 1 && current.length + group.length > RESET_BATCH_STATEMENTS) {
|
|
23095
26390
|
batches.push(current);
|
|
@@ -23114,7 +26409,7 @@ function refuseOversizedCycle(groups) {
|
|
|
23114
26409
|
`A foreign key cycle of ${String(cycle.length)} tables cannot be dropped in one request (the limit is ${String(room)}): ${nameList(cycle)}. Break the cycle or drop those tables by hand.`
|
|
23115
26410
|
);
|
|
23116
26411
|
}
|
|
23117
|
-
function buildResetPlan(input) {
|
|
26412
|
+
function buildResetPlan(input, options = {}) {
|
|
23118
26413
|
const objects = input.objects.filter((object2) => !isSystemTable(object2.name));
|
|
23119
26414
|
const views = sorted(
|
|
23120
26415
|
objects.filter((object2) => object2.type === "view").map((object2) => object2.name)
|
|
@@ -23145,10 +26440,113 @@ function buildResetPlan(input) {
|
|
|
23145
26440
|
...clear.length > 0 ? [clear] : []
|
|
23146
26441
|
];
|
|
23147
26442
|
return {
|
|
23148
|
-
batches: pack(
|
|
26443
|
+
batches: pack(
|
|
26444
|
+
groups,
|
|
26445
|
+
options.clearPolicies === true ? [RESET_CLEAR_POLICIES] : []
|
|
26446
|
+
),
|
|
23149
26447
|
dropped: { tables: ordered.flat(), views, triggers }
|
|
23150
26448
|
};
|
|
23151
26449
|
}
|
|
26450
|
+
function sameTable(left, right) {
|
|
26451
|
+
return asciiLower(left) === asciiLower(right);
|
|
26452
|
+
}
|
|
26453
|
+
function policiesFromFiles(files) {
|
|
26454
|
+
let policies = [];
|
|
26455
|
+
const tables = /* @__PURE__ */ new Set();
|
|
26456
|
+
const forget = (table) => {
|
|
26457
|
+
policies = policies.filter((policy) => !sameTable(policy.table, table));
|
|
26458
|
+
};
|
|
26459
|
+
for (const [file2, sql] of files.entries()) {
|
|
26460
|
+
const split = splitStatements(sql);
|
|
26461
|
+
if (!split.ok)
|
|
26462
|
+
return {
|
|
26463
|
+
policies,
|
|
26464
|
+
tables,
|
|
26465
|
+
refused: { file: file2, reason: `sql could not be parsed: ${split.reason}.` }
|
|
26466
|
+
};
|
|
26467
|
+
const planned = planPolicyMigration(
|
|
26468
|
+
split.statements.map((statement2) => statement2.text)
|
|
26469
|
+
);
|
|
26470
|
+
if (!planned.ok)
|
|
26471
|
+
return {
|
|
26472
|
+
policies,
|
|
26473
|
+
tables,
|
|
26474
|
+
refused: { file: file2, reason: planned.message }
|
|
26475
|
+
};
|
|
26476
|
+
for (const statement2 of planned.plan.statements) {
|
|
26477
|
+
if (statement2.kind === "create-policy") {
|
|
26478
|
+
const { table, name } = statement2.policy;
|
|
26479
|
+
policies.push({ table, name });
|
|
26480
|
+
} else if (statement2.kind === "drop-policy") {
|
|
26481
|
+
policies = policies.filter(
|
|
26482
|
+
(policy) => !(sameTable(policy.table, statement2.table) && policy.name === statement2.name)
|
|
26483
|
+
);
|
|
26484
|
+
} else if (statement2.kind === "verbatim") {
|
|
26485
|
+
const head = statement2.head;
|
|
26486
|
+
if (head.kind === "create-table") {
|
|
26487
|
+
if (!tables.has(asciiLower(head.table))) forget(head.table);
|
|
26488
|
+
tables.add(asciiLower(head.table));
|
|
26489
|
+
} else if (head.kind === "drop-table") {
|
|
26490
|
+
forget(head.table);
|
|
26491
|
+
tables.delete(asciiLower(head.table));
|
|
26492
|
+
} else if (head.kind === "rename-table") {
|
|
26493
|
+
if (!tables.has(asciiLower(head.to))) forget(head.to);
|
|
26494
|
+
tables.delete(asciiLower(head.table));
|
|
26495
|
+
tables.add(asciiLower(head.to));
|
|
26496
|
+
}
|
|
26497
|
+
}
|
|
26498
|
+
}
|
|
26499
|
+
}
|
|
26500
|
+
return { policies, tables };
|
|
26501
|
+
}
|
|
26502
|
+
function policiesLostByReset(input) {
|
|
26503
|
+
const present = new Set(input.tables.map(asciiLower));
|
|
26504
|
+
const made = policiesFromFiles(input.files);
|
|
26505
|
+
if (made.refused) return { lost: [], withTables: [], refused: made.refused };
|
|
26506
|
+
const gone = input.current.filter(
|
|
26507
|
+
(policy) => present.has(asciiLower(policy.table)) && !made.policies.some(
|
|
26508
|
+
(other) => sameTable(other.table, policy.table) && other.name === policy.name
|
|
26509
|
+
)
|
|
26510
|
+
);
|
|
26511
|
+
return {
|
|
26512
|
+
lost: gone.filter((policy) => made.tables.has(asciiLower(policy.table))),
|
|
26513
|
+
withTables: gone.filter(
|
|
26514
|
+
(policy) => !made.tables.has(asciiLower(policy.table))
|
|
26515
|
+
)
|
|
26516
|
+
};
|
|
26517
|
+
}
|
|
26518
|
+
function resetPoliciesLostMessage(lost) {
|
|
26519
|
+
return `These ${String(lost.length)} policies are not in your migration files and will be gone after the reset: ${lost.map((policy) => `${policy.table}.${policy.name}`).join(", ")}. To keep them, add this to a migration first:`;
|
|
26520
|
+
}
|
|
26521
|
+
function resetPoliciesWithTablesMessage(policies) {
|
|
26522
|
+
return `Policies of tables your migration files do not create: ${policies.map((policy) => `${policy.table}.${policy.name}`).join(", ")} (they go with their tables).`;
|
|
26523
|
+
}
|
|
26524
|
+
function resetFileRefusedMessage(file2, reason) {
|
|
26525
|
+
return `${file2} would be refused: ${reason}`;
|
|
26526
|
+
}
|
|
26527
|
+
function storedPolicySql(row) {
|
|
26528
|
+
const policy = loadPolicy(row);
|
|
26529
|
+
if (policy === void 0) {
|
|
26530
|
+
const comment = unreadablePolicyComment({
|
|
26531
|
+
name: typeof row.name === "string" ? row.name : "(unnamed)",
|
|
26532
|
+
table: typeof row.table_name === "string" ? row.table_name : "",
|
|
26533
|
+
invalid: "the table name is missing"
|
|
26534
|
+
});
|
|
26535
|
+
return { sql: comment, statements: [comment] };
|
|
26536
|
+
}
|
|
26537
|
+
if (policy.invalid !== null) {
|
|
26538
|
+
const comment = unreadablePolicyComment({
|
|
26539
|
+
name: policy.name,
|
|
26540
|
+
table: policy.table,
|
|
26541
|
+
invalid: policy.invalid
|
|
26542
|
+
});
|
|
26543
|
+
return { sql: comment, statements: [comment] };
|
|
26544
|
+
}
|
|
26545
|
+
const sql = policyMigrationSql(policy);
|
|
26546
|
+
if (policy.owner === null) return { sql, statements: [sql] };
|
|
26547
|
+
const create = policyMigrationSql({ ...policy, owner: null });
|
|
26548
|
+
return { sql, statements: [create, sql.slice(create.length + 1)] };
|
|
26549
|
+
}
|
|
23152
26550
|
|
|
23153
26551
|
// src/env-consent.ts
|
|
23154
26552
|
import { existsSync as existsSync6 } from "fs";
|
|
@@ -23218,10 +26616,10 @@ var SUPPORTED_REMOTE = [
|
|
|
23218
26616
|
/^ssh:\/\/[^\s]+$/,
|
|
23219
26617
|
/^git@[A-Za-z0-9.-]+:[A-Za-z0-9._/-]+(\.git)?$/
|
|
23220
26618
|
];
|
|
23221
|
-
var defaultExec = (args, cwd) => new Promise((
|
|
26619
|
+
var defaultExec = (args, cwd) => new Promise((resolve11, reject) => {
|
|
23222
26620
|
execFile("git", args, { cwd }, (error51, stdout) => {
|
|
23223
26621
|
if (error51) reject(error51);
|
|
23224
|
-
else
|
|
26622
|
+
else resolve11(stdout);
|
|
23225
26623
|
});
|
|
23226
26624
|
});
|
|
23227
26625
|
async function tryExec(exec, args, cwd) {
|
|
@@ -23368,8 +26766,8 @@ async function uploadArtifact(artifact, opts) {
|
|
|
23368
26766
|
}
|
|
23369
26767
|
|
|
23370
26768
|
// src/deploy.ts
|
|
23371
|
-
function parseDotenv2(
|
|
23372
|
-
return parseDotenv(
|
|
26769
|
+
function parseDotenv2(text2) {
|
|
26770
|
+
return parseDotenv(text2);
|
|
23373
26771
|
}
|
|
23374
26772
|
function readProjectEnv2(dir) {
|
|
23375
26773
|
return readProjectEnv(dir);
|
|
@@ -23392,7 +26790,7 @@ function eventsObserver(cp3, deploymentId, token, fetchImpl = fetch, logIngestUr
|
|
|
23392
26790
|
} catch (error51) {
|
|
23393
26791
|
lastError = error51;
|
|
23394
26792
|
await new Promise(
|
|
23395
|
-
(
|
|
26793
|
+
(resolve11) => setTimeout(resolve11, 500 * (attempt + 1))
|
|
23396
26794
|
);
|
|
23397
26795
|
}
|
|
23398
26796
|
}
|
|
@@ -23528,7 +26926,7 @@ function localDependencies(sourceDir) {
|
|
|
23528
26926
|
async function deployDirectory(sourceDir, options, log2) {
|
|
23529
26927
|
const cpUrl = controlPlaneUrl();
|
|
23530
26928
|
const production = options.production ?? false;
|
|
23531
|
-
const appRoot = options.rootDirectory ?
|
|
26929
|
+
const appRoot = options.rootDirectory ? resolve10(sourceDir, options.rootDirectory) : sourceDir;
|
|
23532
26930
|
const framework = detectFramework2(appRoot);
|
|
23533
26931
|
if (!framework.deployable && !options.force) {
|
|
23534
26932
|
throw new NaroError(
|
|
@@ -23682,6 +27080,7 @@ export {
|
|
|
23682
27080
|
parseGitHubRemote,
|
|
23683
27081
|
schemaMigrationTitle,
|
|
23684
27082
|
projectPath,
|
|
27083
|
+
splitStatements,
|
|
23685
27084
|
NaroError,
|
|
23686
27085
|
isNaroError,
|
|
23687
27086
|
toNaroError,
|
|
@@ -23709,12 +27108,22 @@ export {
|
|
|
23709
27108
|
summarizeStatement,
|
|
23710
27109
|
serializeDotenv,
|
|
23711
27110
|
systemTablePredicate,
|
|
27111
|
+
SCHEMA_SQL,
|
|
27112
|
+
tableNames,
|
|
27113
|
+
snapshotFromRows,
|
|
23712
27114
|
quoteIdent,
|
|
23713
27115
|
commentText,
|
|
23714
27116
|
quotePolicyName,
|
|
27117
|
+
NAMED_POLICY_TABLE,
|
|
27118
|
+
NAMED_POLICY_SQL,
|
|
27119
|
+
loadPolicy,
|
|
23715
27120
|
parsePolicyNode,
|
|
27121
|
+
INDEX_SQL,
|
|
27122
|
+
indexesFromRows,
|
|
23716
27123
|
parsePolicyStatement,
|
|
23717
27124
|
splitPolicyStatements,
|
|
27125
|
+
planPolicyMigration,
|
|
27126
|
+
unreadablePolicyComment,
|
|
23718
27127
|
parseRedirectEntry,
|
|
23719
27128
|
formatRedirectEntry,
|
|
23720
27129
|
policiesForTypes,
|
|
@@ -23732,7 +27141,13 @@ export {
|
|
|
23732
27141
|
pendingMigrationNames,
|
|
23733
27142
|
migrationsPendingError,
|
|
23734
27143
|
migrationNameMismatch,
|
|
27144
|
+
RESET_BACKEND_OUTDATED,
|
|
23735
27145
|
buildResetPlan,
|
|
27146
|
+
policiesLostByReset,
|
|
27147
|
+
resetPoliciesLostMessage,
|
|
27148
|
+
resetPoliciesWithTablesMessage,
|
|
27149
|
+
resetFileRefusedMessage,
|
|
27150
|
+
storedPolicySql,
|
|
23736
27151
|
promptForMissingEnv,
|
|
23737
27152
|
detectGitInfo,
|
|
23738
27153
|
parseDotenv2 as parseDotenv,
|