chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { hash } from "@node-rs/argon2";
|
||||
import { SystemRole } from "@planarchy/shared";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { loadWorkspaceEnv, resolveWorkspacePath } from "./load-workspace-env.js";
|
||||
|
||||
loadWorkspaceEnv();
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const DEFAULT_BACKUP_DIR = resolveWorkspacePath("packages", "db", "backups");
|
||||
|
||||
interface ResetOptions {
|
||||
force: boolean;
|
||||
skipBackup: boolean;
|
||||
backupDir: string;
|
||||
adminEmail: string;
|
||||
adminPassword: string;
|
||||
adminName: string;
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): ResetOptions {
|
||||
const options: ResetOptions = {
|
||||
force: false,
|
||||
skipBackup: false,
|
||||
backupDir: DEFAULT_BACKUP_DIR,
|
||||
adminEmail: "admin@planarchy.dev",
|
||||
adminPassword: "admin123",
|
||||
adminName: "Planarchy Admin",
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index];
|
||||
|
||||
if (argument === "--force") {
|
||||
options.force = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument === "--skip-backup") {
|
||||
options.skipBackup = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument === "--backup-dir") {
|
||||
options.backupDir = resolve(argv[index + 1] ?? DEFAULT_BACKUP_DIR);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument === "--admin-email") {
|
||||
options.adminEmail = argv[index + 1] ?? options.adminEmail;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument === "--admin-password") {
|
||||
options.adminPassword = argv[index + 1] ?? options.adminPassword;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument === "--admin-name") {
|
||||
options.adminName = argv[index + 1] ?? options.adminName;
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function createTimestamp() {
|
||||
return new Date().toISOString().replace(/[:.]/g, "-");
|
||||
}
|
||||
|
||||
function quoteIdentifier(identifier: string): string {
|
||||
return `"${identifier.replace(/"/g, "\"\"")}"`;
|
||||
}
|
||||
|
||||
function createDatabaseBackup(databaseUrl: string, backupDir: string): string {
|
||||
mkdirSync(backupDir, { recursive: true });
|
||||
|
||||
const backupPath = resolve(backupDir, `dispo-reset-${createTimestamp()}.dump`);
|
||||
execFileSync(
|
||||
"pg_dump",
|
||||
["--format=custom", "--file", backupPath, databaseUrl],
|
||||
{
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
},
|
||||
);
|
||||
|
||||
return backupPath;
|
||||
}
|
||||
|
||||
async function listPublicTables() {
|
||||
return prisma.$queryRaw<Array<{ tablename: string }>>`
|
||||
SELECT tablename
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
AND tablename <> '_prisma_migrations'
|
||||
ORDER BY tablename ASC
|
||||
`;
|
||||
}
|
||||
|
||||
async function truncatePublicTables() {
|
||||
const tables = await listPublicTables();
|
||||
if (tables.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const quotedTables = tables.map((table) => quoteIdentifier(table.tablename)).join(", ");
|
||||
await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${quotedTables} RESTART IDENTITY CASCADE`);
|
||||
return tables.map((table) => table.tablename);
|
||||
}
|
||||
|
||||
async function bootstrapPlatform(adminEmail: string, adminPassword: string, adminName: string) {
|
||||
const passwordHash = await hash(adminPassword);
|
||||
|
||||
const admin = await prisma.user.create({
|
||||
data: {
|
||||
email: adminEmail,
|
||||
name: adminName,
|
||||
passwordHash,
|
||||
systemRole: SystemRole.ADMIN,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.systemSettings.upsert({
|
||||
where: { id: "singleton" },
|
||||
update: {
|
||||
vacationDefaultDays: 28,
|
||||
},
|
||||
create: {
|
||||
id: "singleton",
|
||||
vacationDefaultDays: 28,
|
||||
},
|
||||
});
|
||||
|
||||
return admin;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
|
||||
if (!options.force) {
|
||||
throw new Error("Refusing to reset the database without --force.");
|
||||
}
|
||||
|
||||
if (!databaseUrl) {
|
||||
throw new Error("DATABASE_URL is not configured.");
|
||||
}
|
||||
|
||||
let backupPath: string | null = null;
|
||||
|
||||
if (options.skipBackup) {
|
||||
console.warn("Skipping pg_dump backup because --skip-backup was provided.");
|
||||
} else {
|
||||
try {
|
||||
backupPath = createDatabaseBackup(databaseUrl, options.backupDir);
|
||||
console.log(`Backup created at ${backupPath}`);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Backup failed. Install pg_dump or rerun with --skip-backup if this is an intentional disposable environment.\n${String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const truncatedTables = await truncatePublicTables();
|
||||
console.log(`Truncated ${truncatedTables.length} public tables.`);
|
||||
|
||||
const admin = await bootstrapPlatform(options.adminEmail, options.adminPassword, options.adminName);
|
||||
console.log(`Bootstrap admin created: ${admin.email}`);
|
||||
|
||||
if (backupPath) {
|
||||
console.log(`Database backup: ${backupPath}`);
|
||||
}
|
||||
|
||||
console.log("Dispo import reset/bootstrap complete.");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user