124 lines
3.7 KiB
JavaScript
124 lines
3.7 KiB
JavaScript
// Hidden _dd_row_uuid column infrastructure for managed/starter tables.
|
|
//
|
|
// When an admin marks a user table as managed or starter, this module
|
|
// adds a TEXT column _dd_row_uuid to the underlying SQL table via raw ALTER
|
|
// (NOT registered in _sc_fields, so Saltcorn's table builder doesn't show
|
|
// it). Existing rows are backfilled with random UUIDs. From that point,
|
|
// the wrap layer reads/writes _dd_row_uuid as the cross-environment identity
|
|
// for each row.
|
|
|
|
const crypto = require("crypto");
|
|
|
|
const db = require("@saltcorn/data/db");
|
|
|
|
|
|
const COLUMN_NAME = "_dd_row_uuid";
|
|
|
|
|
|
const tableSqlRef = (tableName) => {
|
|
const schema = db.getTenantSchemaPrefix();
|
|
return `${schema}"${db.sqlsanitize(tableName)}"`;
|
|
};
|
|
|
|
|
|
const columnExists = async (tableName) => {
|
|
if (db.isSQLite) {
|
|
const rs = await db.query(`PRAGMA table_info("${db.sqlsanitize(tableName)}")`);
|
|
return rs.rows.some((r) => r.name === COLUMN_NAME);
|
|
}
|
|
const rs = await db.query(
|
|
`SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = current_schema()
|
|
AND table_name = $1
|
|
AND column_name = $2`,
|
|
[tableName, COLUMN_NAME]
|
|
);
|
|
return rs.rows.length > 0;
|
|
};
|
|
|
|
|
|
const ensureManagedSchema = async (tableName) => {
|
|
if (await columnExists(tableName)) {
|
|
return { added: false };
|
|
}
|
|
await db.query(`ALTER TABLE ${tableSqlRef(tableName)} ADD COLUMN ${COLUMN_NAME} TEXT`);
|
|
// Backfill existing rows with random UUIDs.
|
|
const rs = await db.query(`SELECT id FROM ${tableSqlRef(tableName)} WHERE ${COLUMN_NAME} IS NULL`);
|
|
let backfilled = 0;
|
|
for (const r of rs.rows) {
|
|
await db.query(
|
|
`UPDATE ${tableSqlRef(tableName)} SET ${COLUMN_NAME} = $1 WHERE id = $2`,
|
|
[crypto.randomUUID(), r.id]
|
|
);
|
|
backfilled++;
|
|
}
|
|
// Index for fast lookups by uuid.
|
|
await db.query(
|
|
`CREATE INDEX IF NOT EXISTS "${db.sqlsanitize(tableName)}_dd_row_uuid_idx"
|
|
ON ${tableSqlRef(tableName)} (${COLUMN_NAME})`
|
|
).catch(() => {});
|
|
return { added: true, backfilled: backfilled };
|
|
};
|
|
|
|
|
|
const dropManagedSchema = async (tableName) => {
|
|
if (!(await columnExists(tableName))) {
|
|
return { dropped: false };
|
|
}
|
|
// SQLite 3.35+ and Postgres both support DROP COLUMN. If the SQLite is
|
|
// older it will throw — surface the error.
|
|
await db.query(`ALTER TABLE ${tableSqlRef(tableName)} DROP COLUMN ${COLUMN_NAME}`);
|
|
return { dropped: true };
|
|
};
|
|
|
|
|
|
const getRowUuid = async (tableName, id) => {
|
|
const rs = await db.query(
|
|
`SELECT ${COLUMN_NAME} AS uuid FROM ${tableSqlRef(tableName)} WHERE id = $1`,
|
|
[id]
|
|
);
|
|
return rs.rows.length > 0 ? rs.rows[0].uuid : null;
|
|
};
|
|
|
|
|
|
const findIdByRowUuid = async (tableName, uuid) => {
|
|
const rs = await db.query(
|
|
`SELECT id FROM ${tableSqlRef(tableName)} WHERE ${COLUMN_NAME} = $1`,
|
|
[uuid]
|
|
);
|
|
return rs.rows.length > 0 ? rs.rows[0].id : null;
|
|
};
|
|
|
|
|
|
const setRowUuid = async (tableName, id, uuid) => {
|
|
await db.query(
|
|
`UPDATE ${tableSqlRef(tableName)} SET ${COLUMN_NAME} = $1 WHERE id = $2`,
|
|
[uuid, id]
|
|
);
|
|
};
|
|
|
|
|
|
const newRowUuid = () => crypto.randomUUID();
|
|
|
|
|
|
// All rows currently in the table, including their _dd_row_uuid. Used during
|
|
// the initial managed/starter ship to journal an insert_row op for each.
|
|
const allRowsWithUuid = async (tableName) => {
|
|
const rs = await db.query(
|
|
`SELECT * FROM ${tableSqlRef(tableName)} ORDER BY id ASC`
|
|
);
|
|
return rs.rows;
|
|
};
|
|
|
|
|
|
module.exports = {
|
|
COLUMN_NAME,
|
|
columnExists,
|
|
ensureManagedSchema,
|
|
dropManagedSchema,
|
|
getRowUuid,
|
|
findIdByRowUuid,
|
|
setRowUuid,
|
|
newRowUuid,
|
|
allRowsWithUuid
|
|
};
|