@paroicms/internal-server-lib
Version:
Common utilitaries for the paroicms server.
239 lines • 8.21 kB
JavaScript
import { type } from "arktype";
import knex from "knex";
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { pathExists } from "./fs-utils.js";
export const METADATA_TABLE_NAME = "PaMetadata";
export const DB_SCHEMA_VERSION_KEY = "dbSchemaVersion";
export async function createOrOpenSqliteConnection(options) {
const { sqliteFile, ddlFile, dbSchemaName, canCreate, migrateDb, logger, knexLogger, ensureDependsOn, } = options;
const isNewDatabase = !(await pathExists(sqliteFile));
if (isNewDatabase && !canCreate) {
throw new Error(`missing '${dbSchemaName}' database`);
}
const cn = knex({
client: "sqlite3",
connection: {
filename: sqliteFile,
},
useNullAsDefault: true,
debug: true, // process.env.NODE_ENV === "development"
asyncStackTraces: true, // process.env.NODE_ENV === "development"
log: knexLogger,
pool: {
afterCreate: (sqliteCn, done) => {
// Enable foreign keys in SQLite
sqliteCn.run("PRAGMA foreign_keys = 1", done);
},
min: 0,
idleTimeoutMillis: 30_000,
},
});
let wasReset = false;
let shouldInitializeMetadata = false;
if (isNewDatabase) {
await executeDdl(cn, ddlFile);
logger.info(`created '${dbSchemaName}' database`);
shouldInitializeMetadata = true;
}
let migrationReport = await migrateDb(cn);
if (migrationReport.migrated && isNewDatabase) {
throw new Error(`unexpected migration of newly created '${dbSchemaName}' database from version ${migrationReport.fromVersion} to ${migrationReport.schemaVersion}`);
}
if (ensureDependsOn && !isNewDatabase) {
const storedDependsOn = await getMetadataValue(cn, {
dbSchemaName,
key: "dependsOn",
});
if (storedDependsOn !== ensureDependsOn.dependsOn) {
if (ensureDependsOn.onMismatch === "error") {
throw new Error(`[${dbSchemaName}] dependsOn mismatch: expected '${ensureDependsOn.dependsOn}', found '${storedDependsOn}'`);
}
logger.info(`[${dbSchemaName}] Reset database due to '${ensureDependsOn.dependsOn}', previous value was '${storedDependsOn}'`);
await executeDdl(cn, ddlFile, { dropAll: true });
wasReset = true;
shouldInitializeMetadata = true;
migrationReport = await migrateDb(cn);
}
}
let databaseId;
if (shouldInitializeMetadata) {
const inserted = await insertNewDatabaseMetadataValues(cn, {
dbSchemaName,
dependsOn: ensureDependsOn?.dependsOn,
});
databaseId = inserted.databaseId;
}
else {
databaseId = await getMetadataValue(cn, {
dbSchemaName,
key: "databaseId",
});
if (databaseId === undefined) {
throw new Error(`[${dbSchemaName}] missing 'databaseId' in '${METADATA_TABLE_NAME}'`);
}
}
return {
cn,
newDatabase: isNewDatabase ? "created" : wasReset ? "reset" : false,
migrationReport,
databaseId,
};
}
export async function insertNewDatabaseMetadataValues(cn, options) {
const { dbSchemaName, dependsOn } = options;
const databaseId = generateDatabaseId();
await setMetadataValue(cn, {
dbSchemaName,
key: "databaseId",
value: databaseId,
});
if (dependsOn) {
await setMetadataValue(cn, {
dbSchemaName,
key: "dependsOn",
value: dependsOn,
});
}
return { databaseId };
}
export function generateDatabaseId() {
return randomUUID();
}
const DropAllUserTablesRowAT = type({
name: "string",
type: "string",
"+": "reject",
});
export async function executeDdl(cn, ddlFile, { dropAll } = {}) {
if (dropAll) {
await dropAllUserTables(cn);
}
const ddl = await readFile(ddlFile, {
encoding: "utf-8",
});
const queries = ddl
.split(";")
.map((query) => query.trim())
.filter((query) => query !== "");
for (const query of queries) {
if (!onlySqlComment(query)) {
await cn.raw(query);
}
}
}
export async function dropAllUserTables(cn) {
await cn.raw("pragma foreign_keys = 0");
try {
const rows = await cn("sqlite_master")
.select("name", "type")
.whereIn("type", ["table", "view"])
.where("name", "not like", "sqlite_%");
for (const row of rows) {
const { name, type } = DropAllUserTablesRowAT.assert(row);
const quotedName = name.replace(/"/g, '"');
if (type === "view") {
await cn.raw(`drop view if exists "${quotedName}"`);
}
else {
await cn.raw(`drop table if exists "${quotedName}"`);
}
}
const sqliteSequenceExists = await cn("sqlite_master")
.select("name")
.where({ type: "table", name: "sqlite_sequence" })
.first();
if (sqliteSequenceExists) {
await cn.raw("delete from sqlite_sequence");
}
}
finally {
await cn.raw("pragma foreign_keys = 1");
}
}
function onlySqlComment(sql) {
const lines = sql.split(/\r\n?|\n/);
for (let line of lines) {
line = line.trim();
if (line && !line.startsWith("--"))
return false;
}
return true;
}
const GetMetadataValueAT = type({ val: "string", "+": "reject" });
export async function getMetadataValue(cn, { dbSchemaName, key }) {
const row = await cn(METADATA_TABLE_NAME)
.select("val")
.where({ dbSchema: dbSchemaName, k: key })
.first();
if (row) {
return GetMetadataValueAT.assert(row).val;
}
}
export async function setMetadataValue(cn, { dbSchemaName, key, value, errorIfMissing, }) {
const affectedRows = await cn(METADATA_TABLE_NAME)
.where({ dbSchema: dbSchemaName, k: key })
.update({ val: value });
if (affectedRows === 0) {
if (errorIfMissing) {
throw new Error(`[${dbSchemaName}] missing '${key}' in '${METADATA_TABLE_NAME}'`);
}
await cn(METADATA_TABLE_NAME).insert({
dbSchema: dbSchemaName,
k: key,
val: value,
});
}
}
export async function getMetadataDbSchemaVersion(cn, { dbSchemaName }) {
let value;
try {
value = await getMetadataValue(cn, { dbSchemaName, key: DB_SCHEMA_VERSION_KEY });
if (value === undefined) {
throw new Error(`[${dbSchemaName}] missing '${DB_SCHEMA_VERSION_KEY}' in '${METADATA_TABLE_NAME}'`);
}
}
catch (error) {
try {
value = await migrateMetadataTable(cn, dbSchemaName);
}
catch {
throw error;
}
}
return Number(value);
}
export async function setMetadataDbSchemaVersion(cn, { dbSchemaName, value }) {
await setMetadataValue(cn, {
dbSchemaName,
key: DB_SCHEMA_VERSION_KEY,
value: String(value),
errorIfMissing: true,
});
}
const MigrateMetadataTableAT = type({ dbVersion: "number", "+": "reject" });
// Note: deprecated table
async function migrateMetadataTable(cn, dbSchemaName) {
const row = await cn("PaDbMetadata")
.select("dbVersion")
.where({ dbSchema: dbSchemaName })
.first();
if (!row)
throw new Error(); // will be catched in the caller
const version = MigrateMetadataTableAT.assert(row).dbVersion;
await cn.raw(`create table PaMetadata (
dbSchema varchar(100) not null,
k varchar(100) not null,
val varchar(250) not null,
primary key (dbSchema, k)
)`);
await cn(METADATA_TABLE_NAME).insert({
dbSchema: dbSchemaName,
k: "dbSchemaVersion",
val: String(version),
});
await cn.schema.dropTable("PaDbMetadata");
console.warn(`[${dbSchemaName}] … migrated 'PaDbMetadata' table to 'PaMetadata'`);
return String(version);
}
//# sourceMappingURL=sqlite-db-init.js.map