mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: give plugins their own tables, migrations and sync entities
This commit is contained in:
1 parent
5796186a1c
commit
c496a093ca
47 files changed
+32595
-290
No files matched your search
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { readManifest } from "../lib/plugin-dir.mjs";
|
||||
import { LEGACY_TABLE_OWNERS as LEGACY_TABLES } from "../../dist/db.js";
|
||||
|
||||
/**
|
||||
* The manifest rules live in src/manifest.ts, which the server uses too, so
|
||||
@@ -35,6 +36,8 @@ export async function validate({ cwd }) {
|
||||
}
|
||||
}
|
||||
|
||||
problems.push(...validateMigrations(cwd, raw.id ?? path.basename(cwd)));
|
||||
|
||||
if (problems.length > 0) {
|
||||
for (const problem of problems) console.error(` ${problem}`);
|
||||
throw new Error(`${raw.id ?? path.basename(cwd)}: manifest is not valid.`);
|
||||
@@ -42,3 +45,77 @@ export async function validate({ cwd }) {
|
||||
|
||||
console.log(`ok ${raw.id ?? path.basename(cwd)}`);
|
||||
}
|
||||
|
||||
const DIALECTS = ["sqlite", "postgres", "mysql"];
|
||||
|
||||
const CREATE_OR_ALTER =
|
||||
/(?:CREATE\s+(?:TEMP\s+|TEMPORARY\s+)?TABLE(?:\s+IF\s+NOT\s+EXISTS)?|ALTER\s+TABLE|DROP\s+TABLE(?:\s+IF\s+EXISTS)?|TRUNCATE\s+TABLE)\s+([`"']?)([a-zA-Z0-9_]+)\1/gi;
|
||||
|
||||
/**
|
||||
* Checks that a plugin's migrations only touch tables it owns.
|
||||
*
|
||||
* The server enforces this too, because a plugin installed from a tarball
|
||||
* never ran this command. Here it is a build-time error with a file name
|
||||
* attached, rather than a plugin that fails to activate later.
|
||||
*/
|
||||
function validateMigrations(cwd, pluginId) {
|
||||
const problems = [];
|
||||
const prefix = `p_${String(pluginId).replace(/-/g, "_")}_`;
|
||||
const root = path.join(cwd, "migrations");
|
||||
if (!fs.existsSync(root)) return problems;
|
||||
|
||||
const present = DIALECTS.filter((dialect) =>
|
||||
fs.existsSync(path.join(root, dialect)),
|
||||
);
|
||||
|
||||
const byDialect = new Map();
|
||||
for (const dialect of present) {
|
||||
const dir = path.join(root, dialect);
|
||||
const files = fs
|
||||
.readdirSync(dir)
|
||||
.filter((file) => file.endsWith(".sql"))
|
||||
.sort();
|
||||
byDialect.set(dialect, files);
|
||||
|
||||
for (const file of files) {
|
||||
if (!/^\d{4}_[a-z0-9_]+\.sql$/.test(file)) {
|
||||
problems.push(
|
||||
`migrations/${dialect}/${file} must be named NNNN_name.sql, lower snake_case`,
|
||||
);
|
||||
}
|
||||
|
||||
const sql = fs.readFileSync(path.join(dir, file), "utf8");
|
||||
CREATE_OR_ALTER.lastIndex = 0;
|
||||
let match;
|
||||
while ((match = CREATE_OR_ALTER.exec(sql)) !== null) {
|
||||
const table = match[2];
|
||||
if (table.startsWith(prefix)) continue;
|
||||
if (LEGACY_TABLES[table] === pluginId) continue;
|
||||
problems.push(
|
||||
`migrations/${dialect}/${file} touches "${table}", which is not prefixed "${prefix}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A migration that exists for one engine and not another leaves that
|
||||
// deployment without the table, which is how alert_rules ended up
|
||||
// SQLite-only in core.
|
||||
const [first, ...rest] = present;
|
||||
for (const dialect of rest) {
|
||||
const a = byDialect.get(first);
|
||||
const b = byDialect.get(dialect);
|
||||
for (const file of a) {
|
||||
if (!b.includes(file)) {
|
||||
problems.push(`migrations/${dialect}/${file} is missing`);
|
||||
}
|
||||
}
|
||||
for (const file of b) {
|
||||
if (!a.includes(file)) {
|
||||
problems.push(`migrations/${first}/${file} is missing`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return problems;
|
||||
}
|
||||
Reference in new issue
Block a user