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(); 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(); 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 { 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); } });