Files
Ferraz Fagundes 8c22597420
ci / lint, typecheck, testes e build (push) Canceled after 0s
ci / Postgres e Redis reais (push) Canceled after 0s
ci / imagem Docker Linux (push) Canceled after 0s
fix(db): protege conexoes e migrations de homologacao
2026-10-19 16:32:03 -03:00

135 lines
5.7 KiB
TypeScript

import { readFile } from "node:fs/promises";
import path from "node:path";
import { adapterEnv, isProductionEnv } from "../src/config/env";
import { migrate } from "../src/infra/db/migrate";
import type { Db } from "../src/infra/db/types";
import { bootstrapScript, describeTarget, openOperationalDb, runScript } from "./_bootstrap";
/**
* Seed idempotente de homologação.
*
* Não cria conta, senha ou sessão: as identidades vivem no Supabase Auth do
* projeto de homologação e são criadas pela operação no painel do Supabase.
* Este script apenas liga aqueles `user_id` já existentes a workspaces e
* papéis. Rodar duas vezes produz o mesmo estado.
*
* Arquivo: `ADAPTER_STAGING_SEED_FILE` (padrão `config/staging-seed.json`).
* Modelo sem segredo em `config/staging-seed.example.json`.
*/
type SeedWorkspace = { id: string; name: string; moduleEnabled: boolean; allowNonAdminModelUpload?: boolean };
type SeedUser = { id: string; displayName: string; isSuperadmin?: boolean };
type SeedMember = { workspaceId: string; userId: string; role: "owner" | "admin" | "gestor" | "agent" };
type SeedFile = { workspaces: SeedWorkspace[]; users: SeedUser[]; members: SeedMember[] };
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const ROLES = new Set(["owner", "admin", "gestor", "agent"]);
function assertSeed(seed: SeedFile): void {
const workspaceIds = new Set<string>();
for (const workspace of seed.workspaces) {
if (!UUID_RE.test(workspace.id)) throw new Error(`workspace.id não é UUID: ${workspace.id}`);
if (!workspace.name?.trim()) throw new Error(`workspace ${workspace.id} sem nome`);
workspaceIds.add(workspace.id.toLowerCase());
}
const userIds = new Set<string>();
for (const user of seed.users) {
if (!UUID_RE.test(user.id)) {
throw new Error(`users[].id deve ser o UUID do usuário no Supabase Auth: ${user.id}`);
}
if (!user.displayName?.trim()) throw new Error(`usuário ${user.id} sem displayName`);
userIds.add(user.id.toLowerCase());
}
for (const member of seed.members) {
if (!ROLES.has(member.role)) throw new Error(`papel inválido: ${member.role}`);
if (!workspaceIds.has(member.workspaceId.toLowerCase())) {
throw new Error(`membro aponta workspace fora do seed: ${member.workspaceId}`);
}
if (!userIds.has(member.userId.toLowerCase())) {
throw new Error(`membro aponta usuário fora do seed: ${member.userId}`);
}
}
}
async function applySeed(db: Db, seed: SeedFile): Promise<void> {
await db.withService(async (query) => {
for (const workspace of seed.workspaces) {
await query.query(
`insert into public.workspaces (id, name) values ($1, $2)
on conflict (id) do update set name = excluded.name`,
[workspace.id, workspace.name],
);
await query.query(
`insert into public.workspace_module_flags (workspace_id, module_key, enabled)
values ($1, 'spreadsheet_adapter', $2)
on conflict (workspace_id, module_key) do update set enabled = excluded.enabled, updated_at = now()`,
[workspace.id, workspace.moduleEnabled],
);
if (workspace.allowNonAdminModelUpload !== undefined) {
await query.query(
`insert into public.spreadsheet_adapter_settings (workspace_id, allow_non_admin_model_upload)
values ($1, $2)
on conflict (workspace_id) do update set allow_non_admin_model_upload = excluded.allow_non_admin_model_upload`,
[workspace.id, workspace.allowNonAdminModelUpload],
);
}
}
for (const user of seed.users) {
await query.query(
`insert into public.profiles (id, display_name, is_superadmin) values ($1, $2, $3)
on conflict (id) do update set display_name = excluded.display_name, is_superadmin = excluded.is_superadmin`,
[user.id, user.displayName, user.isSuperadmin ?? false],
);
}
for (const member of seed.members) {
await query.query(
`insert into public.workspace_members (workspace_id, user_id, role) values ($1, $2, $3)
on conflict (workspace_id, user_id) do update set role = excluded.role`,
[member.workspaceId, member.userId, member.role],
);
}
});
}
bootstrapScript("seed-staging");
if (isProductionEnv()) {
console.error("seed-staging não roda em produção. As associações de produção são da plataforma Elo Pro.");
process.exit(1);
}
if (adapterEnv() === "development" && process.env.ADAPTER_SEED_FORCE !== "1") {
console.error("Em development use `npm run seed`. Para forçar este seed, defina ADAPTER_SEED_FORCE=1.");
process.exit(1);
}
const file = process.env.ADAPTER_STAGING_SEED_FILE ?? path.join("config", "staging-seed.json");
runScript("seed-staging", async () => {
let seed: SeedFile;
try {
seed = JSON.parse(await readFile(path.resolve(process.cwd(), file), "utf8")) as SeedFile;
} catch (error) {
console.error(
`Não foi possível ler o seed em "${file}". Copie config/staging-seed.example.json e preencha com os UUID do Supabase Auth.`,
);
console.error(error instanceof Error ? error.name : "erro");
process.exit(1);
}
seed.workspaces ??= [];
seed.users ??= [];
seed.members ??= [];
assertSeed(seed);
const db = await openOperationalDb("admin");
try {
const ran = await migrate(db);
if (ran.length > 0) console.log(`migrations aplicadas antes do seed: ${ran.join(", ")}`);
await applySeed(db, seed);
console.log(
`seed de homologação aplicado em ${describeTarget()}: ${seed.workspaces.length} workspace(s), ${seed.users.length} identidade(s), ${seed.members.length} associação(ões)`,
);
} finally {
await db.close().catch(() => undefined);
}
});