export-import.ts75 lines · main
| 1 | import { brivenError, NotFoundError } from '@briven/shared'; |
| 2 | import { eq } from 'drizzle-orm'; |
| 3 | |
| 4 | import { getDb } from '../db/client.js'; |
| 5 | import { deployments, projects } from '../db/schema.js'; |
| 6 | import { getCurrentDeployment, getDeploymentBundle } from './deployments.js'; |
| 7 | |
| 8 | /** |
| 9 | * Project export — schema + functions, bundled as a single JSON object. |
| 10 | * |
| 11 | * Phase 2 first slice. Data movement (pg_dump streaming) is a follow-up |
| 12 | * gated on the data-plane shell-token path; this slice ships only what's |
| 13 | * already in the meta-DB so it can land without touching the data plane. |
| 14 | * |
| 15 | * Wire format is `briven-export.v1.json`: |
| 16 | * { |
| 17 | * manifest: { version: 1, sourceProjectId, sourceDeploymentId, exportedAt } |
| 18 | * schema: SchemaSnapshot — the SchemaDef shape from @briven/schema |
| 19 | * functions: { [filename]: source } — keys are relative paths under briven/functions/ |
| 20 | * } |
| 21 | * |
| 22 | * `briven import` POSTs this back into a target project's `/v1/projects/:id/deployments`. |
| 23 | */ |
| 24 | |
| 25 | export const EXPORT_VERSION = 1 as const; |
| 26 | |
| 27 | export interface ProjectExport { |
| 28 | readonly manifest: { |
| 29 | readonly version: typeof EXPORT_VERSION; |
| 30 | readonly sourceProjectId: string; |
| 31 | readonly sourceProjectName: string; |
| 32 | readonly sourceDeploymentId: string; |
| 33 | readonly exportedAt: string; |
| 34 | }; |
| 35 | readonly schema: Record<string, unknown> | null; |
| 36 | readonly functions: Readonly<Record<string, string>>; |
| 37 | } |
| 38 | |
| 39 | export async function buildProjectExport(projectId: string): Promise<ProjectExport> { |
| 40 | const db = getDb(); |
| 41 | const [project] = await db |
| 42 | .select({ id: projects.id, name: projects.name }) |
| 43 | .from(projects) |
| 44 | .where(eq(projects.id, projectId)) |
| 45 | .limit(1); |
| 46 | if (!project) throw new NotFoundError('project', projectId); |
| 47 | |
| 48 | const current = await getCurrentDeployment(projectId); |
| 49 | if (!current) { |
| 50 | throw new brivenError( |
| 51 | 'no_deployment', |
| 52 | 'project has no eligible deployment to export — deploy at least once first', |
| 53 | { status: 409 }, |
| 54 | ); |
| 55 | } |
| 56 | |
| 57 | const [snapshotRow] = await db |
| 58 | .select({ schemaSnapshot: deployments.schemaSnapshot }) |
| 59 | .from(deployments) |
| 60 | .where(eq(deployments.id, current.id)) |
| 61 | .limit(1); |
| 62 | const bundle = (await getDeploymentBundle(current.id)) ?? {}; |
| 63 | |
| 64 | return { |
| 65 | manifest: { |
| 66 | version: EXPORT_VERSION, |
| 67 | sourceProjectId: project.id, |
| 68 | sourceProjectName: project.name, |
| 69 | sourceDeploymentId: current.id, |
| 70 | exportedAt: new Date().toISOString(), |
| 71 | }, |
| 72 | schema: (snapshotRow?.schemaSnapshot as Record<string, unknown> | null) ?? null, |
| 73 | functions: bundle, |
| 74 | }; |
| 75 | } |