sc-dev-deploy/scripts/installDevDeployTenant.js
2026-06-01 16:43:43 -05:00

99 lines
4.8 KiB
JavaScript

// Clean per-tenant installer for dev-deploy on the Postgres multi-tenant
// instance. Registers + enables the plugin in each named tenant schema and runs
// its onLoad (creating the _dd_* tables + bootstrapping the env), using
// Saltcorn's supported Plugin.loadAndSaveNewPlugin API inside runWithTenant --
// replacing the old manual "INSERT INTO <tenant>._sc_plugins" SQL hack.
//
// The CLI `install-plugin -t <tenant> -d <dir>` cannot do this: a local (-d)
// plugin is "unsafe" on a non-root tenant, so loadAndSaveNewPlugin returns
// before the upsert unless allowUnsafeOnTenantsWithoutConfigSetting (its 5th arg)
// is set, and the CLI never passes it. This script passes it.
//
// Usage (from project root, PG env sourced -- see installDevDeployTenant.sh):
// node dev-deploy/scripts/installDevDeployTenant.js t1 t2 (or '*' for all tenants)
const { createRequire } = require("node:module");
const path = require("node:path");
// Re-root @saltcorn/* against the Saltcorn checkout's node_modules. This file
// lives at dev-deploy/scripts/, so up 2 = project root, then saltcorn/packages/...
const scRequire = createRequire(path.join(__dirname, "..", "..", "saltcorn", "packages", "saltcorn-data", "package.json"));
const Plugin = scRequire("@saltcorn/data/models/plugin");
const db = scRequire("@saltcorn/data/db");
const { init_multi_tenant, getRootState } = scRequire("@saltcorn/data/db/state");
const { getAllTenants } = scRequire("@saltcorn/admin-models/models/tenant");
const PLUGIN_NAME = "dev-deploy";
const DEV_DEPLOY_DIR = path.resolve(__dirname, "..");
const installInto = async (tenant) => {
await db.runWithTenant(tenant, async () => {
await db.withTransaction(async () => {
// Remove any prior rows (including the old manual-hack row and earlier
// installs) so we converge on exactly one _sc_plugins row -- no
// duplicate source of truth.
await db.deleteWhere("_sc_plugins", { name: PLUGIN_NAME });
const plugin = new Plugin({ name: PLUGIN_NAME, source: "local", location: DEV_DEPLOY_DIR, configuration: {} });
await Plugin.loadAndSaveNewPlugin(plugin, true, false);
});
// Verify against dev-deploy's own table so a stale row can't pass.
const row = await db.selectMaybeOne("_sc_plugins", { name: PLUGIN_NAME });
const svc = await db.query(
"SELECT 1 FROM information_schema.tables WHERE table_schema = $1 AND table_name = '_dd_env'",
[db.getTenantSchema()]
);
const hasSvc = svc && svc.rows && svc.rows.length > 0;
// eslint-disable-next-line no-console
console.log(`[installDevDeployTenant] ${tenant}: _sc_plugins=${row ? "yes" : "NO"} _dd_env=${hasSvc ? "yes" : "NO"}`);
if (!row || !hasSvc) {
throw new Error("install verification failed for tenant " + tenant + " (onLoad did not run)");
}
});
};
const main = async () => {
await Plugin.loadAllPlugins();
// Resolve the target tenants (from the public _sc_tenants list).
let tenants = process.argv.slice(2);
if (tenants.length === 0 || (tenants.length === 1 && tenants[0] === "*")) {
const all = await db.runWithTenant(db.connectObj.default_schema, getAllTenants);
tenants = (all || []).map((t) => (typeof t === "string" ? t : t.subdomain)).filter(Boolean);
}
if (!tenants || tenants.length === 0) {
// eslint-disable-next-line no-console
console.error("[installDevDeployTenant] no tenants to install into");
process.exit(1);
}
// Initialize per-tenant State (so getState() resolves inside runWithTenant)
// without running migrations. This also runs each tenant's existing plugins'
// onLoad, which is itself idempotent.
await init_multi_tenant(Plugin.loadAllPlugins, true, tenants);
// Permit installing this LOCAL plugin into tenant schemas. In this Saltcorn
// build loadAndSaveNewPlugin skips any non-"npm" plugin on a non-root tenant
// BEFORE the allowUnsafe arg is consulted; the supported lever is this
// root-only config -- the intended setting for a multi-tenant deployment that
// offers the dev-deploy plugin to its tenants.
await getRootState().setConfig("tenants_unsafe_plugins", true);
// eslint-disable-next-line no-console
console.log("[installDevDeployTenant] installing " + PLUGIN_NAME + " into: " + tenants.join(", "));
for (const t of tenants) {
await installInto(t);
}
// eslint-disable-next-line no-console
console.log("[installDevDeployTenant] done");
process.exit(0);
};
main().catch((e) => {
// eslint-disable-next-line no-console
console.error("[installDevDeployTenant] ERROR:", e && (e.stack || e.message || e));
process.exit(1);
});