@topogram/cli 0.3.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +67 -0
- package/CHANGELOG.md +240 -0
- package/README.md +223 -0
- package/package.json +51 -0
- package/src/adoption/index.js +3 -0
- package/src/adoption/plan.js +702 -0
- package/src/adoption/reporting.js +464 -0
- package/src/adoption/review-groups.js +313 -0
- package/src/agent-ops/query-builders.js +5012 -0
- package/src/archive/archive.js +141 -0
- package/src/archive/compact.js +26 -0
- package/src/archive/jsonl.js +70 -0
- package/src/archive/resolver-bridge.js +82 -0
- package/src/archive/schema.js +87 -0
- package/src/archive/unarchive.js +108 -0
- package/src/catalog.js +752 -0
- package/src/cli/catalog-alias.js +166 -0
- package/src/cli.js +9738 -0
- package/src/component-behavior.js +173 -0
- package/src/example-implementation.js +91 -0
- package/src/format.js +19 -0
- package/src/generator/adapters.d.ts +4 -0
- package/src/generator/adapters.js +325 -0
- package/src/generator/api.d.ts +1 -0
- package/src/generator/api.js +1196 -0
- package/src/generator/check.js +355 -0
- package/src/generator/component-conformance.js +767 -0
- package/src/generator/components.js +39 -0
- package/src/generator/context/bundle.js +291 -0
- package/src/generator/context/diff.js +256 -0
- package/src/generator/context/digest.js +182 -0
- package/src/generator/context/domain-coverage.js +94 -0
- package/src/generator/context/domain-page.js +137 -0
- package/src/generator/context/index.js +42 -0
- package/src/generator/context/report.js +121 -0
- package/src/generator/context/shared.js +1397 -0
- package/src/generator/context/slice.js +703 -0
- package/src/generator/context/task-mode.js +466 -0
- package/src/generator/docs.js +327 -0
- package/src/generator/index.js +161 -0
- package/src/generator/native/parity-bundle.js +311 -0
- package/src/generator/output.js +300 -0
- package/src/generator/registry.js +482 -0
- package/src/generator/runtime/app-bundle.js +456 -0
- package/src/generator/runtime/bundle-shared.js +166 -0
- package/src/generator/runtime/compile-check.js +163 -0
- package/src/generator/runtime/deployment.js +287 -0
- package/src/generator/runtime/environment.js +635 -0
- package/src/generator/runtime/index.js +32 -0
- package/src/generator/runtime/runtime-check.js +554 -0
- package/src/generator/runtime/shared.js +515 -0
- package/src/generator/runtime/smoke.js +219 -0
- package/src/generator/schema.js +204 -0
- package/src/generator/sdlc/board.js +66 -0
- package/src/generator/sdlc/doc-page.js +53 -0
- package/src/generator/sdlc/index.js +23 -0
- package/src/generator/sdlc/release-notes.js +62 -0
- package/src/generator/sdlc/traceability-matrix.js +65 -0
- package/src/generator/shared.js +29 -0
- package/src/generator/surfaces/contracts.js +146 -0
- package/src/generator/surfaces/databases/contract.js +40 -0
- package/src/generator/surfaces/databases/index.js +84 -0
- package/src/generator/surfaces/databases/lifecycle-shared.d.ts +1 -0
- package/src/generator/surfaces/databases/lifecycle-shared.js +612 -0
- package/src/generator/surfaces/databases/migration-plan.js +281 -0
- package/src/generator/surfaces/databases/postgres/capabilities.js +14 -0
- package/src/generator/surfaces/databases/postgres/drizzle.js +99 -0
- package/src/generator/surfaces/databases/postgres/index.js +9 -0
- package/src/generator/surfaces/databases/postgres/lifecycle.js +16 -0
- package/src/generator/surfaces/databases/postgres/prisma.js +159 -0
- package/src/generator/surfaces/databases/postgres/sql-migration.js +102 -0
- package/src/generator/surfaces/databases/postgres/sql-schema.js +34 -0
- package/src/generator/surfaces/databases/shared.d.ts +1 -0
- package/src/generator/surfaces/databases/shared.js +350 -0
- package/src/generator/surfaces/databases/snapshot.js +96 -0
- package/src/generator/surfaces/databases/sqlite/capabilities.js +14 -0
- package/src/generator/surfaces/databases/sqlite/index.js +8 -0
- package/src/generator/surfaces/databases/sqlite/lifecycle.js +16 -0
- package/src/generator/surfaces/databases/sqlite/prisma.js +143 -0
- package/src/generator/surfaces/databases/sqlite/sql-migration.js +65 -0
- package/src/generator/surfaces/databases/sqlite/sql-schema.js +27 -0
- package/src/generator/surfaces/index.js +25 -0
- package/src/generator/surfaces/native/swiftui-app.js +38 -0
- package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +20 -0
- package/src/generator/surfaces/native/swiftui-templates/README.generated.md +26 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +682 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/TodoAPIClient.swift +156 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/TodoSwiftUIApp.swift +44 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/Visibility.swift +183 -0
- package/src/generator/surfaces/services/express.d.ts +1 -0
- package/src/generator/surfaces/services/express.js +766 -0
- package/src/generator/surfaces/services/hono.d.ts +1 -0
- package/src/generator/surfaces/services/hono.js +204 -0
- package/src/generator/surfaces/services/index.js +42 -0
- package/src/generator/surfaces/services/persistence-wiring.js +240 -0
- package/src/generator/surfaces/services/runtime-helpers.js +631 -0
- package/src/generator/surfaces/services/server-contract.js +80 -0
- package/src/generator/surfaces/services/stateless.d.ts +1 -0
- package/src/generator/surfaces/services/stateless.js +97 -0
- package/src/generator/surfaces/shared.js +64 -0
- package/src/generator/surfaces/web/api-client.js +1 -0
- package/src/generator/surfaces/web/forms.js +1 -0
- package/src/generator/surfaces/web/index.d.ts +2 -0
- package/src/generator/surfaces/web/index.js +53 -0
- package/src/generator/surfaces/web/react-components.js +248 -0
- package/src/generator/surfaces/web/react.js +538 -0
- package/src/generator/surfaces/web/routes.js +1 -0
- package/src/generator/surfaces/web/screens.js +1 -0
- package/src/generator/surfaces/web/shared.js +369 -0
- package/src/generator/surfaces/web/sveltekit-actions.js +28 -0
- package/src/generator/surfaces/web/sveltekit-components.js +234 -0
- package/src/generator/surfaces/web/sveltekit.js +426 -0
- package/src/generator/surfaces/web/ui-web-contract.js +65 -0
- package/src/generator/surfaces/web/vanilla.js +239 -0
- package/src/generator/verification.js +84 -0
- package/src/generator.js +1 -0
- package/src/import/core/context.js +52 -0
- package/src/import/core/contracts.js +23 -0
- package/src/import/core/registry.js +81 -0
- package/src/import/core/runner.js +646 -0
- package/src/import/core/shared.js +910 -0
- package/src/import/enrichers/auth-session.js +18 -0
- package/src/import/enrichers/django-rest.js +226 -0
- package/src/import/enrichers/doc-linking.js +20 -0
- package/src/import/enrichers/rails-controllers.js +246 -0
- package/src/import/enrichers/rails-models.js +130 -0
- package/src/import/enrichers/workflow-target-state.js +10 -0
- package/src/import/extractors/api/aspnet-core.js +304 -0
- package/src/import/extractors/api/django-routes.js +318 -0
- package/src/import/extractors/api/express.js +154 -0
- package/src/import/extractors/api/fastify.js +371 -0
- package/src/import/extractors/api/flutter-dio.js +135 -0
- package/src/import/extractors/api/generic-route-fallback.js +90 -0
- package/src/import/extractors/api/graphql-code-first.js +565 -0
- package/src/import/extractors/api/graphql-sdl.js +309 -0
- package/src/import/extractors/api/jaxrs.js +303 -0
- package/src/import/extractors/api/micronaut.js +213 -0
- package/src/import/extractors/api/next-route.js +50 -0
- package/src/import/extractors/api/next-server-action.js +51 -0
- package/src/import/extractors/api/nextauth.js +52 -0
- package/src/import/extractors/api/openapi-code.js +242 -0
- package/src/import/extractors/api/openapi.js +232 -0
- package/src/import/extractors/api/rails-routes.js +230 -0
- package/src/import/extractors/api/react-native-repository.js +128 -0
- package/src/import/extractors/api/retrofit.js +103 -0
- package/src/import/extractors/api/spring-web.js +372 -0
- package/src/import/extractors/api/swift-webapi.js +116 -0
- package/src/import/extractors/api/trpc.js +212 -0
- package/src/import/extractors/db/django-models.js +232 -0
- package/src/import/extractors/db/dotnet-models.js +93 -0
- package/src/import/extractors/db/drizzle.js +242 -0
- package/src/import/extractors/db/ef-core.js +221 -0
- package/src/import/extractors/db/flutter-entities.js +120 -0
- package/src/import/extractors/db/jpa.js +120 -0
- package/src/import/extractors/db/liquibase.js +180 -0
- package/src/import/extractors/db/mybatis-xml.js +145 -0
- package/src/import/extractors/db/prisma.js +185 -0
- package/src/import/extractors/db/rails-schema.js +175 -0
- package/src/import/extractors/db/react-native-entities.js +95 -0
- package/src/import/extractors/db/room.js +193 -0
- package/src/import/extractors/db/snapshot.js +112 -0
- package/src/import/extractors/db/sql.js +180 -0
- package/src/import/extractors/db/swiftdata.js +137 -0
- package/src/import/extractors/ui/android-compose.js +230 -0
- package/src/import/extractors/ui/backend-only.js +70 -0
- package/src/import/extractors/ui/blazor.js +227 -0
- package/src/import/extractors/ui/flutter-screens.js +152 -0
- package/src/import/extractors/ui/maui-xaml.js +135 -0
- package/src/import/extractors/ui/next-app-router.js +83 -0
- package/src/import/extractors/ui/next-pages-router.js +141 -0
- package/src/import/extractors/ui/razor-pages.js +181 -0
- package/src/import/extractors/ui/react-native-screens.js +166 -0
- package/src/import/extractors/ui/react-router.js +139 -0
- package/src/import/extractors/ui/sveltekit.js +123 -0
- package/src/import/extractors/ui/swiftui.js +193 -0
- package/src/import/extractors/ui/uikit.js +175 -0
- package/src/import/extractors/verification/generic.js +290 -0
- package/src/import/extractors/workflows/generic.js +137 -0
- package/src/import/index.js +7 -0
- package/src/import/provenance.js +158 -0
- package/src/new-project.js +2107 -0
- package/src/parser.js +439 -0
- package/src/policy/review-boundaries.js +165 -0
- package/src/project-config.js +535 -0
- package/src/proofs/backend-parity.js +19 -0
- package/src/proofs/contract-audit.js +220 -0
- package/src/proofs/ios-parity.js +7 -0
- package/src/proofs/issues-parity.js +10 -0
- package/src/proofs/web-parity.js +50 -0
- package/src/realization/api/build-api-realization.js +5 -0
- package/src/realization/api/index.js +1 -0
- package/src/realization/backend/build-backend-runtime-realization.js +82 -0
- package/src/realization/backend/index.d.ts +1 -0
- package/src/realization/backend/index.js +4 -0
- package/src/realization/db/build-db-realization.js +17 -0
- package/src/realization/db/index.js +3 -0
- package/src/realization/db/migration-plan.js +5 -0
- package/src/realization/db/snapshot.js +5 -0
- package/src/realization/ui/build-ui-shared-realization.js +305 -0
- package/src/realization/ui/build-web-realization.js +189 -0
- package/src/realization/ui/index.js +2 -0
- package/src/reconcile/docs.js +280 -0
- package/src/reconcile/index.js +3 -0
- package/src/reconcile/journeys.js +441 -0
- package/src/resolver/docs.js +1 -0
- package/src/resolver/enrich/acceptance-criterion.js +14 -0
- package/src/resolver/enrich/bug.js +12 -0
- package/src/resolver/enrich/component.js +2 -0
- package/src/resolver/enrich/index.js +1 -0
- package/src/resolver/enrich/pitch.js +18 -0
- package/src/resolver/enrich/requirement.js +20 -0
- package/src/resolver/enrich/task.js +16 -0
- package/src/resolver/expressions.js +1 -0
- package/src/resolver/index.js +2422 -0
- package/src/resolver/normalize.js +1 -0
- package/src/resolver.js +1 -0
- package/src/sdlc/adopt.js +65 -0
- package/src/sdlc/check.js +86 -0
- package/src/sdlc/dod/acceptance-criterion.js +22 -0
- package/src/sdlc/dod/bug.js +26 -0
- package/src/sdlc/dod/document.js +23 -0
- package/src/sdlc/dod/index.js +25 -0
- package/src/sdlc/dod/pitch.js +23 -0
- package/src/sdlc/dod/requirement.js +34 -0
- package/src/sdlc/dod/task.js +39 -0
- package/src/sdlc/explain.js +116 -0
- package/src/sdlc/history.js +80 -0
- package/src/sdlc/paths.js +11 -0
- package/src/sdlc/release.js +106 -0
- package/src/sdlc/scaffold.js +89 -0
- package/src/sdlc/status-filter.js +54 -0
- package/src/sdlc/transition.js +112 -0
- package/src/sdlc/transitions/acceptance-criterion.js +28 -0
- package/src/sdlc/transitions/bug.js +31 -0
- package/src/sdlc/transitions/document.js +29 -0
- package/src/sdlc/transitions/index.js +56 -0
- package/src/sdlc/transitions/pitch.js +34 -0
- package/src/sdlc/transitions/requirement.js +31 -0
- package/src/sdlc/transitions/task.js +34 -0
- package/src/template-trust.js +597 -0
- package/src/validator/expressions.js +1 -0
- package/src/validator/index.js +3424 -0
- package/src/validator/kinds.js +346 -0
- package/src/validator/per-kind/acceptance-criterion.js +91 -0
- package/src/validator/per-kind/bug.js +77 -0
- package/src/validator/per-kind/component.js +274 -0
- package/src/validator/per-kind/domain.js +205 -0
- package/src/validator/per-kind/pitch.js +101 -0
- package/src/validator/per-kind/requirement.js +75 -0
- package/src/validator/per-kind/task.js +96 -0
- package/src/validator/registry.js +1 -0
- package/src/validator/utils.js +12 -0
- package/src/validator.js +1 -0
- package/src/workflows.js +7597 -0
- package/src/workspace-docs.js +265 -0
- package/template-helpers/react.js +5 -0
- package/template-helpers/sveltekit.js +5 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildDbProjectionContract,
|
|
3
|
+
dbProfileForProjection,
|
|
4
|
+
dbProjectionCandidates,
|
|
5
|
+
generatorDefaultsMap,
|
|
6
|
+
getProjection
|
|
7
|
+
} from "./shared.js";
|
|
8
|
+
import { normalizeDbSchemaSnapshot } from "./snapshot.js";
|
|
9
|
+
import { generatePostgresDrizzleSchema } from "./postgres/drizzle.js";
|
|
10
|
+
import { generatePostgresPrismaSchema } from "./postgres/prisma.js";
|
|
11
|
+
import { generateSqlitePrismaSchema } from "./sqlite/prisma.js";
|
|
12
|
+
|
|
13
|
+
function defaultInputPathForGraph(graph, options = {}) {
|
|
14
|
+
if (options.topogramInputPath) {
|
|
15
|
+
return options.topogramInputPath;
|
|
16
|
+
}
|
|
17
|
+
return ".";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function dbLifecyclePlan(graph, projection, options = {}) {
|
|
21
|
+
const contract = buildDbProjectionContract(graph, projection);
|
|
22
|
+
const snapshot = normalizeDbSchemaSnapshot(contract);
|
|
23
|
+
const engine = snapshot.engine;
|
|
24
|
+
const ormProfiles = engine === "postgres" ? ["prisma", "drizzle"] : ["prisma"];
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
type: "db_lifecycle_plan",
|
|
28
|
+
projection: snapshot.projection,
|
|
29
|
+
engine,
|
|
30
|
+
dbProfile: contract.profile,
|
|
31
|
+
ormProfiles,
|
|
32
|
+
runtimeProfile: ormProfiles.includes("prisma") ? "prisma" : null,
|
|
33
|
+
inputPath: defaultInputPathForGraph(graph, options),
|
|
34
|
+
state: {
|
|
35
|
+
currentSnapshot: "state/current.snapshot.json",
|
|
36
|
+
desiredSnapshot: "state/desired.snapshot.json",
|
|
37
|
+
migrationPlan: "state/migration.plan.json",
|
|
38
|
+
migrationSql: "state/migration.sql"
|
|
39
|
+
},
|
|
40
|
+
bundle: {
|
|
41
|
+
emptySnapshot: "snapshots/empty.snapshot.json",
|
|
42
|
+
prismaSchema: ormProfiles.includes("prisma") ? "prisma/schema.prisma" : null,
|
|
43
|
+
drizzleSchema: engine === "postgres" ? "drizzle/schema.ts" : null
|
|
44
|
+
},
|
|
45
|
+
environment: {
|
|
46
|
+
required: ["DATABASE_URL"],
|
|
47
|
+
optional:
|
|
48
|
+
engine === "postgres"
|
|
49
|
+
? ["DATABASE_ADMIN_URL", "TOPOGRAM_BIN", "TOPOGRAM_INPUT_PATH", "TOPOGRAM_DB_STATE_DIR"]
|
|
50
|
+
: ["TOPOGRAM_BIN", "TOPOGRAM_INPUT_PATH", "TOPOGRAM_DB_STATE_DIR"]
|
|
51
|
+
},
|
|
52
|
+
greenfield: {
|
|
53
|
+
detection: "missing current snapshot",
|
|
54
|
+
provisioning:
|
|
55
|
+
engine === "postgres"
|
|
56
|
+
? "create database when DATABASE_ADMIN_URL is available, otherwise assume DATABASE_URL already points at a provisioned database"
|
|
57
|
+
: "create sqlite file if missing",
|
|
58
|
+
setup: [
|
|
59
|
+
"generate desired schema snapshot",
|
|
60
|
+
"generate initial SQL migration from the bundled empty snapshot",
|
|
61
|
+
"apply SQL to the target database",
|
|
62
|
+
...(ormProfiles.includes("prisma") ? ["run prisma generate against the bundled schema"] : []),
|
|
63
|
+
"persist desired snapshot as current"
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
brownfield: {
|
|
67
|
+
detection: "existing current snapshot",
|
|
68
|
+
setup: [
|
|
69
|
+
"generate desired schema snapshot",
|
|
70
|
+
"diff current snapshot against desired snapshot",
|
|
71
|
+
"stop if manual intervention is required",
|
|
72
|
+
"generate additive SQL migration",
|
|
73
|
+
"apply SQL to the target database",
|
|
74
|
+
...(ormProfiles.includes("prisma") ? ["run prisma generate against the bundled schema"] : []),
|
|
75
|
+
"persist desired snapshot as current"
|
|
76
|
+
],
|
|
77
|
+
safety: "manual migration required plans are never auto-applied"
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function renderEmptySnapshotForProjection(projection) {
|
|
83
|
+
const engine = projection.platform === "db_sqlite" ? "sqlite" : "postgres";
|
|
84
|
+
return {
|
|
85
|
+
type: "db_schema_snapshot",
|
|
86
|
+
projection: {
|
|
87
|
+
id: projection.id,
|
|
88
|
+
name: projection.name || projection.id,
|
|
89
|
+
platform: projection.platform
|
|
90
|
+
},
|
|
91
|
+
profile: dbProfileForProjection(projection),
|
|
92
|
+
generatorDefaults: generatorDefaultsMap(projection),
|
|
93
|
+
engine,
|
|
94
|
+
enums: [],
|
|
95
|
+
tables: []
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderDbLifecycleEnvExample(projection, plan) {
|
|
100
|
+
const engine = projection.platform === "db_sqlite" ? "sqlite" : "postgres";
|
|
101
|
+
const inputPath = "../../../../topogram";
|
|
102
|
+
if (engine === "sqlite") {
|
|
103
|
+
return `DATABASE_URL=file:./var/${projection.id}.sqlite\nTOPOGRAM_INPUT_PATH=${inputPath}\n`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return `POSTGRES_USER=\${USER:-postgres}\nDATABASE_URL=postgresql://\${POSTGRES_USER}@localhost:5432/${projection.id}\nDATABASE_ADMIN_URL=postgresql://\${POSTGRES_USER}@localhost:5432/postgres\nTOPOGRAM_INPUT_PATH=${inputPath}\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function renderDbLifecycleReadme(plan) {
|
|
110
|
+
return `# ${plan.projection.name} Lifecycle
|
|
111
|
+
|
|
112
|
+
This bundle gives agents a repeatable database workflow for projection \`${plan.projection.id}\`.
|
|
113
|
+
|
|
114
|
+
## Modes
|
|
115
|
+
|
|
116
|
+
- Greenfield: run \`./scripts/db-bootstrap-or-migrate.sh\` with no current snapshot
|
|
117
|
+
- Brownfield: run \`./scripts/db-bootstrap-or-migrate.sh\` with \`state/current.snapshot.json\` already present
|
|
118
|
+
|
|
119
|
+
## Required Environment
|
|
120
|
+
|
|
121
|
+
${plan.environment.required.map((name) => `- \`${name}\``).join("\n")}
|
|
122
|
+
|
|
123
|
+
## Optional Environment
|
|
124
|
+
|
|
125
|
+
${plan.environment.optional.map((name) => `- \`${name}\``).join("\n")}
|
|
126
|
+
|
|
127
|
+
## Files
|
|
128
|
+
|
|
129
|
+
- Desired snapshot: \`${plan.state.desiredSnapshot}\`
|
|
130
|
+
- Current snapshot: \`${plan.state.currentSnapshot}\`
|
|
131
|
+
- Migration plan: \`${plan.state.migrationPlan}\`
|
|
132
|
+
- Migration SQL: \`${plan.state.migrationSql}\`
|
|
133
|
+
|
|
134
|
+
## Commands
|
|
135
|
+
|
|
136
|
+
- \`./scripts/db-status.sh\`
|
|
137
|
+
- \`./scripts/db-bootstrap.sh\`
|
|
138
|
+
- \`./scripts/db-migrate.sh\`
|
|
139
|
+
- \`./scripts/db-bootstrap-or-migrate.sh\`
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function renderDbLifecycleCommonScript(plan) {
|
|
144
|
+
const engine = plan.engine;
|
|
145
|
+
return `#!/usr/bin/env bash
|
|
146
|
+
set -euo pipefail
|
|
147
|
+
|
|
148
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
149
|
+
BUNDLE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
150
|
+
resolve_path_candidate() {
|
|
151
|
+
local candidate="$1"
|
|
152
|
+
local base_dir="$2"
|
|
153
|
+
local resolved=""
|
|
154
|
+
if [[ -z "$candidate" ]]; then
|
|
155
|
+
return 1
|
|
156
|
+
fi
|
|
157
|
+
if [[ "$candidate" = /* ]]; then
|
|
158
|
+
resolved="$candidate"
|
|
159
|
+
else
|
|
160
|
+
resolved="$(cd "$base_dir" && cd "$candidate" 2>/dev/null && pwd)" || return 1
|
|
161
|
+
fi
|
|
162
|
+
printf '%s\\n' "$resolved"
|
|
163
|
+
}
|
|
164
|
+
find_topogram_bin() {
|
|
165
|
+
if [[ -n "\${TOPOGRAM_BIN:-}" ]]; then
|
|
166
|
+
printf '%s\\n' "$TOPOGRAM_BIN"
|
|
167
|
+
return
|
|
168
|
+
fi
|
|
169
|
+
local candidates=(
|
|
170
|
+
"$BUNDLE_DIR/../../../../node_modules/.bin/topogram"
|
|
171
|
+
"$BUNDLE_DIR/../../../node_modules/.bin/topogram"
|
|
172
|
+
"$PWD/node_modules/.bin/topogram"
|
|
173
|
+
)
|
|
174
|
+
local candidate
|
|
175
|
+
for candidate in "\${candidates[@]}"; do
|
|
176
|
+
if [[ -x "$candidate" ]]; then
|
|
177
|
+
printf '%s\\n' "$candidate"
|
|
178
|
+
return
|
|
179
|
+
fi
|
|
180
|
+
done
|
|
181
|
+
if command -v topogram >/dev/null 2>&1; then
|
|
182
|
+
command -v topogram
|
|
183
|
+
return
|
|
184
|
+
fi
|
|
185
|
+
echo "Unable to locate the Topogram CLI. Install @topogram/cli or set TOPOGRAM_BIN." >&2
|
|
186
|
+
exit 1
|
|
187
|
+
}
|
|
188
|
+
discover_input_path() {
|
|
189
|
+
if [[ -n "\${TOPOGRAM_INPUT_PATH:-}" ]]; then
|
|
190
|
+
local resolved=""
|
|
191
|
+
if resolved="$(resolve_path_candidate "$TOPOGRAM_INPUT_PATH" "$PWD")"; then
|
|
192
|
+
printf '%s\\n' "$resolved"
|
|
193
|
+
return
|
|
194
|
+
fi
|
|
195
|
+
if resolved="$(resolve_path_candidate "$TOPOGRAM_INPUT_PATH" "$BUNDLE_DIR")"; then
|
|
196
|
+
printf '%s\\n' "$resolved"
|
|
197
|
+
return
|
|
198
|
+
fi
|
|
199
|
+
echo "TOPOGRAM_INPUT_PATH is set but cannot be resolved: $TOPOGRAM_INPUT_PATH" >&2
|
|
200
|
+
exit 1
|
|
201
|
+
fi
|
|
202
|
+
local resolved=""
|
|
203
|
+
local candidates=(
|
|
204
|
+
"$BUNDLE_DIR/../../../.."
|
|
205
|
+
"$BUNDLE_DIR/../../.."
|
|
206
|
+
"$BUNDLE_DIR/../../../../.."
|
|
207
|
+
"$PWD"
|
|
208
|
+
)
|
|
209
|
+
local candidate
|
|
210
|
+
for candidate in "\${candidates[@]}"; do
|
|
211
|
+
if resolved="$(resolve_path_candidate "$candidate" "$PWD")" && [[ -d "$resolved/topogram" ]]; then
|
|
212
|
+
printf '%s\\n' "$resolved/topogram"
|
|
213
|
+
return
|
|
214
|
+
fi
|
|
215
|
+
done
|
|
216
|
+
echo "Unable to locate a Topogram workspace. Set TOPOGRAM_INPUT_PATH." >&2
|
|
217
|
+
exit 1
|
|
218
|
+
}
|
|
219
|
+
TOPOGRAM_BIN="$(find_topogram_bin)"
|
|
220
|
+
INPUT_PATH="$(discover_input_path)"
|
|
221
|
+
PROJECTION_ID="${plan.projection.id}"
|
|
222
|
+
STATE_DIR="\${TOPOGRAM_DB_STATE_DIR:-$BUNDLE_DIR/state}"
|
|
223
|
+
CURRENT_SNAPSHOT="$STATE_DIR/current.snapshot.json"
|
|
224
|
+
DESIRED_SNAPSHOT="$STATE_DIR/desired.snapshot.json"
|
|
225
|
+
PLAN_JSON="$STATE_DIR/migration.plan.json"
|
|
226
|
+
MIGRATION_SQL="$STATE_DIR/migration.sql"
|
|
227
|
+
EMPTY_SNAPSHOT="$BUNDLE_DIR/${plan.bundle.emptySnapshot}"
|
|
228
|
+
PRISMA_SCHEMA="${plan.bundle.prismaSchema ? `$BUNDLE_DIR/${plan.bundle.prismaSchema}` : ""}"
|
|
229
|
+
|
|
230
|
+
mkdir -p "$STATE_DIR"
|
|
231
|
+
|
|
232
|
+
require_env() {
|
|
233
|
+
local name="$1"
|
|
234
|
+
if [[ -z "\${!name:-}" ]]; then
|
|
235
|
+
echo "Missing required environment variable: $name" >&2
|
|
236
|
+
exit 1
|
|
237
|
+
fi
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
${engine === "postgres" ? `postgres_cli_url() {
|
|
241
|
+
require_env DATABASE_URL
|
|
242
|
+
node --input-type=module -e 'const url = new URL(process.argv[1]); url.searchParams.delete("schema"); console.log(url.toString());' "$DATABASE_URL"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
postgres_admin_cli_url() {
|
|
246
|
+
require_env DATABASE_ADMIN_URL
|
|
247
|
+
node --input-type=module -e 'const url = new URL(process.argv[1]); url.searchParams.delete("schema"); console.log(url.toString());' "$DATABASE_ADMIN_URL"
|
|
248
|
+
}
|
|
249
|
+
` : `normalize_sqlite_database_url() {
|
|
250
|
+
require_env DATABASE_URL
|
|
251
|
+
local base_dir
|
|
252
|
+
base_dir="$(cd "$BUNDLE_DIR/../.." && pwd)"
|
|
253
|
+
local database_path="$DATABASE_URL"
|
|
254
|
+
if [[ "$database_path" == file:* ]]; then
|
|
255
|
+
database_path="\${database_path#file:}"
|
|
256
|
+
fi
|
|
257
|
+
if [[ "$database_path" != /* ]]; then
|
|
258
|
+
database_path="$base_dir/$(printf '%s' "$database_path" | sed 's#^\./##')"
|
|
259
|
+
fi
|
|
260
|
+
mkdir -p "$(dirname "$database_path")"
|
|
261
|
+
export DATABASE_URL="file:$database_path"
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
sqlite_cli_database_path() {
|
|
265
|
+
normalize_sqlite_database_url
|
|
266
|
+
printf '%s\n' "\${DATABASE_URL#file:}"
|
|
267
|
+
}
|
|
268
|
+
`}
|
|
269
|
+
|
|
270
|
+
ensure_db_cli_on_path() {
|
|
271
|
+
${engine === "postgres" ? ` if command -v psql >/dev/null 2>&1; then
|
|
272
|
+
return
|
|
273
|
+
fi
|
|
274
|
+
local candidates=(
|
|
275
|
+
"/opt/homebrew/opt/postgresql@18/bin"
|
|
276
|
+
"/opt/homebrew/opt/postgresql@17/bin"
|
|
277
|
+
"/opt/homebrew/opt/postgresql@16/bin"
|
|
278
|
+
"/usr/local/opt/postgresql@18/bin"
|
|
279
|
+
"/usr/local/opt/postgresql@17/bin"
|
|
280
|
+
"/usr/local/opt/postgresql@16/bin"
|
|
281
|
+
)
|
|
282
|
+
local candidate
|
|
283
|
+
for candidate in "\${candidates[@]}"; do
|
|
284
|
+
if [[ -x "$candidate/psql" ]]; then
|
|
285
|
+
export PATH="$candidate:$PATH"
|
|
286
|
+
return
|
|
287
|
+
fi
|
|
288
|
+
done
|
|
289
|
+
if command -v brew >/dev/null 2>&1; then
|
|
290
|
+
local prefix=""
|
|
291
|
+
prefix="$(brew --prefix postgresql@18 2>/dev/null || true)"
|
|
292
|
+
if [[ -n "$prefix" && -x "$prefix/bin/psql" ]]; then
|
|
293
|
+
export PATH="$prefix/bin:$PATH"
|
|
294
|
+
return
|
|
295
|
+
fi
|
|
296
|
+
prefix="$(brew --prefix postgresql@17 2>/dev/null || true)"
|
|
297
|
+
if [[ -n "$prefix" && -x "$prefix/bin/psql" ]]; then
|
|
298
|
+
export PATH="$prefix/bin:$PATH"
|
|
299
|
+
return
|
|
300
|
+
fi
|
|
301
|
+
prefix="$(brew --prefix postgresql@16 2>/dev/null || true)"
|
|
302
|
+
if [[ -n "$prefix" && -x "$prefix/bin/psql" ]]; then
|
|
303
|
+
export PATH="$prefix/bin:$PATH"
|
|
304
|
+
return
|
|
305
|
+
fi
|
|
306
|
+
fi
|
|
307
|
+
echo "Unable to locate psql. Install PostgreSQL or add it to PATH." >&2
|
|
308
|
+
exit 1` : ` if command -v sqlite3 >/dev/null 2>&1; then
|
|
309
|
+
return
|
|
310
|
+
fi
|
|
311
|
+
echo "Unable to locate sqlite3. Install SQLite or add it to PATH." >&2
|
|
312
|
+
exit 1`}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
list_live_tables_json() {
|
|
316
|
+
require_env DATABASE_URL
|
|
317
|
+
ensure_db_cli_on_path
|
|
318
|
+
${engine === "postgres" ? ` local cli_url
|
|
319
|
+
cli_url="$(postgres_cli_url)"
|
|
320
|
+
psql "$cli_url" -Atqc "select coalesce(json_agg(tablename order by tablename)::text, '[]') from pg_tables where schemaname = 'public'"` : ` normalize_sqlite_database_url
|
|
321
|
+
local database_path
|
|
322
|
+
database_path="$(sqlite_cli_database_path)"
|
|
323
|
+
if [[ ! -f "$database_path" ]]; then
|
|
324
|
+
printf '[]\\n'
|
|
325
|
+
return
|
|
326
|
+
fi
|
|
327
|
+
sqlite3 "$database_path" "select json_group_array(name) from (select name from sqlite_master where type = 'table' and name not like 'sqlite_%' order by name)" | sed 's/^$/[]/'`}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
reconcile_existing_database_snapshot() {
|
|
331
|
+
generate_desired_snapshot
|
|
332
|
+
local live_tables
|
|
333
|
+
live_tables="$(list_live_tables_json)"
|
|
334
|
+
node --input-type=module -e 'import fs from "node:fs"; const live = JSON.parse(process.argv[1] || "[]"); const desired = JSON.parse(fs.readFileSync(process.argv[2], "utf8")).tables.map((table) => table.table).sort(); const actual = [...live].sort(); if (actual.length === 0) process.exit(1); if (JSON.stringify(actual) === JSON.stringify(desired)) process.exit(0); console.error("Existing database tables do not match the desired Topogram schema."); console.error(JSON.stringify({ actual, desired }, null, 2)); process.exit(2);' "$live_tables" "$DESIRED_SNAPSHOT"
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
current_snapshot_matches_live_database() {
|
|
338
|
+
if [[ ! -f "$CURRENT_SNAPSHOT" ]]; then
|
|
339
|
+
return 1
|
|
340
|
+
fi
|
|
341
|
+
local live_tables
|
|
342
|
+
live_tables="$(list_live_tables_json)"
|
|
343
|
+
node --input-type=module -e 'import fs from "node:fs"; const live = [...new Set(JSON.parse(process.argv[1] || "[]"))].sort(); const current = JSON.parse(fs.readFileSync(process.argv[2], "utf8")).tables.map((table) => table.table).sort(); process.exit(JSON.stringify(live) === JSON.stringify(current) ? 0 : 1);' "$live_tables" "$CURRENT_SNAPSHOT"
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
infer_current_snapshot_from_live_tables() {
|
|
347
|
+
generate_desired_snapshot
|
|
348
|
+
local live_tables
|
|
349
|
+
live_tables="$(list_live_tables_json)"
|
|
350
|
+
node --input-type=module -e 'import fs from "node:fs"; const live = [...new Set(JSON.parse(process.argv[1] || "[]"))].sort(); const desiredSnapshotPath = process.argv[2]; const outputPath = process.argv[3]; const desiredSnapshot = JSON.parse(fs.readFileSync(desiredSnapshotPath, "utf8")); const desiredTables = new Map((desiredSnapshot.tables || []).map((table) => [table.table, table])); if (live.length === 0) process.exit(1); const unknown = live.filter((name) => !desiredTables.has(name)); if (unknown.length > 0) process.exit(2); const inferredTables = live.map((name) => desiredTables.get(name)).filter(Boolean); const inferredSnapshot = { ...desiredSnapshot, tables: inferredTables }; fs.writeFileSync(outputPath, JSON.stringify(inferredSnapshot, null, 2) + "\\n", "utf8"); process.exit(inferredTables.length === desiredSnapshot.tables.length ? 0 : 3);' "$live_tables" "$DESIRED_SNAPSHOT" "$CURRENT_SNAPSHOT"
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
generate_desired_snapshot() {
|
|
354
|
+
"$TOPOGRAM_BIN" "$INPUT_PATH" --generate db-schema-snapshot --projection "$PROJECTION_ID" > "$DESIRED_SNAPSHOT"
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
generate_migration_plan() {
|
|
358
|
+
local from_snapshot="$1"
|
|
359
|
+
"$TOPOGRAM_BIN" "$INPUT_PATH" --generate db-migration-plan --projection "$PROJECTION_ID" --from-snapshot "$from_snapshot" > "$PLAN_JSON"
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
generate_sql_migration() {
|
|
363
|
+
local from_snapshot="$1"
|
|
364
|
+
"$TOPOGRAM_BIN" "$INPUT_PATH" --generate sql-migration --projection "$PROJECTION_ID" --from-snapshot "$from_snapshot" > "$MIGRATION_SQL"
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
ensure_supported_plan() {
|
|
368
|
+
node --input-type=module -e 'import fs from "node:fs"; const plan = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); if (!plan.supported) { console.error("Manual migration required."); console.error(JSON.stringify(plan.manual, null, 2)); process.exit(2); }' "$PLAN_JSON"
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
provision_database_if_needed() {
|
|
372
|
+
require_env DATABASE_URL
|
|
373
|
+
ensure_db_cli_on_path
|
|
374
|
+
${engine === "postgres" ? ` local cli_url
|
|
375
|
+
cli_url="$(postgres_cli_url)"
|
|
376
|
+
if psql "$cli_url" -c 'select 1' >/dev/null 2>&1; then
|
|
377
|
+
return
|
|
378
|
+
fi
|
|
379
|
+
if [[ -z "\${DATABASE_ADMIN_URL:-}" ]]; then
|
|
380
|
+
echo "DATABASE_URL is not reachable and DATABASE_ADMIN_URL was not provided." >&2
|
|
381
|
+
exit 1
|
|
382
|
+
fi
|
|
383
|
+
local admin_cli_url
|
|
384
|
+
admin_cli_url="$(postgres_admin_cli_url)"
|
|
385
|
+
local db_name
|
|
386
|
+
db_name="$(node --input-type=module -e 'const url = new URL(process.argv[1]); console.log(url.pathname.replace(/^\\//, ""));' "$DATABASE_URL")"
|
|
387
|
+
if ! psql "$admin_cli_url" -Atqc "select 1 from pg_database where datname = '$db_name'" | grep -q 1; then
|
|
388
|
+
psql "$admin_cli_url" -c "create database \\"$db_name\\""
|
|
389
|
+
fi` : ` local database_path
|
|
390
|
+
database_path="$(sqlite_cli_database_path)"
|
|
391
|
+
mkdir -p "$(dirname "$database_path")"`}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
apply_sql() {
|
|
395
|
+
require_env DATABASE_URL
|
|
396
|
+
ensure_db_cli_on_path
|
|
397
|
+
${engine === "postgres" ? ` local cli_url
|
|
398
|
+
cli_url="$(postgres_cli_url)"
|
|
399
|
+
psql "$cli_url" -v ON_ERROR_STOP=1 -f "$MIGRATION_SQL"` : ` local database_path
|
|
400
|
+
database_path="$(sqlite_cli_database_path)"
|
|
401
|
+
sqlite3 "$database_path" < "$MIGRATION_SQL"`}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
refresh_runtime_clients() {
|
|
405
|
+
${plan.bundle.prismaSchema ? ` if [[ "\${TOPOGRAM_SKIP_RUNTIME_CLIENT_REFRESH:-0}" == "1" ]]; then
|
|
406
|
+
return
|
|
407
|
+
fi
|
|
408
|
+
local prisma_version="5.22.0"
|
|
409
|
+
${engine === "sqlite" ? ` normalize_sqlite_database_url
|
|
410
|
+
` : ""} local runtime_server_dir="\${TOPOGRAM_RUNTIME_SERVER_DIR:-}"
|
|
411
|
+
if [[ -z "$runtime_server_dir" && -f "$BUNDLE_DIR/../server/package.json" ]]; then
|
|
412
|
+
runtime_server_dir="$BUNDLE_DIR/../server"
|
|
413
|
+
fi
|
|
414
|
+
if [[ -z "$runtime_server_dir" && -d "$BUNDLE_DIR/../../services" ]]; then
|
|
415
|
+
for candidate in "$BUNDLE_DIR"/../../services/*; do
|
|
416
|
+
if [[ -f "$candidate/package.json" ]]; then
|
|
417
|
+
runtime_server_dir="$candidate"
|
|
418
|
+
break
|
|
419
|
+
fi
|
|
420
|
+
done
|
|
421
|
+
fi
|
|
422
|
+
if [[ -n "$runtime_server_dir" && -f "$runtime_server_dir/package.json" ]]; then
|
|
423
|
+
(cd "$runtime_server_dir" && npm install && npm exec -- prisma db push --schema "$PRISMA_SCHEMA" --skip-generate)
|
|
424
|
+
return
|
|
425
|
+
fi
|
|
426
|
+
npx -p "prisma@$prisma_version" prisma db push --schema "$PRISMA_SCHEMA" --skip-generate` : ` :`}
|
|
427
|
+
}
|
|
428
|
+
`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function renderDbStatusScript() {
|
|
432
|
+
return `#!/usr/bin/env bash
|
|
433
|
+
set -euo pipefail
|
|
434
|
+
. "$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)/db-common.sh"
|
|
435
|
+
|
|
436
|
+
generate_desired_snapshot
|
|
437
|
+
|
|
438
|
+
if [[ -f "$CURRENT_SNAPSHOT" ]]; then
|
|
439
|
+
generate_migration_plan "$CURRENT_SNAPSHOT"
|
|
440
|
+
cat "$PLAN_JSON"
|
|
441
|
+
else
|
|
442
|
+
echo '{"mode":"greenfield","currentSnapshot":null}'
|
|
443
|
+
fi
|
|
444
|
+
`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function renderDbBootstrapScript() {
|
|
448
|
+
return `#!/usr/bin/env bash
|
|
449
|
+
set -euo pipefail
|
|
450
|
+
. "$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)/db-common.sh"
|
|
451
|
+
|
|
452
|
+
if [[ -f "$CURRENT_SNAPSHOT" ]]; then
|
|
453
|
+
echo "Current snapshot already exists at $CURRENT_SNAPSHOT. Run ./scripts/db-migrate.sh instead." >&2
|
|
454
|
+
exit 1
|
|
455
|
+
fi
|
|
456
|
+
|
|
457
|
+
if reconcile_existing_database_snapshot; then
|
|
458
|
+
refresh_runtime_clients
|
|
459
|
+
cp "$DESIRED_SNAPSHOT" "$CURRENT_SNAPSHOT"
|
|
460
|
+
echo "Existing database already matches desired schema. Recorded current snapshot."
|
|
461
|
+
exit 0
|
|
462
|
+
else
|
|
463
|
+
status=$?
|
|
464
|
+
if [[ "$status" -eq 2 ]]; then
|
|
465
|
+
if infer_current_snapshot_from_live_tables; then
|
|
466
|
+
echo "Existing database tables matched a subset of the desired Topogram schema. Recorded an inferred current snapshot."
|
|
467
|
+
exec bash "$SCRIPT_DIR/db-migrate.sh"
|
|
468
|
+
else
|
|
469
|
+
infer_status=$?
|
|
470
|
+
if [[ "$infer_status" -eq 3 ]]; then
|
|
471
|
+
echo "Existing database tables matched a subset of the desired Topogram schema. Recorded an inferred current snapshot."
|
|
472
|
+
exec bash "$SCRIPT_DIR/db-migrate.sh"
|
|
473
|
+
fi
|
|
474
|
+
echo "Existing database is not empty and does not match the desired Topogram schema." >&2
|
|
475
|
+
echo "Create or provide a matching current snapshot before running bootstrap." >&2
|
|
476
|
+
exit 1
|
|
477
|
+
fi
|
|
478
|
+
fi
|
|
479
|
+
fi
|
|
480
|
+
|
|
481
|
+
provision_database_if_needed
|
|
482
|
+
generate_desired_snapshot
|
|
483
|
+
generate_sql_migration "$EMPTY_SNAPSHOT"
|
|
484
|
+
apply_sql
|
|
485
|
+
refresh_runtime_clients
|
|
486
|
+
cp "$DESIRED_SNAPSHOT" "$CURRENT_SNAPSHOT"
|
|
487
|
+
echo "Greenfield bootstrap complete."
|
|
488
|
+
`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function renderDbMigrateScript() {
|
|
492
|
+
return `#!/usr/bin/env bash
|
|
493
|
+
set -euo pipefail
|
|
494
|
+
. "$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)/db-common.sh"
|
|
495
|
+
|
|
496
|
+
if [[ ! -f "$CURRENT_SNAPSHOT" ]]; then
|
|
497
|
+
echo "No current snapshot found at $CURRENT_SNAPSHOT. Run ./scripts/db-bootstrap.sh instead." >&2
|
|
498
|
+
exit 1
|
|
499
|
+
fi
|
|
500
|
+
|
|
501
|
+
if ! current_snapshot_matches_live_database; then
|
|
502
|
+
if infer_current_snapshot_from_live_tables; then
|
|
503
|
+
echo "Current snapshot did not match the live database. Replaced it with an inferred snapshot from live tables."
|
|
504
|
+
else
|
|
505
|
+
infer_status=$?
|
|
506
|
+
if [[ "$infer_status" -eq 3 ]]; then
|
|
507
|
+
echo "Current snapshot did not match the live database. Replaced it with an inferred snapshot from live tables."
|
|
508
|
+
else
|
|
509
|
+
echo "Current snapshot does not match the live database and could not be inferred safely." >&2
|
|
510
|
+
exit 1
|
|
511
|
+
fi
|
|
512
|
+
fi
|
|
513
|
+
fi
|
|
514
|
+
|
|
515
|
+
generate_desired_snapshot
|
|
516
|
+
generate_migration_plan "$CURRENT_SNAPSHOT"
|
|
517
|
+
ensure_supported_plan
|
|
518
|
+
generate_sql_migration "$CURRENT_SNAPSHOT"
|
|
519
|
+
apply_sql
|
|
520
|
+
refresh_runtime_clients
|
|
521
|
+
cp "$DESIRED_SNAPSHOT" "$CURRENT_SNAPSHOT"
|
|
522
|
+
echo "Brownfield migration complete."
|
|
523
|
+
`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function renderDbBootstrapOrMigrateScript() {
|
|
527
|
+
return `#!/usr/bin/env bash
|
|
528
|
+
set -euo pipefail
|
|
529
|
+
|
|
530
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
531
|
+
STATE_DIR="\${TOPOGRAM_DB_STATE_DIR:-$(cd "$SCRIPT_DIR/../state" && pwd)}"
|
|
532
|
+
CURRENT_SNAPSHOT="$STATE_DIR/current.snapshot.json"
|
|
533
|
+
|
|
534
|
+
if [[ -f "$CURRENT_SNAPSHOT" ]]; then
|
|
535
|
+
exec bash "$SCRIPT_DIR/db-migrate.sh"
|
|
536
|
+
fi
|
|
537
|
+
|
|
538
|
+
. "$SCRIPT_DIR/db-common.sh"
|
|
539
|
+
if reconcile_existing_database_snapshot; then
|
|
540
|
+
refresh_runtime_clients
|
|
541
|
+
cp "$DESIRED_SNAPSHOT" "$CURRENT_SNAPSHOT"
|
|
542
|
+
echo "Existing database already matches desired schema. Recorded current snapshot."
|
|
543
|
+
exit 0
|
|
544
|
+
else
|
|
545
|
+
status=$?
|
|
546
|
+
if [[ "$status" -eq 2 ]]; then
|
|
547
|
+
if infer_current_snapshot_from_live_tables; then
|
|
548
|
+
echo "Existing database tables matched a subset of the desired Topogram schema. Recorded an inferred current snapshot."
|
|
549
|
+
exec bash "$SCRIPT_DIR/db-migrate.sh"
|
|
550
|
+
else
|
|
551
|
+
infer_status=$?
|
|
552
|
+
if [[ "$infer_status" -eq 3 ]]; then
|
|
553
|
+
echo "Existing database tables matched a subset of the desired Topogram schema. Recorded an inferred current snapshot."
|
|
554
|
+
exec bash "$SCRIPT_DIR/db-migrate.sh"
|
|
555
|
+
fi
|
|
556
|
+
echo "Existing database is not empty and does not match the desired Topogram schema." >&2
|
|
557
|
+
echo "Create or provide a matching current snapshot before running migrations." >&2
|
|
558
|
+
exit 1
|
|
559
|
+
fi
|
|
560
|
+
fi
|
|
561
|
+
fi
|
|
562
|
+
|
|
563
|
+
exec bash "$SCRIPT_DIR/db-bootstrap.sh"
|
|
564
|
+
`;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function generateDbLifecycleBundle(graph, projection, options = {}) {
|
|
568
|
+
const plan = dbLifecyclePlan(graph, projection, options);
|
|
569
|
+
const files = {
|
|
570
|
+
"README.md": renderDbLifecycleReadme(plan),
|
|
571
|
+
".env.example": renderDbLifecycleEnvExample(projection, plan),
|
|
572
|
+
"scripts/db-common.sh": renderDbLifecycleCommonScript(plan),
|
|
573
|
+
"scripts/db-status.sh": renderDbStatusScript(),
|
|
574
|
+
"scripts/db-bootstrap.sh": renderDbBootstrapScript(),
|
|
575
|
+
"scripts/db-migrate.sh": renderDbMigrateScript(),
|
|
576
|
+
"scripts/db-bootstrap-or-migrate.sh": renderDbBootstrapOrMigrateScript(),
|
|
577
|
+
"snapshots/empty.snapshot.json": `${JSON.stringify(renderEmptySnapshotForProjection(projection), null, 2)}\n`,
|
|
578
|
+
"state/.gitkeep": ""
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
if (plan.bundle.prismaSchema) {
|
|
582
|
+
files[plan.bundle.prismaSchema] =
|
|
583
|
+
projection.platform === "db_sqlite"
|
|
584
|
+
? generateSqlitePrismaSchema(graph, { projectionId: projection.id })
|
|
585
|
+
: generatePostgresPrismaSchema(graph, { projectionId: projection.id });
|
|
586
|
+
}
|
|
587
|
+
if (plan.bundle.drizzleSchema) {
|
|
588
|
+
files[plan.bundle.drizzleSchema] = generatePostgresDrizzleSchema(graph, { projectionId: projection.id });
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return files;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export function generateDbLifecyclePlanForProjection(graph, projection, options = {}) {
|
|
595
|
+
return dbLifecyclePlan(graph, projection, options);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function generateDbLifecyclePlan(graph, options = {}) {
|
|
599
|
+
if (options.projectionId) {
|
|
600
|
+
return dbLifecyclePlan(graph, getProjection(graph, options.projectionId), options);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const output = {};
|
|
604
|
+
for (const projection of dbProjectionCandidates(graph)) {
|
|
605
|
+
output[projection.id] = dbLifecyclePlan(graph, projection, options);
|
|
606
|
+
}
|
|
607
|
+
return output;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function generateDbLifecycleBundleForProjection(graph, projection, options = {}) {
|
|
611
|
+
return generateDbLifecycleBundle(graph, projection, options);
|
|
612
|
+
}
|