@amerged/ohmyhost-cli 0.1.29 → 0.1.31
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 +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-Y4VB4EGQ.js → chunk-JPDCTZE5.js} +292 -105
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/apps/product-cli/package.json +1 -1
- package/src/apps/product-cli/src/repository-init.ts +2 -2
- package/src/packages/contracts/src/database-migration-admission.ts +65 -125
- package/src/packages/contracts/src/database-migration-errors.ts +14 -0
- package/src/packages/contracts/src/database-migration-lexical.ts +180 -0
- package/src/packages/contracts/src/database-migration-policies.ts +150 -0
- package/src/packages/contracts/src/framework-admission.ts +1 -1
- package/src/packages/contracts/src/framework-build-script.mjs +1 -2
- package/src/packages/sdk-ts/package.json +1 -1
package/README.md
CHANGED
package/dist/bin.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createRequire as __ohmyhostCreateRequire } from "node:module"; const require = __ohmyhostCreateRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
runProductionCli
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-JPDCTZE5.js";
|
|
6
6
|
import "./chunk-7D2RFLWM.js";
|
|
7
7
|
|
|
8
8
|
// apps/product-cli/dist/bin.js
|
|
@@ -35600,7 +35600,7 @@ function isAdmittedNextVersion(version2) {
|
|
|
35600
35600
|
const patch = Number(match[3]);
|
|
35601
35601
|
if (![major, minor, patch].every(Number.isSafeInteger))
|
|
35602
35602
|
return false;
|
|
35603
|
-
return major === 15 && minor === 5 && patch >= 26 || major === 16 && minor === 3 && patch
|
|
35603
|
+
return major === 15 && minor === 5 && patch >= 26 || major === 16 && minor === 3 && patch === 8;
|
|
35604
35604
|
}
|
|
35605
35605
|
var TYPESCRIPT_BUILD_STAGE = /^tsc(?:[ \t]+(?:--noEmit|-b|--build))?$/u;
|
|
35606
35606
|
function isAdmittedFrameworkBuildScript(framework, script) {
|
|
@@ -35710,7 +35710,7 @@ function resolveFrameworkConfig(basename2, files) {
|
|
|
35710
35710
|
var VERIFIED_VERSIONS = Object.freeze({
|
|
35711
35711
|
vite: /* @__PURE__ */ new Set(["5.4.21", "8.0.16", "8.2.2"]),
|
|
35712
35712
|
"tanstack-start": /* @__PURE__ */ new Set(["1.168.26", "1.168.49"]),
|
|
35713
|
-
nextjs: /* @__PURE__ */ new Set(["15.5.26", "16.3.
|
|
35713
|
+
nextjs: /* @__PURE__ */ new Set(["15.5.26", "16.3.8"])
|
|
35714
35714
|
});
|
|
35715
35715
|
function classifyFrameworkVersion(framework, version2) {
|
|
35716
35716
|
const parsed = parseVersionSpec(version2);
|
|
@@ -40236,7 +40236,7 @@ function exact3(v, keys) {
|
|
|
40236
40236
|
// apps/product-cli/package.json
|
|
40237
40237
|
var package_default = {
|
|
40238
40238
|
name: "@ohmyhost/product-cli",
|
|
40239
|
-
version: "0.1.
|
|
40239
|
+
version: "0.1.31",
|
|
40240
40240
|
private: true,
|
|
40241
40241
|
ohmyhost: {
|
|
40242
40242
|
deployment: "production",
|
|
@@ -46753,11 +46753,7 @@ function classifyFrameworkFamily(dependencies) {
|
|
|
46753
46753
|
return vite ? "vite" : "unknown";
|
|
46754
46754
|
}
|
|
46755
46755
|
|
|
46756
|
-
// packages/contracts/dist/database-migration-
|
|
46757
|
-
var MAX_MIGRATION_COUNT = 128;
|
|
46758
|
-
var MAX_MIGRATION_BYTES = 256 * 1024;
|
|
46759
|
-
var MAX_MIGRATION_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
46760
|
-
var MIGRATION_PATH = /^(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*\d{14}_[a-z0-9][a-z0-9_-]*\.sql$/u;
|
|
46756
|
+
// packages/contracts/dist/database-migration-errors.js
|
|
46761
46757
|
var DatabaseMigrationAdmissionError = class extends Error {
|
|
46762
46758
|
reason;
|
|
46763
46759
|
code = "database_migration_not_admitted";
|
|
@@ -46767,143 +46763,337 @@ var DatabaseMigrationAdmissionError = class extends Error {
|
|
|
46767
46763
|
this.name = "DatabaseMigrationAdmissionError";
|
|
46768
46764
|
}
|
|
46769
46765
|
};
|
|
46770
|
-
|
|
46771
|
-
|
|
46772
|
-
|
|
46773
|
-
}
|
|
46774
|
-
let previousPath = "";
|
|
46775
|
-
let totalBytes = 0;
|
|
46776
|
-
let statementCount = 0;
|
|
46777
|
-
const files = migrations.map((migration) => {
|
|
46778
|
-
if (typeof migration !== "object" || migration === null || !MIGRATION_PATH.test(migration.path) || migration.path <= previousPath || !(migration.body instanceof Uint8Array) || migration.body.byteLength === 0 || migration.body.byteLength > MAX_MIGRATION_BYTES) {
|
|
46779
|
-
throw new DatabaseMigrationAdmissionError("invalid");
|
|
46780
|
-
}
|
|
46781
|
-
previousPath = migration.path;
|
|
46782
|
-
totalBytes += migration.body.byteLength;
|
|
46783
|
-
if (totalBytes > MAX_MIGRATION_TOTAL_BYTES || migration.body.includes(0)) {
|
|
46784
|
-
throw new DatabaseMigrationAdmissionError("invalid");
|
|
46785
|
-
}
|
|
46786
|
-
let sql;
|
|
46787
|
-
try {
|
|
46788
|
-
sql = new TextDecoder("utf-8", { fatal: true }).decode(migration.body);
|
|
46789
|
-
} catch {
|
|
46790
|
-
throw new DatabaseMigrationAdmissionError("invalid");
|
|
46791
|
-
}
|
|
46792
|
-
if (sql.length === 0 || sql.includes("\r") || sql.charCodeAt(0) === 65279) {
|
|
46793
|
-
throw new DatabaseMigrationAdmissionError("invalid");
|
|
46794
|
-
}
|
|
46795
|
-
if (/\bohmyhost\b/iu.test(sql))
|
|
46796
|
-
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
46797
|
-
rejectProviderSpecificSql(sql);
|
|
46798
|
-
const statements = splitSqlStatements(sql);
|
|
46799
|
-
if (statements.length === 0)
|
|
46800
|
-
throw new DatabaseMigrationAdmissionError("invalid");
|
|
46801
|
-
for (const statement of statements)
|
|
46802
|
-
admitStatement(statement);
|
|
46803
|
-
statementCount += statements.length;
|
|
46804
|
-
return Object.freeze({ path: migration.path, statementCount: statements.length });
|
|
46805
|
-
});
|
|
46806
|
-
return Object.freeze({ files: Object.freeze(files), statementCount });
|
|
46807
|
-
}
|
|
46808
|
-
function rejectProviderSpecificSql(sql) {
|
|
46809
|
-
const normalized = sql.toLowerCase();
|
|
46810
|
-
const withoutPortableAuth = normalized.replace(/\bauth\s*\.\s*"(?:account|ratelimit|session|user|verification)"/gu, "");
|
|
46811
|
-
if (/\bauth\s*\./u.test(withoutPortableAuth) || /\bstorage\s*\./u.test(normalized) || /\b(?:to|from)\s+(?:anon|authenticated|service_role)\b/u.test(normalized) || /\bcreate\s+policy\b/u.test(normalized) || /\brow\s+level\s+security\b/u.test(normalized) || /\bsupabase\b/u.test(normalized)) {
|
|
46812
|
-
throw new DatabaseMigrationAdmissionError("provider_specific");
|
|
46813
|
-
}
|
|
46814
|
-
}
|
|
46815
|
-
function splitSqlStatements(sql) {
|
|
46766
|
+
|
|
46767
|
+
// packages/contracts/dist/database-migration-lexical.js
|
|
46768
|
+
function readMigrationSql(sql) {
|
|
46816
46769
|
const statements = [];
|
|
46770
|
+
let tokens = [];
|
|
46817
46771
|
let start = 0;
|
|
46818
46772
|
let index = 0;
|
|
46819
46773
|
while (index < sql.length) {
|
|
46820
|
-
const character = sql[index];
|
|
46774
|
+
const character = sql[index] ?? "";
|
|
46821
46775
|
const next = sql[index + 1];
|
|
46776
|
+
if (/\s/u.test(character)) {
|
|
46777
|
+
index += 1;
|
|
46778
|
+
continue;
|
|
46779
|
+
}
|
|
46822
46780
|
if (character === "-" && next === "-") {
|
|
46823
|
-
|
|
46781
|
+
const newline = sql.indexOf("\n", index + 2);
|
|
46782
|
+
index = newline < 0 ? sql.length : newline + 1;
|
|
46824
46783
|
continue;
|
|
46825
46784
|
}
|
|
46826
46785
|
if (character === "/" && next === "*") {
|
|
46827
46786
|
index = skipBlockComment(sql, index + 2);
|
|
46828
46787
|
continue;
|
|
46829
46788
|
}
|
|
46830
|
-
if (character === "
|
|
46831
|
-
|
|
46832
|
-
|
|
46833
|
-
|
|
46834
|
-
|
|
46835
|
-
|
|
46789
|
+
if (character === ";") {
|
|
46790
|
+
if (tokens.length > 0) {
|
|
46791
|
+
statements.push(Object.freeze({ sql: sql.slice(start, index + 1).trim(), tokens }));
|
|
46792
|
+
}
|
|
46793
|
+
tokens = [];
|
|
46794
|
+
start = ++index;
|
|
46836
46795
|
continue;
|
|
46837
46796
|
}
|
|
46838
|
-
|
|
46839
|
-
|
|
46840
|
-
|
|
46841
|
-
|
|
46842
|
-
|
|
46843
|
-
throw new DatabaseMigrationAdmissionError("invalid");
|
|
46844
|
-
index = end + delimiter.length;
|
|
46845
|
-
continue;
|
|
46846
|
-
}
|
|
46797
|
+
const tokenStart = index;
|
|
46798
|
+
let kind = "symbol";
|
|
46799
|
+
let value = character;
|
|
46800
|
+
if (/[uU]/u.test(character) && next === "&" && /['"]/u.test(sql[index + 2] ?? "")) {
|
|
46801
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
46847
46802
|
}
|
|
46848
|
-
if (character === "
|
|
46849
|
-
const
|
|
46850
|
-
|
|
46851
|
-
|
|
46852
|
-
|
|
46803
|
+
if (character === "'" || character === '"' || /[eE]/u.test(character) && next === "'") {
|
|
46804
|
+
const escaped = character !== "'" && character !== '"';
|
|
46805
|
+
const quote = character === '"' ? '"' : "'";
|
|
46806
|
+
const quoteStart = index + (escaped ? 1 : 0);
|
|
46807
|
+
index = skipQuoted(sql, quoteStart + 1, quote, escaped);
|
|
46808
|
+
kind = quote === '"' ? "identifier" : "string";
|
|
46809
|
+
value = sql.slice(quoteStart + 1, index - 1).replace(/""/gu, '"');
|
|
46810
|
+
} else if (character === "$" && /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/u.test(sql.slice(index))) {
|
|
46811
|
+
const delimiter = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/u.exec(sql.slice(index))?.[0] ?? "";
|
|
46812
|
+
const end = sql.indexOf(delimiter, index + delimiter.length);
|
|
46813
|
+
if (end < 0)
|
|
46814
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
46815
|
+
index = end + delimiter.length;
|
|
46816
|
+
kind = "string";
|
|
46817
|
+
value = sql.slice(tokenStart, index);
|
|
46818
|
+
} else if (/[\p{L}_]/u.test(character)) {
|
|
46819
|
+
index += 1;
|
|
46820
|
+
while (index < sql.length && /[\p{L}\p{N}_$]/u.test(sql[index] ?? ""))
|
|
46821
|
+
index += 1;
|
|
46822
|
+
kind = "word";
|
|
46823
|
+
value = sql.slice(tokenStart, index).toLowerCase();
|
|
46824
|
+
} else {
|
|
46825
|
+
index += 1;
|
|
46853
46826
|
}
|
|
46854
|
-
index
|
|
46827
|
+
tokens.push(Object.freeze({ kind, value, text: sql.slice(tokenStart, index) }));
|
|
46855
46828
|
}
|
|
46856
|
-
|
|
46857
|
-
|
|
46858
|
-
statements.push(tail);
|
|
46829
|
+
if (tokens.length > 0)
|
|
46830
|
+
statements.push(Object.freeze({ sql: sql.slice(start).trim(), tokens }));
|
|
46859
46831
|
return Object.freeze(statements);
|
|
46860
46832
|
}
|
|
46861
|
-
function skipLineComment(sql, index) {
|
|
46862
|
-
const newline = sql.indexOf("\n", index);
|
|
46863
|
-
return newline < 0 ? sql.length : newline + 1;
|
|
46864
|
-
}
|
|
46865
46833
|
function skipBlockComment(sql, index) {
|
|
46866
46834
|
let depth = 1;
|
|
46867
46835
|
while (index < sql.length) {
|
|
46868
46836
|
if (sql[index] === "/" && sql[index + 1] === "*") {
|
|
46869
46837
|
depth += 1;
|
|
46870
46838
|
index += 2;
|
|
46871
|
-
|
|
46872
|
-
}
|
|
46873
|
-
if (sql[index] === "*" && sql[index + 1] === "/") {
|
|
46839
|
+
} else if (sql[index] === "*" && sql[index + 1] === "/") {
|
|
46874
46840
|
depth -= 1;
|
|
46875
46841
|
index += 2;
|
|
46876
46842
|
if (depth === 0)
|
|
46877
46843
|
return index;
|
|
46878
|
-
|
|
46844
|
+
} else {
|
|
46845
|
+
index += 1;
|
|
46879
46846
|
}
|
|
46880
|
-
index += 1;
|
|
46881
46847
|
}
|
|
46882
46848
|
throw new DatabaseMigrationAdmissionError("invalid");
|
|
46883
46849
|
}
|
|
46884
|
-
function skipQuoted(sql, index, quote) {
|
|
46850
|
+
function skipQuoted(sql, index, quote, escaped) {
|
|
46885
46851
|
while (index < sql.length) {
|
|
46886
|
-
if (sql[index]
|
|
46852
|
+
if (escaped && sql[index] === "\\") {
|
|
46853
|
+
index += 2;
|
|
46854
|
+
} else if (sql[index] === quote) {
|
|
46855
|
+
if (sql[index + 1] === quote) {
|
|
46856
|
+
index += 2;
|
|
46857
|
+
} else {
|
|
46858
|
+
const continuation = quote === "'" ? continuedStringStart(sql, index + 1) : void 0;
|
|
46859
|
+
if (continuation === void 0)
|
|
46860
|
+
return index + 1;
|
|
46861
|
+
index = continuation;
|
|
46862
|
+
}
|
|
46863
|
+
} else {
|
|
46887
46864
|
index += 1;
|
|
46888
|
-
continue;
|
|
46889
46865
|
}
|
|
46890
|
-
|
|
46866
|
+
}
|
|
46867
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
46868
|
+
}
|
|
46869
|
+
function continuedStringStart(sql, index) {
|
|
46870
|
+
let newline = false;
|
|
46871
|
+
while (index < sql.length) {
|
|
46872
|
+
const character = sql[index] ?? "";
|
|
46873
|
+
if (/[ \t\f\v\n\r]/u.test(character)) {
|
|
46874
|
+
newline ||= character === "\n" || character === "\r";
|
|
46875
|
+
index += 1;
|
|
46876
|
+
} else if (character === "-" && sql[index + 1] === "-") {
|
|
46891
46877
|
index += 2;
|
|
46878
|
+
while (index < sql.length && sql[index] !== "\n" && sql[index] !== "\r")
|
|
46879
|
+
index += 1;
|
|
46880
|
+
} else {
|
|
46881
|
+
return character === "'" && newline ? index + 1 : void 0;
|
|
46882
|
+
}
|
|
46883
|
+
}
|
|
46884
|
+
return void 0;
|
|
46885
|
+
}
|
|
46886
|
+
function isSqlKeyword(token, value) {
|
|
46887
|
+
return token?.kind === "word" && token.value === value;
|
|
46888
|
+
}
|
|
46889
|
+
function rejectMigrationSqlReferences(tokens, allowIdentityHelpers = false) {
|
|
46890
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
46891
|
+
const token = tokens[index];
|
|
46892
|
+
if (token?.kind !== "word" && token?.kind !== "identifier")
|
|
46892
46893
|
continue;
|
|
46894
|
+
const value = token.value.toLowerCase();
|
|
46895
|
+
if (value === "ohmyhost") {
|
|
46896
|
+
if (allowIdentityHelpers && token.value === "ohmyhost" && tokens[index + 1]?.kind === "symbol" && tokens[index + 1]?.value === "." && ["user_id", "claims"].includes(tokens[index + 2]?.value ?? "") && ["word", "identifier"].includes(tokens[index + 2]?.kind ?? "") && tokens[index + 3]?.kind === "symbol" && tokens[index + 3]?.value === "(" && tokens[index + 4]?.kind === "symbol" && tokens[index + 4]?.value === ")")
|
|
46897
|
+
continue;
|
|
46898
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
46893
46899
|
}
|
|
46894
|
-
|
|
46900
|
+
const portableAuthTable = tokens[index + 2];
|
|
46901
|
+
if (value === "auth" && tokens[index + 1]?.value === "." && !(portableAuthTable?.kind === "identifier" && ["account", "rateLimit", "ratelimit", "session", "user", "verification"].includes(portableAuthTable.value)) || value === "storage" && tokens[index + 1]?.value === "." || value === "supabase" || (value === "to" || value === "from") && ["anon", "authenticated", "service_role"].includes(tokens[index + 1]?.value ?? ""))
|
|
46902
|
+
throw new DatabaseMigrationAdmissionError("provider_specific");
|
|
46895
46903
|
}
|
|
46896
|
-
throw new DatabaseMigrationAdmissionError("invalid");
|
|
46897
46904
|
}
|
|
46898
|
-
|
|
46899
|
-
|
|
46900
|
-
|
|
46905
|
+
|
|
46906
|
+
// packages/contracts/dist/database-migration-policies.js
|
|
46907
|
+
var PolicyReader = class {
|
|
46908
|
+
tokens;
|
|
46909
|
+
position = 0;
|
|
46910
|
+
constructor(tokens) {
|
|
46911
|
+
this.tokens = tokens;
|
|
46912
|
+
}
|
|
46913
|
+
take(keyword) {
|
|
46914
|
+
if (!isSqlKeyword(this.tokens[this.position], keyword))
|
|
46915
|
+
return false;
|
|
46916
|
+
this.position += 1;
|
|
46917
|
+
return true;
|
|
46918
|
+
}
|
|
46919
|
+
expect(keyword) {
|
|
46920
|
+
if (!this.take(keyword))
|
|
46921
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
46922
|
+
}
|
|
46923
|
+
identifier() {
|
|
46924
|
+
const token = this.tokens[this.position++];
|
|
46925
|
+
if (token?.kind !== "word" && token?.kind !== "identifier" || token.value.length === 0 || new TextEncoder().encode(token.value).byteLength > 63) {
|
|
46926
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
46927
|
+
}
|
|
46928
|
+
return token.value;
|
|
46929
|
+
}
|
|
46930
|
+
target() {
|
|
46931
|
+
const schema = this.identifier();
|
|
46932
|
+
const separator = this.tokens[this.position++];
|
|
46933
|
+
if (schema !== "public" && schema !== "private" || separator?.kind !== "symbol" || separator.value !== ".") {
|
|
46934
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
46935
|
+
}
|
|
46936
|
+
return { schema, table: this.identifier() };
|
|
46937
|
+
}
|
|
46938
|
+
expression() {
|
|
46939
|
+
const opening = this.tokens[this.position++];
|
|
46940
|
+
if (opening?.kind !== "symbol" || opening.value !== "(")
|
|
46941
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
46942
|
+
const start = this.position;
|
|
46943
|
+
let depth = 1;
|
|
46944
|
+
while (this.position < this.tokens.length) {
|
|
46945
|
+
const token = this.tokens[this.position++];
|
|
46946
|
+
if (token?.kind !== "symbol")
|
|
46947
|
+
continue;
|
|
46948
|
+
if (token.value === "(")
|
|
46949
|
+
depth += 1;
|
|
46950
|
+
if (token.value === ")")
|
|
46951
|
+
depth -= 1;
|
|
46952
|
+
if (depth === 0) {
|
|
46953
|
+
if (this.position - 1 === start)
|
|
46954
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
46955
|
+
rejectMigrationSqlReferences(this.tokens.slice(start, this.position - 1), true);
|
|
46956
|
+
return;
|
|
46957
|
+
}
|
|
46958
|
+
}
|
|
46959
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
46960
|
+
}
|
|
46961
|
+
end() {
|
|
46962
|
+
if (this.position !== this.tokens.length)
|
|
46963
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
46964
|
+
}
|
|
46965
|
+
};
|
|
46966
|
+
function readMigrationRlsTarget(tokens) {
|
|
46967
|
+
const first = tokens[0]?.value;
|
|
46968
|
+
const policy = tokens[0]?.kind === "word" && isSqlKeyword(tokens[1], "policy") && ["create", "alter", "drop"].includes(first ?? "");
|
|
46969
|
+
const rls = isSqlKeyword(tokens[0], "alter") && isSqlKeyword(tokens[1], "table") && tokens.some((token, index) => isSqlKeyword(token, "row") && isSqlKeyword(tokens[index + 1], "level") && isSqlKeyword(tokens[index + 2], "security"));
|
|
46970
|
+
if (!policy && !rls)
|
|
46971
|
+
return void 0;
|
|
46972
|
+
const reader = new PolicyReader(tokens);
|
|
46973
|
+
reader.position = 2;
|
|
46974
|
+
if (rls) {
|
|
46975
|
+
rejectMigrationSqlReferences(tokens);
|
|
46976
|
+
const target2 = reader.target();
|
|
46977
|
+
const action = reader.take("enable") ? "enable" : reader.take("force") ? "force" : void 0;
|
|
46978
|
+
if (action === void 0)
|
|
46979
|
+
throw new DatabaseMigrationAdmissionError("destructive");
|
|
46980
|
+
reader.expect("row");
|
|
46981
|
+
reader.expect("level");
|
|
46982
|
+
reader.expect("security");
|
|
46983
|
+
reader.end();
|
|
46984
|
+
return Object.freeze({ ...target2, action });
|
|
46985
|
+
}
|
|
46986
|
+
if (first === "drop" && reader.take("if"))
|
|
46987
|
+
reader.expect("exists");
|
|
46988
|
+
const policyName = reader.identifier();
|
|
46989
|
+
reader.expect("on");
|
|
46990
|
+
const target = reader.target();
|
|
46991
|
+
rejectMigrationSqlReferences(tokens.slice(0, reader.position));
|
|
46992
|
+
if (first === "drop") {
|
|
46993
|
+
reader.take("restrict");
|
|
46994
|
+
reader.end();
|
|
46995
|
+
return Object.freeze({ ...target, policy: policyName, action: "drop" });
|
|
46996
|
+
}
|
|
46997
|
+
if (first === "create" && reader.take("as")) {
|
|
46998
|
+
if (!reader.take("permissive"))
|
|
46999
|
+
reader.expect("restrictive");
|
|
47000
|
+
}
|
|
47001
|
+
if (first === "create" && reader.take("for")) {
|
|
47002
|
+
if (!["all", "select", "insert", "update", "delete"].some((command) => reader.take(command))) {
|
|
47003
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
47004
|
+
}
|
|
47005
|
+
}
|
|
47006
|
+
if (reader.take("to"))
|
|
47007
|
+
reader.expect("public");
|
|
47008
|
+
let expression = false;
|
|
47009
|
+
if (reader.take("using")) {
|
|
47010
|
+
reader.expression();
|
|
47011
|
+
expression = true;
|
|
47012
|
+
}
|
|
47013
|
+
if (reader.take("with")) {
|
|
47014
|
+
reader.expect("check");
|
|
47015
|
+
reader.expression();
|
|
47016
|
+
expression = true;
|
|
47017
|
+
}
|
|
47018
|
+
reader.end();
|
|
47019
|
+
if (first === "alter" && !expression)
|
|
47020
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
47021
|
+
return Object.freeze({
|
|
47022
|
+
...target,
|
|
47023
|
+
policy: policyName,
|
|
47024
|
+
action: first === "create" ? "create" : "alter"
|
|
47025
|
+
});
|
|
46901
47026
|
}
|
|
46902
|
-
|
|
46903
|
-
|
|
47027
|
+
|
|
47028
|
+
// packages/contracts/dist/database-migration-admission.js
|
|
47029
|
+
var MAX_MIGRATION_COUNT = 128;
|
|
47030
|
+
var MAX_MIGRATION_BYTES = 256 * 1024;
|
|
47031
|
+
var MAX_MIGRATION_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
47032
|
+
var MIGRATION_PATH = /^(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*\d{14}_[a-z0-9][a-z0-9_-]*\.sql$/u;
|
|
47033
|
+
function admitExpandOnlyMigrations(migrations) {
|
|
47034
|
+
if (!Array.isArray(migrations) || migrations.length === 0 || migrations.length > MAX_MIGRATION_COUNT) {
|
|
47035
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
47036
|
+
}
|
|
47037
|
+
let previousPath = "";
|
|
47038
|
+
let totalBytes = 0;
|
|
47039
|
+
let statementCount = 0;
|
|
47040
|
+
const files = migrations.map((migration) => {
|
|
47041
|
+
if (typeof migration !== "object" || migration === null || !MIGRATION_PATH.test(migration.path) || migration.path <= previousPath || !(migration.body instanceof Uint8Array) || migration.body.byteLength === 0 || migration.body.byteLength > MAX_MIGRATION_BYTES) {
|
|
47042
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
47043
|
+
}
|
|
47044
|
+
previousPath = migration.path;
|
|
47045
|
+
totalBytes += migration.body.byteLength;
|
|
47046
|
+
if (totalBytes > MAX_MIGRATION_TOTAL_BYTES || migration.body.includes(0)) {
|
|
47047
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
47048
|
+
}
|
|
47049
|
+
let sql;
|
|
47050
|
+
try {
|
|
47051
|
+
sql = new TextDecoder("utf-8", { fatal: true }).decode(migration.body);
|
|
47052
|
+
} catch {
|
|
47053
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
47054
|
+
}
|
|
47055
|
+
if (sql.length === 0 || sql.includes("\r") || sql.charCodeAt(0) === 65279) {
|
|
47056
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
47057
|
+
}
|
|
47058
|
+
const statements = parseDatabaseMigrationStatements(sql);
|
|
47059
|
+
statementCount += statements.length;
|
|
47060
|
+
return Object.freeze({ path: migration.path, statementCount: statements.length });
|
|
47061
|
+
});
|
|
47062
|
+
return Object.freeze({ files: Object.freeze(files), statementCount });
|
|
47063
|
+
}
|
|
47064
|
+
function parseDatabaseMigrationStatements(sql) {
|
|
47065
|
+
if (sql.length === 0 || sql.includes("\0") || sql.includes("\r") || sql.charCodeAt(0) === 65279) {
|
|
47066
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
47067
|
+
}
|
|
47068
|
+
const statements = readMigrationSql(sql);
|
|
47069
|
+
if (statements.length === 0)
|
|
47070
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
47071
|
+
return Object.freeze(statements.map((statement) => {
|
|
47072
|
+
rejectMigrationSqlReferences(statement.tokens, true);
|
|
47073
|
+
const rlsTarget = readMigrationRlsTarget(statement.tokens);
|
|
47074
|
+
if (rlsTarget !== void 0)
|
|
47075
|
+
return Object.freeze({ sql: statement.sql, rlsTarget });
|
|
47076
|
+
rejectMigrationSqlReferences(statement.tokens);
|
|
47077
|
+
admitStatement(statement.tokens, statement.sql);
|
|
47078
|
+
return Object.freeze({ sql: statement.sql });
|
|
47079
|
+
}));
|
|
47080
|
+
}
|
|
47081
|
+
function rejectFunctionBodyReferences(sql) {
|
|
47082
|
+
if (/\bohmyhost\b|\brow\s+level\s+security\b|\b(?:create|alter|drop)\s+policy\b|\bu&['"]/iu.test(sql)) {
|
|
47083
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
47084
|
+
}
|
|
47085
|
+
const normalized = sql.toLowerCase();
|
|
47086
|
+
const withoutPortableAuth = normalized.replace(/(?:\bauth|"auth")\s*\.\s*"(?:account|ratelimit|session|user|verification)"/gu, "");
|
|
47087
|
+
if (/(?:\bauth|"auth")\s*\./u.test(withoutPortableAuth) || /(?:\bstorage|"storage")\s*\./u.test(normalized) || /\b(?:to|from)\s+(?:anon|authenticated|service_role)\b/u.test(normalized) || /\bsupabase\b/u.test(normalized)) {
|
|
47088
|
+
throw new DatabaseMigrationAdmissionError("provider_specific");
|
|
47089
|
+
}
|
|
47090
|
+
}
|
|
47091
|
+
function admitStatement(tokens, sql) {
|
|
47092
|
+
const normalized = tokens.map((token) => token.kind === "string" ? "'literal'" : token.text.toLowerCase()).join(" ").replace(/\s*\.\s*/gu, ".");
|
|
46904
47093
|
if (normalized === "set check_function_bodies = false")
|
|
46905
47094
|
return;
|
|
46906
47095
|
if (/^create\s+function\b/u.test(normalized)) {
|
|
47096
|
+
rejectFunctionBodyReferences(sql);
|
|
46907
47097
|
if (!/\blanguage\s+(?:sql|plpgsql)\b/u.test(normalized) || /\bsecurity\s+definer\b/u.test(normalized) && !/\bset\s+search_path\s*(?:=\s*|to\s+)/u.test(normalized)) {
|
|
46908
47098
|
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
46909
47099
|
}
|
|
@@ -46923,9 +47113,6 @@ function admitStatement(statement) {
|
|
|
46923
47113
|
}
|
|
46924
47114
|
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
46925
47115
|
}
|
|
46926
|
-
function normalizeStatement(statement) {
|
|
46927
|
-
return statement.replace(/--[^\n]*(?:\n|$)/gu, " ").replace(/\/\*[\s\S]*?\*\//gu, " ").trim().replace(/\s+/gu, " ").toLowerCase();
|
|
46928
|
-
}
|
|
46929
47116
|
|
|
46930
47117
|
// apps/product-cli/dist/repository-init.js
|
|
46931
47118
|
var MAXIMUM_PACKAGE_BYTES = 1024 * 1024;
|
|
@@ -47648,7 +47835,7 @@ function repositoryBlockers(packageJson, framework, nextStandaloneOutput, tanSta
|
|
|
47648
47835
|
const unsupported = compatibility.frameworks.filter((entry) => entry.classification === "unsupported").map((entry) => `${FRAMEWORK_PACKAGE[entry.framework]}@${entry.version || "missing"}`).join(", ");
|
|
47649
47836
|
blockers.push({
|
|
47650
47837
|
code: "framework_version_unsupported",
|
|
47651
|
-
message: `${unsupported} is outside the versions ohmyho.st builds: next 15.5.26+ (15.5.x) or 16.3.
|
|
47838
|
+
message: `${unsupported} is outside the versions ohmyho.st builds: next 15.5.26+ (15.5.x) or 16.3.8, vite 5.4.0\u20138.2.2, @tanstack/react-start 1.168.26\u20131.168.49, as an exact version or a ^ or ~ range whose base version is inside. Pin a version in that range in package.json, update the lockfile, commit and push.`
|
|
47652
47839
|
});
|
|
47653
47840
|
}
|
|
47654
47841
|
const scripts = packageJson["scripts"];
|
|
@@ -47686,7 +47873,7 @@ function repositoryBlockers(packageJson, framework, nextStandaloneOutput, tanSta
|
|
|
47686
47873
|
if (!nextConfigured) {
|
|
47687
47874
|
blockers.push({
|
|
47688
47875
|
code: "next_adapter_unconfigured",
|
|
47689
|
-
message: 'Next.js needs scripts.build exactly "next build" or "next build --webpack", react and react-dom at exact versions such as "19.1.0" (no ^ or ~), next within 15.5.26+ (15.5.x) or 16.3.
|
|
47876
|
+
message: 'Next.js needs scripts.build exactly "next build" or "next build --webpack", react and react-dom at exact versions such as "19.1.0" (no ^ or ~), next within 15.5.26+ (15.5.x) or 16.3.8, and exactly one next.config.js, .mjs or .ts, with at most one open-next.config file. ohmyho.st adds the OpenNext build itself; do not make it the build script.'
|
|
47690
47877
|
});
|
|
47691
47878
|
}
|
|
47692
47879
|
}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1242,7 +1242,7 @@ function repositoryBlockers(
|
|
|
1242
1242
|
.join(", ");
|
|
1243
1243
|
blockers.push({
|
|
1244
1244
|
code: "framework_version_unsupported",
|
|
1245
|
-
message: `${unsupported} is outside the versions ohmyho.st builds: next 15.5.26+ (15.5.x) or 16.3.
|
|
1245
|
+
message: `${unsupported} is outside the versions ohmyho.st builds: next 15.5.26+ (15.5.x) or 16.3.8, vite 5.4.0–8.2.2, @tanstack/react-start 1.168.26–1.168.49, as an exact version or a ^ or ~ range whose base version is inside. Pin a version in that range in package.json, update the lockfile, commit and push.`,
|
|
1246
1246
|
});
|
|
1247
1247
|
}
|
|
1248
1248
|
const scripts = packageJson["scripts"];
|
|
@@ -1297,7 +1297,7 @@ function repositoryBlockers(
|
|
|
1297
1297
|
blockers.push({
|
|
1298
1298
|
code: "next_adapter_unconfigured",
|
|
1299
1299
|
message:
|
|
1300
|
-
'Next.js needs scripts.build exactly "next build" or "next build --webpack", react and react-dom at exact versions such as "19.1.0" (no ^ or ~), next within 15.5.26+ (15.5.x) or 16.3.
|
|
1300
|
+
'Next.js needs scripts.build exactly "next build" or "next build --webpack", react and react-dom at exact versions such as "19.1.0" (no ^ or ~), next within 15.5.26+ (15.5.x) or 16.3.8, and exactly one next.config.js, .mjs or .ts, with at most one open-next.config file. ohmyho.st adds the OpenNext build itself; do not make it the build script.',
|
|
1301
1301
|
});
|
|
1302
1302
|
}
|
|
1303
1303
|
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
+
import { DatabaseMigrationAdmissionError } from "./database-migration-errors.js";
|
|
2
|
+
import {
|
|
3
|
+
readMigrationSql,
|
|
4
|
+
rejectMigrationSqlReferences,
|
|
5
|
+
type MigrationSqlToken,
|
|
6
|
+
} from "./database-migration-lexical.js";
|
|
7
|
+
import {
|
|
8
|
+
readMigrationRlsTarget,
|
|
9
|
+
type DatabaseMigrationRlsTarget,
|
|
10
|
+
} from "./database-migration-policies.js";
|
|
11
|
+
|
|
12
|
+
export { DatabaseMigrationAdmissionError } from "./database-migration-errors.js";
|
|
13
|
+
export type { DatabaseMigrationAdmissionReason } from "./database-migration-errors.js";
|
|
14
|
+
export type { DatabaseMigrationRlsTarget } from "./database-migration-policies.js";
|
|
15
|
+
|
|
1
16
|
const MAX_MIGRATION_COUNT = 128;
|
|
2
17
|
const MAX_MIGRATION_BYTES = 256 * 1024;
|
|
3
18
|
const MAX_MIGRATION_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
4
19
|
const MIGRATION_PATH = /^(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*\d{14}_[a-z0-9][a-z0-9_-]*\.sql$/u;
|
|
5
20
|
|
|
6
|
-
export type DatabaseMigrationAdmissionReason =
|
|
7
|
-
| "destructive"
|
|
8
|
-
| "invalid"
|
|
9
|
-
| "provider_specific"
|
|
10
|
-
| "unsupported";
|
|
11
|
-
|
|
12
|
-
export class DatabaseMigrationAdmissionError extends Error {
|
|
13
|
-
public readonly code = "database_migration_not_admitted";
|
|
14
|
-
|
|
15
|
-
public constructor(public readonly reason: DatabaseMigrationAdmissionReason) {
|
|
16
|
-
super("Database migration is not admitted");
|
|
17
|
-
this.name = "DatabaseMigrationAdmissionError";
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
21
|
export interface DatabaseMigrationInput {
|
|
22
22
|
readonly path: string;
|
|
23
23
|
readonly body: Uint8Array;
|
|
@@ -67,127 +67,76 @@ export function admitExpandOnlyMigrations(
|
|
|
67
67
|
if (sql.length === 0 || sql.includes("\r") || sql.charCodeAt(0) === 0xfeff) {
|
|
68
68
|
throw new DatabaseMigrationAdmissionError("invalid");
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
rejectProviderSpecificSql(sql);
|
|
72
|
-
const statements = splitSqlStatements(sql);
|
|
73
|
-
if (statements.length === 0) throw new DatabaseMigrationAdmissionError("invalid");
|
|
74
|
-
for (const statement of statements) admitStatement(statement);
|
|
70
|
+
const statements = parseDatabaseMigrationStatements(sql);
|
|
75
71
|
statementCount += statements.length;
|
|
76
72
|
return Object.freeze({ path: migration.path, statementCount: statements.length });
|
|
77
73
|
});
|
|
78
74
|
return Object.freeze({ files: Object.freeze(files), statementCount });
|
|
79
75
|
}
|
|
80
76
|
|
|
81
|
-
|
|
77
|
+
export interface DatabaseMigrationStatement {
|
|
78
|
+
readonly sql: string;
|
|
79
|
+
readonly rlsTarget?: DatabaseMigrationRlsTarget;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function parseDatabaseMigrationStatements(
|
|
83
|
+
sql: string,
|
|
84
|
+
): readonly DatabaseMigrationStatement[] {
|
|
85
|
+
if (
|
|
86
|
+
sql.length === 0 ||
|
|
87
|
+
sql.includes("\0") ||
|
|
88
|
+
sql.includes("\r") ||
|
|
89
|
+
sql.charCodeAt(0) === 0xfeff
|
|
90
|
+
) {
|
|
91
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
92
|
+
}
|
|
93
|
+
const statements = readMigrationSql(sql);
|
|
94
|
+
if (statements.length === 0) throw new DatabaseMigrationAdmissionError("invalid");
|
|
95
|
+
return Object.freeze(
|
|
96
|
+
statements.map((statement) => {
|
|
97
|
+
// Provider references are forbidden throughout executable policy SQL, while
|
|
98
|
+
// the native helper exception is narrowed by the policy reader below.
|
|
99
|
+
rejectMigrationSqlReferences(statement.tokens, true);
|
|
100
|
+
const rlsTarget = readMigrationRlsTarget(statement.tokens);
|
|
101
|
+
if (rlsTarget !== undefined) return Object.freeze({ sql: statement.sql, rlsTarget });
|
|
102
|
+
rejectMigrationSqlReferences(statement.tokens);
|
|
103
|
+
admitStatement(statement.tokens, statement.sql);
|
|
104
|
+
return Object.freeze({ sql: statement.sql });
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function rejectFunctionBodyReferences(sql: string): void {
|
|
110
|
+
if (
|
|
111
|
+
/\bohmyhost\b|\brow\s+level\s+security\b|\b(?:create|alter|drop)\s+policy\b|\bu&['"]/iu.test(
|
|
112
|
+
sql,
|
|
113
|
+
)
|
|
114
|
+
) {
|
|
115
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
116
|
+
}
|
|
82
117
|
const normalized = sql.toLowerCase();
|
|
83
118
|
const withoutPortableAuth = normalized.replace(
|
|
84
|
-
|
|
119
|
+
/(?:\bauth|"auth")\s*\.\s*"(?:account|ratelimit|session|user|verification)"/gu,
|
|
85
120
|
"",
|
|
86
121
|
);
|
|
87
122
|
if (
|
|
88
|
-
|
|
89
|
-
|
|
123
|
+
/(?:\bauth|"auth")\s*\./u.test(withoutPortableAuth) ||
|
|
124
|
+
/(?:\bstorage|"storage")\s*\./u.test(normalized) ||
|
|
90
125
|
/\b(?:to|from)\s+(?:anon|authenticated|service_role)\b/u.test(normalized) ||
|
|
91
|
-
/\bcreate\s+policy\b/u.test(normalized) ||
|
|
92
|
-
/\brow\s+level\s+security\b/u.test(normalized) ||
|
|
93
126
|
/\bsupabase\b/u.test(normalized)
|
|
94
127
|
) {
|
|
95
128
|
throw new DatabaseMigrationAdmissionError("provider_specific");
|
|
96
129
|
}
|
|
97
130
|
}
|
|
98
131
|
|
|
99
|
-
function
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const character = sql[index];
|
|
105
|
-
const next = sql[index + 1];
|
|
106
|
-
if (character === "-" && next === "-") {
|
|
107
|
-
index = skipLineComment(sql, index + 2);
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if (character === "/" && next === "*") {
|
|
111
|
-
index = skipBlockComment(sql, index + 2);
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
if (character === "'") {
|
|
115
|
-
index = skipQuoted(sql, index + 1, "'");
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (character === '"') {
|
|
119
|
-
index = skipQuoted(sql, index + 1, '"');
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (character === "$") {
|
|
123
|
-
const delimiter = dollarDelimiter(sql, index);
|
|
124
|
-
if (delimiter !== null) {
|
|
125
|
-
const end = sql.indexOf(delimiter, index + delimiter.length);
|
|
126
|
-
if (end < 0) throw new DatabaseMigrationAdmissionError("invalid");
|
|
127
|
-
index = end + delimiter.length;
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (character === ";") {
|
|
132
|
-
const statement = sql.slice(start, index).trim();
|
|
133
|
-
if (statement.length > 0) statements.push(statement);
|
|
134
|
-
start = index + 1;
|
|
135
|
-
}
|
|
136
|
-
index += 1;
|
|
137
|
-
}
|
|
138
|
-
const tail = sql.slice(start).trim();
|
|
139
|
-
if (tail.length > 0) statements.push(tail);
|
|
140
|
-
return Object.freeze(statements);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function skipLineComment(sql: string, index: number): number {
|
|
144
|
-
const newline = sql.indexOf("\n", index);
|
|
145
|
-
return newline < 0 ? sql.length : newline + 1;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function skipBlockComment(sql: string, index: number): number {
|
|
149
|
-
let depth = 1;
|
|
150
|
-
while (index < sql.length) {
|
|
151
|
-
if (sql[index] === "/" && sql[index + 1] === "*") {
|
|
152
|
-
depth += 1;
|
|
153
|
-
index += 2;
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (sql[index] === "*" && sql[index + 1] === "/") {
|
|
157
|
-
depth -= 1;
|
|
158
|
-
index += 2;
|
|
159
|
-
if (depth === 0) return index;
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
index += 1;
|
|
163
|
-
}
|
|
164
|
-
throw new DatabaseMigrationAdmissionError("invalid");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function skipQuoted(sql: string, index: number, quote: "'" | '"'): number {
|
|
168
|
-
while (index < sql.length) {
|
|
169
|
-
if (sql[index] !== quote) {
|
|
170
|
-
index += 1;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (sql[index + 1] === quote) {
|
|
174
|
-
index += 2;
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
return index + 1;
|
|
178
|
-
}
|
|
179
|
-
throw new DatabaseMigrationAdmissionError("invalid");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function dollarDelimiter(sql: string, index: number): string | null {
|
|
183
|
-
const match = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/u.exec(sql.slice(index));
|
|
184
|
-
return match?.[0] ?? null;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function admitStatement(statement: string): void {
|
|
188
|
-
const normalized = normalizeStatement(statement);
|
|
132
|
+
function admitStatement(tokens: readonly MigrationSqlToken[], sql: string): void {
|
|
133
|
+
const normalized = tokens
|
|
134
|
+
.map((token) => (token.kind === "string" ? "'literal'" : token.text.toLowerCase()))
|
|
135
|
+
.join(" ")
|
|
136
|
+
.replace(/\s*\.\s*/gu, ".");
|
|
189
137
|
if (normalized === "set check_function_bodies = false") return;
|
|
190
138
|
if (/^create\s+function\b/u.test(normalized)) {
|
|
139
|
+
rejectFunctionBodyReferences(sql);
|
|
191
140
|
if (
|
|
192
141
|
!/\blanguage\s+(?:sql|plpgsql)\b/u.test(normalized) ||
|
|
193
142
|
(/\bsecurity\s+definer\b/u.test(normalized) &&
|
|
@@ -224,12 +173,3 @@ function admitStatement(statement: string): void {
|
|
|
224
173
|
}
|
|
225
174
|
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
226
175
|
}
|
|
227
|
-
|
|
228
|
-
function normalizeStatement(statement: string): string {
|
|
229
|
-
return statement
|
|
230
|
-
.replace(/--[^\n]*(?:\n|$)/gu, " ")
|
|
231
|
-
.replace(/\/\*[\s\S]*?\*\//gu, " ")
|
|
232
|
-
.trim()
|
|
233
|
-
.replace(/\s+/gu, " ")
|
|
234
|
-
.toLowerCase();
|
|
235
|
-
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type DatabaseMigrationAdmissionReason =
|
|
2
|
+
| "destructive"
|
|
3
|
+
| "invalid"
|
|
4
|
+
| "provider_specific"
|
|
5
|
+
| "unsupported";
|
|
6
|
+
|
|
7
|
+
export class DatabaseMigrationAdmissionError extends Error {
|
|
8
|
+
public readonly code = "database_migration_not_admitted";
|
|
9
|
+
|
|
10
|
+
public constructor(public readonly reason: DatabaseMigrationAdmissionReason) {
|
|
11
|
+
super("Database migration is not admitted");
|
|
12
|
+
this.name = "DatabaseMigrationAdmissionError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { DatabaseMigrationAdmissionError } from "./database-migration-errors.js";
|
|
2
|
+
|
|
3
|
+
export interface MigrationSqlToken {
|
|
4
|
+
readonly kind: "word" | "identifier" | "string" | "symbol";
|
|
5
|
+
readonly value: string;
|
|
6
|
+
readonly text: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface MigrationSqlStatement {
|
|
10
|
+
readonly sql: string;
|
|
11
|
+
readonly tokens: readonly MigrationSqlToken[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** SQL-aware boundaries only; PostgreSQL remains the expression parser. */
|
|
15
|
+
export function readMigrationSql(sql: string): readonly MigrationSqlStatement[] {
|
|
16
|
+
const statements: MigrationSqlStatement[] = [];
|
|
17
|
+
let tokens: MigrationSqlToken[] = [];
|
|
18
|
+
let start = 0;
|
|
19
|
+
let index = 0;
|
|
20
|
+
while (index < sql.length) {
|
|
21
|
+
const character = sql[index] ?? "";
|
|
22
|
+
const next = sql[index + 1];
|
|
23
|
+
if (/\s/u.test(character)) {
|
|
24
|
+
index += 1;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (character === "-" && next === "-") {
|
|
28
|
+
const newline = sql.indexOf("\n", index + 2);
|
|
29
|
+
index = newline < 0 ? sql.length : newline + 1;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (character === "/" && next === "*") {
|
|
33
|
+
index = skipBlockComment(sql, index + 2);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (character === ";") {
|
|
37
|
+
if (tokens.length > 0) {
|
|
38
|
+
statements.push(Object.freeze({ sql: sql.slice(start, index + 1).trim(), tokens }));
|
|
39
|
+
}
|
|
40
|
+
tokens = [];
|
|
41
|
+
start = ++index;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const tokenStart = index;
|
|
45
|
+
let kind: MigrationSqlToken["kind"] = "symbol";
|
|
46
|
+
let value = character;
|
|
47
|
+
if (/[uU]/u.test(character) && next === "&" && /['"]/u.test(sql[index + 2] ?? "")) {
|
|
48
|
+
// Unicode escapes can conceal reserved schema identifiers.
|
|
49
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
50
|
+
}
|
|
51
|
+
if (character === "'" || character === '"' || (/[eE]/u.test(character) && next === "'")) {
|
|
52
|
+
const escaped = character !== "'" && character !== '"';
|
|
53
|
+
const quote = character === '"' ? '"' : "'";
|
|
54
|
+
const quoteStart = index + (escaped ? 1 : 0);
|
|
55
|
+
index = skipQuoted(sql, quoteStart + 1, quote, escaped);
|
|
56
|
+
kind = quote === '"' ? "identifier" : "string";
|
|
57
|
+
value = sql.slice(quoteStart + 1, index - 1).replace(/""/gu, '"');
|
|
58
|
+
} else if (character === "$" && /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/u.test(sql.slice(index))) {
|
|
59
|
+
const delimiter = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/u.exec(sql.slice(index))?.[0] ?? "";
|
|
60
|
+
const end = sql.indexOf(delimiter, index + delimiter.length);
|
|
61
|
+
if (end < 0) throw new DatabaseMigrationAdmissionError("invalid");
|
|
62
|
+
index = end + delimiter.length;
|
|
63
|
+
kind = "string";
|
|
64
|
+
value = sql.slice(tokenStart, index);
|
|
65
|
+
} else if (/[\p{L}_]/u.test(character)) {
|
|
66
|
+
index += 1;
|
|
67
|
+
while (index < sql.length && /[\p{L}\p{N}_$]/u.test(sql[index] ?? "")) index += 1;
|
|
68
|
+
kind = "word";
|
|
69
|
+
value = sql.slice(tokenStart, index).toLowerCase();
|
|
70
|
+
} else {
|
|
71
|
+
index += 1;
|
|
72
|
+
}
|
|
73
|
+
tokens.push(Object.freeze({ kind, value, text: sql.slice(tokenStart, index) }));
|
|
74
|
+
}
|
|
75
|
+
if (tokens.length > 0) statements.push(Object.freeze({ sql: sql.slice(start).trim(), tokens }));
|
|
76
|
+
return Object.freeze(statements);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function skipBlockComment(sql: string, index: number): number {
|
|
80
|
+
let depth = 1;
|
|
81
|
+
while (index < sql.length) {
|
|
82
|
+
if (sql[index] === "/" && sql[index + 1] === "*") {
|
|
83
|
+
depth += 1;
|
|
84
|
+
index += 2;
|
|
85
|
+
} else if (sql[index] === "*" && sql[index + 1] === "/") {
|
|
86
|
+
depth -= 1;
|
|
87
|
+
index += 2;
|
|
88
|
+
if (depth === 0) return index;
|
|
89
|
+
} else {
|
|
90
|
+
index += 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function skipQuoted(sql: string, index: number, quote: string, escaped: boolean): number {
|
|
97
|
+
while (index < sql.length) {
|
|
98
|
+
if (escaped && sql[index] === "\\") {
|
|
99
|
+
index += 2;
|
|
100
|
+
} else if (sql[index] === quote) {
|
|
101
|
+
if (sql[index + 1] === quote) {
|
|
102
|
+
index += 2;
|
|
103
|
+
} else {
|
|
104
|
+
const continuation = quote === "'" ? continuedStringStart(sql, index + 1) : undefined;
|
|
105
|
+
if (continuation === undefined) return index + 1;
|
|
106
|
+
// Keep the initial E-string's escape semantics in every continuation.
|
|
107
|
+
index = continuation;
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
index += 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function continuedStringStart(sql: string, index: number): number | undefined {
|
|
117
|
+
// Match PostgreSQL scan.l quotecontinue with a linear scan: at least one
|
|
118
|
+
// newline, with line comments as whitespace. Block comments end continuation.
|
|
119
|
+
let newline = false;
|
|
120
|
+
while (index < sql.length) {
|
|
121
|
+
const character = sql[index] ?? "";
|
|
122
|
+
if (/[ \t\f\v\n\r]/u.test(character)) {
|
|
123
|
+
newline ||= character === "\n" || character === "\r";
|
|
124
|
+
index += 1;
|
|
125
|
+
} else if (character === "-" && sql[index + 1] === "-") {
|
|
126
|
+
index += 2;
|
|
127
|
+
while (index < sql.length && sql[index] !== "\n" && sql[index] !== "\r") index += 1;
|
|
128
|
+
} else {
|
|
129
|
+
return character === "'" && newline ? index + 1 : undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function isSqlKeyword(token: MigrationSqlToken | undefined, value: string): boolean {
|
|
136
|
+
return token?.kind === "word" && token.value === value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function rejectMigrationSqlReferences(
|
|
140
|
+
tokens: readonly MigrationSqlToken[],
|
|
141
|
+
allowIdentityHelpers = false,
|
|
142
|
+
): void {
|
|
143
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
144
|
+
const token = tokens[index];
|
|
145
|
+
if (token?.kind !== "word" && token?.kind !== "identifier") continue;
|
|
146
|
+
const value = token.value.toLowerCase();
|
|
147
|
+
if (value === "ohmyhost") {
|
|
148
|
+
if (
|
|
149
|
+
allowIdentityHelpers &&
|
|
150
|
+
token.value === "ohmyhost" &&
|
|
151
|
+
tokens[index + 1]?.kind === "symbol" &&
|
|
152
|
+
tokens[index + 1]?.value === "." &&
|
|
153
|
+
["user_id", "claims"].includes(tokens[index + 2]?.value ?? "") &&
|
|
154
|
+
["word", "identifier"].includes(tokens[index + 2]?.kind ?? "") &&
|
|
155
|
+
tokens[index + 3]?.kind === "symbol" &&
|
|
156
|
+
tokens[index + 3]?.value === "(" &&
|
|
157
|
+
tokens[index + 4]?.kind === "symbol" &&
|
|
158
|
+
tokens[index + 4]?.value === ")"
|
|
159
|
+
)
|
|
160
|
+
continue;
|
|
161
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
162
|
+
}
|
|
163
|
+
const portableAuthTable = tokens[index + 2];
|
|
164
|
+
if (
|
|
165
|
+
(value === "auth" &&
|
|
166
|
+
tokens[index + 1]?.value === "." &&
|
|
167
|
+
!(
|
|
168
|
+
portableAuthTable?.kind === "identifier" &&
|
|
169
|
+
["account", "rateLimit", "ratelimit", "session", "user", "verification"].includes(
|
|
170
|
+
portableAuthTable.value,
|
|
171
|
+
)
|
|
172
|
+
)) ||
|
|
173
|
+
(value === "storage" && tokens[index + 1]?.value === ".") ||
|
|
174
|
+
value === "supabase" ||
|
|
175
|
+
((value === "to" || value === "from") &&
|
|
176
|
+
["anon", "authenticated", "service_role"].includes(tokens[index + 1]?.value ?? ""))
|
|
177
|
+
)
|
|
178
|
+
throw new DatabaseMigrationAdmissionError("provider_specific");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { DatabaseMigrationAdmissionError } from "./database-migration-errors.js";
|
|
2
|
+
import {
|
|
3
|
+
isSqlKeyword,
|
|
4
|
+
rejectMigrationSqlReferences,
|
|
5
|
+
type MigrationSqlToken,
|
|
6
|
+
} from "./database-migration-lexical.js";
|
|
7
|
+
|
|
8
|
+
export interface DatabaseMigrationRlsTarget {
|
|
9
|
+
readonly schema: "public" | "private";
|
|
10
|
+
readonly table: string;
|
|
11
|
+
readonly policy?: string;
|
|
12
|
+
readonly action: "create" | "alter" | "drop" | "enable" | "force";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class PolicyReader {
|
|
16
|
+
public position = 0;
|
|
17
|
+
|
|
18
|
+
public constructor(private readonly tokens: readonly MigrationSqlToken[]) {}
|
|
19
|
+
|
|
20
|
+
public take(keyword: string): boolean {
|
|
21
|
+
if (!isSqlKeyword(this.tokens[this.position], keyword)) return false;
|
|
22
|
+
this.position += 1;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public expect(keyword: string): void {
|
|
27
|
+
if (!this.take(keyword)) throw new DatabaseMigrationAdmissionError("unsupported");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public identifier(): string {
|
|
31
|
+
const token = this.tokens[this.position++];
|
|
32
|
+
if (
|
|
33
|
+
(token?.kind !== "word" && token?.kind !== "identifier") ||
|
|
34
|
+
token.value.length === 0 ||
|
|
35
|
+
new TextEncoder().encode(token.value).byteLength > 63
|
|
36
|
+
) {
|
|
37
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
38
|
+
}
|
|
39
|
+
return token.value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public target(): Readonly<{ schema: "public" | "private"; table: string }> {
|
|
43
|
+
const schema = this.identifier();
|
|
44
|
+
const separator = this.tokens[this.position++];
|
|
45
|
+
if (
|
|
46
|
+
(schema !== "public" && schema !== "private") ||
|
|
47
|
+
separator?.kind !== "symbol" ||
|
|
48
|
+
separator.value !== "."
|
|
49
|
+
) {
|
|
50
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
51
|
+
}
|
|
52
|
+
return { schema, table: this.identifier() };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public expression(): void {
|
|
56
|
+
const opening = this.tokens[this.position++];
|
|
57
|
+
if (opening?.kind !== "symbol" || opening.value !== "(")
|
|
58
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
59
|
+
const start = this.position;
|
|
60
|
+
let depth = 1;
|
|
61
|
+
while (this.position < this.tokens.length) {
|
|
62
|
+
const token = this.tokens[this.position++];
|
|
63
|
+
if (token?.kind !== "symbol") continue;
|
|
64
|
+
if (token.value === "(") depth += 1;
|
|
65
|
+
if (token.value === ")") depth -= 1;
|
|
66
|
+
if (depth === 0) {
|
|
67
|
+
if (this.position - 1 === start) throw new DatabaseMigrationAdmissionError("invalid");
|
|
68
|
+
rejectMigrationSqlReferences(this.tokens.slice(start, this.position - 1), true);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw new DatabaseMigrationAdmissionError("invalid");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public end(): void {
|
|
76
|
+
if (this.position !== this.tokens.length)
|
|
77
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function readMigrationRlsTarget(
|
|
82
|
+
tokens: readonly MigrationSqlToken[],
|
|
83
|
+
): DatabaseMigrationRlsTarget | undefined {
|
|
84
|
+
const first = tokens[0]?.value;
|
|
85
|
+
const policy =
|
|
86
|
+
tokens[0]?.kind === "word" &&
|
|
87
|
+
isSqlKeyword(tokens[1], "policy") &&
|
|
88
|
+
["create", "alter", "drop"].includes(first ?? "");
|
|
89
|
+
const rls =
|
|
90
|
+
isSqlKeyword(tokens[0], "alter") &&
|
|
91
|
+
isSqlKeyword(tokens[1], "table") &&
|
|
92
|
+
tokens.some(
|
|
93
|
+
(token, index) =>
|
|
94
|
+
isSqlKeyword(token, "row") &&
|
|
95
|
+
isSqlKeyword(tokens[index + 1], "level") &&
|
|
96
|
+
isSqlKeyword(tokens[index + 2], "security"),
|
|
97
|
+
);
|
|
98
|
+
if (!policy && !rls) return undefined;
|
|
99
|
+
// Header and table identifiers must never use the reserved namespace. Helpers
|
|
100
|
+
// are admitted separately, only inside balanced policy expressions.
|
|
101
|
+
const reader = new PolicyReader(tokens);
|
|
102
|
+
reader.position = 2;
|
|
103
|
+
if (rls) {
|
|
104
|
+
rejectMigrationSqlReferences(tokens);
|
|
105
|
+
const target = reader.target();
|
|
106
|
+
const action = reader.take("enable") ? "enable" : reader.take("force") ? "force" : undefined;
|
|
107
|
+
if (action === undefined) throw new DatabaseMigrationAdmissionError("destructive");
|
|
108
|
+
reader.expect("row");
|
|
109
|
+
reader.expect("level");
|
|
110
|
+
reader.expect("security");
|
|
111
|
+
reader.end();
|
|
112
|
+
return Object.freeze({ ...target, action });
|
|
113
|
+
}
|
|
114
|
+
if (first === "drop" && reader.take("if")) reader.expect("exists");
|
|
115
|
+
const policyName = reader.identifier();
|
|
116
|
+
reader.expect("on");
|
|
117
|
+
const target = reader.target();
|
|
118
|
+
rejectMigrationSqlReferences(tokens.slice(0, reader.position));
|
|
119
|
+
if (first === "drop") {
|
|
120
|
+
reader.take("restrict");
|
|
121
|
+
reader.end();
|
|
122
|
+
return Object.freeze({ ...target, policy: policyName, action: "drop" });
|
|
123
|
+
}
|
|
124
|
+
if (first === "create" && reader.take("as")) {
|
|
125
|
+
if (!reader.take("permissive")) reader.expect("restrictive");
|
|
126
|
+
}
|
|
127
|
+
if (first === "create" && reader.take("for")) {
|
|
128
|
+
if (!["all", "select", "insert", "update", "delete"].some((command) => reader.take(command))) {
|
|
129
|
+
throw new DatabaseMigrationAdmissionError("unsupported");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (reader.take("to")) reader.expect("public");
|
|
133
|
+
let expression = false;
|
|
134
|
+
if (reader.take("using")) {
|
|
135
|
+
reader.expression();
|
|
136
|
+
expression = true;
|
|
137
|
+
}
|
|
138
|
+
if (reader.take("with")) {
|
|
139
|
+
reader.expect("check");
|
|
140
|
+
reader.expression();
|
|
141
|
+
expression = true;
|
|
142
|
+
}
|
|
143
|
+
reader.end();
|
|
144
|
+
if (first === "alter" && !expression) throw new DatabaseMigrationAdmissionError("unsupported");
|
|
145
|
+
return Object.freeze({
|
|
146
|
+
...target,
|
|
147
|
+
policy: policyName,
|
|
148
|
+
action: first === "create" ? "create" : "alter",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -129,7 +129,7 @@ export interface FrameworkVersionAdmission {
|
|
|
129
129
|
const VERIFIED_VERSIONS: Readonly<Record<AdmittedFramework, ReadonlySet<string>>> = Object.freeze({
|
|
130
130
|
vite: new Set(["5.4.21", "8.0.16", "8.2.2"]),
|
|
131
131
|
"tanstack-start": new Set(["1.168.26", "1.168.49"]),
|
|
132
|
-
nextjs: new Set(["15.5.26", "16.3.
|
|
132
|
+
nextjs: new Set(["15.5.26", "16.3.8"]),
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
export function classifyFrameworkVersion(
|
|
@@ -14,8 +14,7 @@ export function isAdmittedNextVersion(version) {
|
|
|
14
14
|
const patch = Number(match[3]);
|
|
15
15
|
if (![major, minor, patch].every(Number.isSafeInteger)) return false;
|
|
16
16
|
return (
|
|
17
|
-
(major === 15 && minor === 5 && patch >= 26) ||
|
|
18
|
-
(major === 16 && minor === 3 && patch >= 6 && patch <= 7)
|
|
17
|
+
(major === 15 && minor === 5 && patch >= 26) || (major === 16 && minor === 3 && patch === 8)
|
|
19
18
|
);
|
|
20
19
|
}
|
|
21
20
|
|