@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.
Files changed (257) hide show
  1. package/ARCHITECTURE.md +67 -0
  2. package/CHANGELOG.md +240 -0
  3. package/README.md +223 -0
  4. package/package.json +51 -0
  5. package/src/adoption/index.js +3 -0
  6. package/src/adoption/plan.js +702 -0
  7. package/src/adoption/reporting.js +464 -0
  8. package/src/adoption/review-groups.js +313 -0
  9. package/src/agent-ops/query-builders.js +5012 -0
  10. package/src/archive/archive.js +141 -0
  11. package/src/archive/compact.js +26 -0
  12. package/src/archive/jsonl.js +70 -0
  13. package/src/archive/resolver-bridge.js +82 -0
  14. package/src/archive/schema.js +87 -0
  15. package/src/archive/unarchive.js +108 -0
  16. package/src/catalog.js +752 -0
  17. package/src/cli/catalog-alias.js +166 -0
  18. package/src/cli.js +9738 -0
  19. package/src/component-behavior.js +173 -0
  20. package/src/example-implementation.js +91 -0
  21. package/src/format.js +19 -0
  22. package/src/generator/adapters.d.ts +4 -0
  23. package/src/generator/adapters.js +325 -0
  24. package/src/generator/api.d.ts +1 -0
  25. package/src/generator/api.js +1196 -0
  26. package/src/generator/check.js +355 -0
  27. package/src/generator/component-conformance.js +767 -0
  28. package/src/generator/components.js +39 -0
  29. package/src/generator/context/bundle.js +291 -0
  30. package/src/generator/context/diff.js +256 -0
  31. package/src/generator/context/digest.js +182 -0
  32. package/src/generator/context/domain-coverage.js +94 -0
  33. package/src/generator/context/domain-page.js +137 -0
  34. package/src/generator/context/index.js +42 -0
  35. package/src/generator/context/report.js +121 -0
  36. package/src/generator/context/shared.js +1397 -0
  37. package/src/generator/context/slice.js +703 -0
  38. package/src/generator/context/task-mode.js +466 -0
  39. package/src/generator/docs.js +327 -0
  40. package/src/generator/index.js +161 -0
  41. package/src/generator/native/parity-bundle.js +311 -0
  42. package/src/generator/output.js +300 -0
  43. package/src/generator/registry.js +482 -0
  44. package/src/generator/runtime/app-bundle.js +456 -0
  45. package/src/generator/runtime/bundle-shared.js +166 -0
  46. package/src/generator/runtime/compile-check.js +163 -0
  47. package/src/generator/runtime/deployment.js +287 -0
  48. package/src/generator/runtime/environment.js +635 -0
  49. package/src/generator/runtime/index.js +32 -0
  50. package/src/generator/runtime/runtime-check.js +554 -0
  51. package/src/generator/runtime/shared.js +515 -0
  52. package/src/generator/runtime/smoke.js +219 -0
  53. package/src/generator/schema.js +204 -0
  54. package/src/generator/sdlc/board.js +66 -0
  55. package/src/generator/sdlc/doc-page.js +53 -0
  56. package/src/generator/sdlc/index.js +23 -0
  57. package/src/generator/sdlc/release-notes.js +62 -0
  58. package/src/generator/sdlc/traceability-matrix.js +65 -0
  59. package/src/generator/shared.js +29 -0
  60. package/src/generator/surfaces/contracts.js +146 -0
  61. package/src/generator/surfaces/databases/contract.js +40 -0
  62. package/src/generator/surfaces/databases/index.js +84 -0
  63. package/src/generator/surfaces/databases/lifecycle-shared.d.ts +1 -0
  64. package/src/generator/surfaces/databases/lifecycle-shared.js +612 -0
  65. package/src/generator/surfaces/databases/migration-plan.js +281 -0
  66. package/src/generator/surfaces/databases/postgres/capabilities.js +14 -0
  67. package/src/generator/surfaces/databases/postgres/drizzle.js +99 -0
  68. package/src/generator/surfaces/databases/postgres/index.js +9 -0
  69. package/src/generator/surfaces/databases/postgres/lifecycle.js +16 -0
  70. package/src/generator/surfaces/databases/postgres/prisma.js +159 -0
  71. package/src/generator/surfaces/databases/postgres/sql-migration.js +102 -0
  72. package/src/generator/surfaces/databases/postgres/sql-schema.js +34 -0
  73. package/src/generator/surfaces/databases/shared.d.ts +1 -0
  74. package/src/generator/surfaces/databases/shared.js +350 -0
  75. package/src/generator/surfaces/databases/snapshot.js +96 -0
  76. package/src/generator/surfaces/databases/sqlite/capabilities.js +14 -0
  77. package/src/generator/surfaces/databases/sqlite/index.js +8 -0
  78. package/src/generator/surfaces/databases/sqlite/lifecycle.js +16 -0
  79. package/src/generator/surfaces/databases/sqlite/prisma.js +143 -0
  80. package/src/generator/surfaces/databases/sqlite/sql-migration.js +65 -0
  81. package/src/generator/surfaces/databases/sqlite/sql-schema.js +27 -0
  82. package/src/generator/surfaces/index.js +25 -0
  83. package/src/generator/surfaces/native/swiftui-app.js +38 -0
  84. package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +20 -0
  85. package/src/generator/surfaces/native/swiftui-templates/README.generated.md +26 -0
  86. package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +682 -0
  87. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoAPIClient.swift +156 -0
  88. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoSwiftUIApp.swift +44 -0
  89. package/src/generator/surfaces/native/swiftui-templates/runtime/Visibility.swift +183 -0
  90. package/src/generator/surfaces/services/express.d.ts +1 -0
  91. package/src/generator/surfaces/services/express.js +766 -0
  92. package/src/generator/surfaces/services/hono.d.ts +1 -0
  93. package/src/generator/surfaces/services/hono.js +204 -0
  94. package/src/generator/surfaces/services/index.js +42 -0
  95. package/src/generator/surfaces/services/persistence-wiring.js +240 -0
  96. package/src/generator/surfaces/services/runtime-helpers.js +631 -0
  97. package/src/generator/surfaces/services/server-contract.js +80 -0
  98. package/src/generator/surfaces/services/stateless.d.ts +1 -0
  99. package/src/generator/surfaces/services/stateless.js +97 -0
  100. package/src/generator/surfaces/shared.js +64 -0
  101. package/src/generator/surfaces/web/api-client.js +1 -0
  102. package/src/generator/surfaces/web/forms.js +1 -0
  103. package/src/generator/surfaces/web/index.d.ts +2 -0
  104. package/src/generator/surfaces/web/index.js +53 -0
  105. package/src/generator/surfaces/web/react-components.js +248 -0
  106. package/src/generator/surfaces/web/react.js +538 -0
  107. package/src/generator/surfaces/web/routes.js +1 -0
  108. package/src/generator/surfaces/web/screens.js +1 -0
  109. package/src/generator/surfaces/web/shared.js +369 -0
  110. package/src/generator/surfaces/web/sveltekit-actions.js +28 -0
  111. package/src/generator/surfaces/web/sveltekit-components.js +234 -0
  112. package/src/generator/surfaces/web/sveltekit.js +426 -0
  113. package/src/generator/surfaces/web/ui-web-contract.js +65 -0
  114. package/src/generator/surfaces/web/vanilla.js +239 -0
  115. package/src/generator/verification.js +84 -0
  116. package/src/generator.js +1 -0
  117. package/src/import/core/context.js +52 -0
  118. package/src/import/core/contracts.js +23 -0
  119. package/src/import/core/registry.js +81 -0
  120. package/src/import/core/runner.js +646 -0
  121. package/src/import/core/shared.js +910 -0
  122. package/src/import/enrichers/auth-session.js +18 -0
  123. package/src/import/enrichers/django-rest.js +226 -0
  124. package/src/import/enrichers/doc-linking.js +20 -0
  125. package/src/import/enrichers/rails-controllers.js +246 -0
  126. package/src/import/enrichers/rails-models.js +130 -0
  127. package/src/import/enrichers/workflow-target-state.js +10 -0
  128. package/src/import/extractors/api/aspnet-core.js +304 -0
  129. package/src/import/extractors/api/django-routes.js +318 -0
  130. package/src/import/extractors/api/express.js +154 -0
  131. package/src/import/extractors/api/fastify.js +371 -0
  132. package/src/import/extractors/api/flutter-dio.js +135 -0
  133. package/src/import/extractors/api/generic-route-fallback.js +90 -0
  134. package/src/import/extractors/api/graphql-code-first.js +565 -0
  135. package/src/import/extractors/api/graphql-sdl.js +309 -0
  136. package/src/import/extractors/api/jaxrs.js +303 -0
  137. package/src/import/extractors/api/micronaut.js +213 -0
  138. package/src/import/extractors/api/next-route.js +50 -0
  139. package/src/import/extractors/api/next-server-action.js +51 -0
  140. package/src/import/extractors/api/nextauth.js +52 -0
  141. package/src/import/extractors/api/openapi-code.js +242 -0
  142. package/src/import/extractors/api/openapi.js +232 -0
  143. package/src/import/extractors/api/rails-routes.js +230 -0
  144. package/src/import/extractors/api/react-native-repository.js +128 -0
  145. package/src/import/extractors/api/retrofit.js +103 -0
  146. package/src/import/extractors/api/spring-web.js +372 -0
  147. package/src/import/extractors/api/swift-webapi.js +116 -0
  148. package/src/import/extractors/api/trpc.js +212 -0
  149. package/src/import/extractors/db/django-models.js +232 -0
  150. package/src/import/extractors/db/dotnet-models.js +93 -0
  151. package/src/import/extractors/db/drizzle.js +242 -0
  152. package/src/import/extractors/db/ef-core.js +221 -0
  153. package/src/import/extractors/db/flutter-entities.js +120 -0
  154. package/src/import/extractors/db/jpa.js +120 -0
  155. package/src/import/extractors/db/liquibase.js +180 -0
  156. package/src/import/extractors/db/mybatis-xml.js +145 -0
  157. package/src/import/extractors/db/prisma.js +185 -0
  158. package/src/import/extractors/db/rails-schema.js +175 -0
  159. package/src/import/extractors/db/react-native-entities.js +95 -0
  160. package/src/import/extractors/db/room.js +193 -0
  161. package/src/import/extractors/db/snapshot.js +112 -0
  162. package/src/import/extractors/db/sql.js +180 -0
  163. package/src/import/extractors/db/swiftdata.js +137 -0
  164. package/src/import/extractors/ui/android-compose.js +230 -0
  165. package/src/import/extractors/ui/backend-only.js +70 -0
  166. package/src/import/extractors/ui/blazor.js +227 -0
  167. package/src/import/extractors/ui/flutter-screens.js +152 -0
  168. package/src/import/extractors/ui/maui-xaml.js +135 -0
  169. package/src/import/extractors/ui/next-app-router.js +83 -0
  170. package/src/import/extractors/ui/next-pages-router.js +141 -0
  171. package/src/import/extractors/ui/razor-pages.js +181 -0
  172. package/src/import/extractors/ui/react-native-screens.js +166 -0
  173. package/src/import/extractors/ui/react-router.js +139 -0
  174. package/src/import/extractors/ui/sveltekit.js +123 -0
  175. package/src/import/extractors/ui/swiftui.js +193 -0
  176. package/src/import/extractors/ui/uikit.js +175 -0
  177. package/src/import/extractors/verification/generic.js +290 -0
  178. package/src/import/extractors/workflows/generic.js +137 -0
  179. package/src/import/index.js +7 -0
  180. package/src/import/provenance.js +158 -0
  181. package/src/new-project.js +2107 -0
  182. package/src/parser.js +439 -0
  183. package/src/policy/review-boundaries.js +165 -0
  184. package/src/project-config.js +535 -0
  185. package/src/proofs/backend-parity.js +19 -0
  186. package/src/proofs/contract-audit.js +220 -0
  187. package/src/proofs/ios-parity.js +7 -0
  188. package/src/proofs/issues-parity.js +10 -0
  189. package/src/proofs/web-parity.js +50 -0
  190. package/src/realization/api/build-api-realization.js +5 -0
  191. package/src/realization/api/index.js +1 -0
  192. package/src/realization/backend/build-backend-runtime-realization.js +82 -0
  193. package/src/realization/backend/index.d.ts +1 -0
  194. package/src/realization/backend/index.js +4 -0
  195. package/src/realization/db/build-db-realization.js +17 -0
  196. package/src/realization/db/index.js +3 -0
  197. package/src/realization/db/migration-plan.js +5 -0
  198. package/src/realization/db/snapshot.js +5 -0
  199. package/src/realization/ui/build-ui-shared-realization.js +305 -0
  200. package/src/realization/ui/build-web-realization.js +189 -0
  201. package/src/realization/ui/index.js +2 -0
  202. package/src/reconcile/docs.js +280 -0
  203. package/src/reconcile/index.js +3 -0
  204. package/src/reconcile/journeys.js +441 -0
  205. package/src/resolver/docs.js +1 -0
  206. package/src/resolver/enrich/acceptance-criterion.js +14 -0
  207. package/src/resolver/enrich/bug.js +12 -0
  208. package/src/resolver/enrich/component.js +2 -0
  209. package/src/resolver/enrich/index.js +1 -0
  210. package/src/resolver/enrich/pitch.js +18 -0
  211. package/src/resolver/enrich/requirement.js +20 -0
  212. package/src/resolver/enrich/task.js +16 -0
  213. package/src/resolver/expressions.js +1 -0
  214. package/src/resolver/index.js +2422 -0
  215. package/src/resolver/normalize.js +1 -0
  216. package/src/resolver.js +1 -0
  217. package/src/sdlc/adopt.js +65 -0
  218. package/src/sdlc/check.js +86 -0
  219. package/src/sdlc/dod/acceptance-criterion.js +22 -0
  220. package/src/sdlc/dod/bug.js +26 -0
  221. package/src/sdlc/dod/document.js +23 -0
  222. package/src/sdlc/dod/index.js +25 -0
  223. package/src/sdlc/dod/pitch.js +23 -0
  224. package/src/sdlc/dod/requirement.js +34 -0
  225. package/src/sdlc/dod/task.js +39 -0
  226. package/src/sdlc/explain.js +116 -0
  227. package/src/sdlc/history.js +80 -0
  228. package/src/sdlc/paths.js +11 -0
  229. package/src/sdlc/release.js +106 -0
  230. package/src/sdlc/scaffold.js +89 -0
  231. package/src/sdlc/status-filter.js +54 -0
  232. package/src/sdlc/transition.js +112 -0
  233. package/src/sdlc/transitions/acceptance-criterion.js +28 -0
  234. package/src/sdlc/transitions/bug.js +31 -0
  235. package/src/sdlc/transitions/document.js +29 -0
  236. package/src/sdlc/transitions/index.js +56 -0
  237. package/src/sdlc/transitions/pitch.js +34 -0
  238. package/src/sdlc/transitions/requirement.js +31 -0
  239. package/src/sdlc/transitions/task.js +34 -0
  240. package/src/template-trust.js +597 -0
  241. package/src/validator/expressions.js +1 -0
  242. package/src/validator/index.js +3424 -0
  243. package/src/validator/kinds.js +346 -0
  244. package/src/validator/per-kind/acceptance-criterion.js +91 -0
  245. package/src/validator/per-kind/bug.js +77 -0
  246. package/src/validator/per-kind/component.js +274 -0
  247. package/src/validator/per-kind/domain.js +205 -0
  248. package/src/validator/per-kind/pitch.js +101 -0
  249. package/src/validator/per-kind/requirement.js +75 -0
  250. package/src/validator/per-kind/task.js +96 -0
  251. package/src/validator/registry.js +1 -0
  252. package/src/validator/utils.js +12 -0
  253. package/src/validator.js +1 -0
  254. package/src/workflows.js +7597 -0
  255. package/src/workspace-docs.js +265 -0
  256. package/template-helpers/react.js +5 -0
  257. package/template-helpers/sveltekit.js +5 -0
@@ -0,0 +1,281 @@
1
+ import { generateDbSchemaSnapshot } from "./snapshot.js";
2
+
3
+ function fieldSetKey(fields) {
4
+ return fields.join("|");
5
+ }
6
+
7
+ function relationKey(relation) {
8
+ return `${relation.field}:${relation.target?.id || ""}:${relation.target?.field || ""}:${relation.onDelete || ""}`;
9
+ }
10
+
11
+ function findInsertedEnumValues(previousValues, nextValues) {
12
+ const previousSet = new Set(previousValues);
13
+ const inserted = [];
14
+ let previousIndex = 0;
15
+
16
+ for (const value of nextValues) {
17
+ if (previousIndex < previousValues.length && previousValues[previousIndex] === value) {
18
+ previousIndex += 1;
19
+ continue;
20
+ }
21
+ if (previousSet.has(value)) {
22
+ return null;
23
+ }
24
+ inserted.push(value);
25
+ }
26
+
27
+ if (previousIndex !== previousValues.length) {
28
+ return null;
29
+ }
30
+
31
+ return inserted;
32
+ }
33
+
34
+ function enumPlacement(nextValues, value, previousValues) {
35
+ const index = nextValues.indexOf(value);
36
+ for (let cursor = index + 1; cursor < nextValues.length; cursor += 1) {
37
+ const candidate = nextValues[cursor];
38
+ if (previousValues.includes(candidate)) {
39
+ return { before: candidate, after: null };
40
+ }
41
+ }
42
+ for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
43
+ const candidate = nextValues[cursor];
44
+ if (previousValues.includes(candidate)) {
45
+ return { before: null, after: candidate };
46
+ }
47
+ }
48
+ return { before: null, after: null };
49
+ }
50
+
51
+ export function buildDbMigrationPlan(fromSnapshot, toSnapshot, options = {}) {
52
+ const operations = [];
53
+ const manual = [];
54
+ const fromEnums = new Map((fromSnapshot.enums || []).map((entry) => [entry.id, entry]));
55
+ const toEnums = new Map((toSnapshot.enums || []).map((entry) => [entry.id, entry]));
56
+ const fromTables = new Map((fromSnapshot.tables || []).map((table) => [table.table, table]));
57
+ const toTables = new Map((toSnapshot.tables || []).map((table) => [table.table, table]));
58
+
59
+ for (const enumEntry of toSnapshot.enums || []) {
60
+ const previous = fromEnums.get(enumEntry.id);
61
+ if (!previous) {
62
+ operations.push({
63
+ type: "create_enum",
64
+ enum: enumEntry.id,
65
+ values: [...enumEntry.values]
66
+ });
67
+ continue;
68
+ }
69
+
70
+ const inserted = findInsertedEnumValues(previous.values || [], enumEntry.values || []);
71
+ if (inserted == null) {
72
+ manual.push({
73
+ type: "alter_enum",
74
+ enum: enumEntry.id,
75
+ reason: "Enum values changed in a non-additive way"
76
+ });
77
+ continue;
78
+ }
79
+
80
+ for (const value of inserted) {
81
+ const placement = enumPlacement(enumEntry.values || [], value, previous.values || []);
82
+ operations.push({
83
+ type: "add_enum_value",
84
+ enum: enumEntry.id,
85
+ value,
86
+ before: placement.before,
87
+ after: placement.after
88
+ });
89
+ }
90
+ }
91
+
92
+ for (const enumEntry of fromSnapshot.enums || []) {
93
+ if (!toEnums.has(enumEntry.id)) {
94
+ manual.push({
95
+ type: "drop_enum",
96
+ enum: enumEntry.id,
97
+ reason: "Dropping enums is not generated automatically"
98
+ });
99
+ }
100
+ }
101
+
102
+ for (const table of toSnapshot.tables || []) {
103
+ const previous = fromTables.get(table.table);
104
+ if (!previous) {
105
+ operations.push({
106
+ type: "create_table",
107
+ table: table.table,
108
+ entity: table.entity?.id || null
109
+ });
110
+ for (const index of table.indexes || []) {
111
+ if (index.type !== "index") {
112
+ continue;
113
+ }
114
+ operations.push({
115
+ type: "add_index",
116
+ table: table.table,
117
+ fields: index.fields
118
+ });
119
+ }
120
+ continue;
121
+ }
122
+
123
+ const previousColumns = new Map((previous.columns || []).map((column) => [column.name, column]));
124
+ const nextColumns = new Map((table.columns || []).map((column) => [column.name, column]));
125
+
126
+ for (const column of table.columns || []) {
127
+ const previousColumn = previousColumns.get(column.name);
128
+ if (!previousColumn) {
129
+ operations.push({
130
+ type: "add_column",
131
+ table: table.table,
132
+ column: column.name
133
+ });
134
+ continue;
135
+ }
136
+
137
+ const changed =
138
+ previousColumn.sourceField !== column.sourceField ||
139
+ previousColumn.fieldType !== column.fieldType ||
140
+ previousColumn.dbType !== column.dbType ||
141
+ previousColumn.required !== column.required ||
142
+ previousColumn.defaultSql !== column.defaultSql;
143
+
144
+ if (changed) {
145
+ manual.push({
146
+ type: "alter_column",
147
+ table: table.table,
148
+ column: column.name,
149
+ reason: "Column definition changed in a non-additive way"
150
+ });
151
+ }
152
+ }
153
+
154
+ for (const column of previous.columns || []) {
155
+ if (!nextColumns.has(column.name)) {
156
+ manual.push({
157
+ type: "drop_column",
158
+ table: table.table,
159
+ column: column.name,
160
+ reason: "Dropping columns is not generated automatically"
161
+ });
162
+ }
163
+ }
164
+
165
+ const previousUniqueKeys = new Set((previous.uniques || []).map(fieldSetKey));
166
+ const nextUniqueKeys = new Set((table.uniques || []).map(fieldSetKey));
167
+ for (const unique of table.uniques || []) {
168
+ const key = fieldSetKey(unique);
169
+ if (!previousUniqueKeys.has(key)) {
170
+ operations.push({
171
+ type: "add_unique_constraint",
172
+ table: table.table,
173
+ fields: unique
174
+ });
175
+ }
176
+ }
177
+ for (const unique of previous.uniques || []) {
178
+ const key = fieldSetKey(unique);
179
+ if (!nextUniqueKeys.has(key)) {
180
+ manual.push({
181
+ type: "drop_unique_constraint",
182
+ table: table.table,
183
+ fields: unique,
184
+ reason: "Dropping unique constraints is not generated automatically"
185
+ });
186
+ }
187
+ }
188
+
189
+ const previousIndexes = new Set((previous.indexes || []).map((entry) => `${entry.type}:${fieldSetKey(entry.fields)}`));
190
+ const nextIndexes = new Set((table.indexes || []).map((entry) => `${entry.type}:${fieldSetKey(entry.fields)}`));
191
+ for (const index of table.indexes || []) {
192
+ const key = `${index.type}:${fieldSetKey(index.fields)}`;
193
+ if (!previousIndexes.has(key)) {
194
+ if (index.type !== "index") {
195
+ continue;
196
+ }
197
+ operations.push({
198
+ type: "add_index",
199
+ table: table.table,
200
+ fields: index.fields
201
+ });
202
+ }
203
+ }
204
+ for (const index of previous.indexes || []) {
205
+ const key = `${index.type}:${fieldSetKey(index.fields)}`;
206
+ if (!nextIndexes.has(key)) {
207
+ manual.push({
208
+ type: "drop_index",
209
+ table: table.table,
210
+ fields: index.fields,
211
+ reason: "Dropping indexes is not generated automatically"
212
+ });
213
+ }
214
+ }
215
+
216
+ const previousRelations = new Set((previous.relations || []).map(relationKey));
217
+ const nextRelations = new Set((table.relations || []).map(relationKey));
218
+ for (const relation of table.relations || []) {
219
+ const key = relationKey(relation);
220
+ if (!previousRelations.has(key)) {
221
+ operations.push({
222
+ type: "add_foreign_key",
223
+ table: table.table,
224
+ field: relation.field,
225
+ target: relation.target,
226
+ onDelete: relation.onDelete || null
227
+ });
228
+ }
229
+ }
230
+ for (const relation of previous.relations || []) {
231
+ const key = relationKey(relation);
232
+ if (!nextRelations.has(key)) {
233
+ manual.push({
234
+ type: "drop_foreign_key",
235
+ table: table.table,
236
+ field: relation.field,
237
+ reason: "Dropping or changing foreign keys is not generated automatically"
238
+ });
239
+ }
240
+ }
241
+ }
242
+
243
+ for (const table of fromSnapshot.tables || []) {
244
+ if (!toTables.has(table.table)) {
245
+ manual.push({
246
+ type: "drop_table",
247
+ table: table.table,
248
+ reason: "Dropping tables is not generated automatically"
249
+ });
250
+ }
251
+ }
252
+
253
+ return {
254
+ type: "db_migration_plan",
255
+ projection: toSnapshot.projection,
256
+ engine: toSnapshot.engine,
257
+ from: {
258
+ snapshotPath: options.fromSnapshotPath || null,
259
+ tableCount: (fromSnapshot.tables || []).length
260
+ },
261
+ to: {
262
+ tableCount: (toSnapshot.tables || []).length
263
+ },
264
+ supported: manual.length === 0,
265
+ operations,
266
+ manualInterventionRequired: manual.length > 0,
267
+ manual
268
+ };
269
+ }
270
+
271
+ export function generateDbMigrationPlan(graph, options = {}) {
272
+ if (!options.projectionId) {
273
+ throw new Error("db-migration-plan requires --projection");
274
+ }
275
+ if (!options.fromSnapshot) {
276
+ throw new Error("db-migration-plan requires --from-snapshot");
277
+ }
278
+
279
+ const toSnapshot = generateDbSchemaSnapshot(graph, options);
280
+ return buildDbMigrationPlan(options.fromSnapshot, toSnapshot, options);
281
+ }
@@ -0,0 +1,14 @@
1
+ export const POSTGRES_CAPABILITIES = {
2
+ default: {
3
+ family: "postgres",
4
+ profiles: ["default"],
5
+ supportsEnums: true,
6
+ supportsPrisma: true,
7
+ supportsDrizzle: true,
8
+ supportsGeneratedBundles: true
9
+ }
10
+ };
11
+
12
+ export function resolvePostgresCapabilities(profileId = "default") {
13
+ return POSTGRES_CAPABILITIES[profileId] || POSTGRES_CAPABILITIES.default;
14
+ }
@@ -0,0 +1,99 @@
1
+ import { buildDbProjectionContract, defaultTableName, getProjection } from "../shared.js";
2
+ import { normalizeDbSchemaSnapshot } from "../snapshot.js";
3
+ import { resolvePostgresCapabilities } from "./capabilities.js";
4
+
5
+ function fieldSetKey(fields) {
6
+ return fields.join("|");
7
+ }
8
+
9
+ function drizzleScalarForColumn(column) {
10
+ switch (column.fieldType) {
11
+ case "uuid":
12
+ return "uuid";
13
+ case "integer":
14
+ return "integer";
15
+ case "number":
16
+ return "doublePrecision";
17
+ case "boolean":
18
+ return "boolean";
19
+ case "datetime":
20
+ return "timestamp";
21
+ default:
22
+ return "text";
23
+ }
24
+ }
25
+
26
+ function drizzleColumnBuilder(column, relation, targetTableVar) {
27
+ const fn = drizzleScalarForColumn(column);
28
+ const args = [`"${column.name}"`];
29
+ const chain = [];
30
+ if (fn === "timestamp") {
31
+ args.push('{ withTimezone: true, mode: "string" }');
32
+ }
33
+ if (column.required) {
34
+ chain.push("notNull()");
35
+ }
36
+ if (column.defaultValue != null) {
37
+ if (column.fieldType === "boolean" || column.fieldType === "integer" || column.fieldType === "number") {
38
+ chain.push(`default(${column.defaultValue})`);
39
+ } else {
40
+ chain.push(`default("${String(column.defaultValue).replace(/"/g, '\\"')}")`);
41
+ }
42
+ }
43
+ if (relation && targetTableVar) {
44
+ const config = relation.onDelete ? `, { onDelete: "${relation.onDelete.replace("_", " ")}" }` : "";
45
+ chain.push(`references(() => ${targetTableVar}.${relation.target.field}${config})`);
46
+ }
47
+ return `${fn}(${args.join(", ")})${chain.length > 0 ? `.${chain.join(".")}` : ""}`;
48
+ }
49
+
50
+ export function generatePostgresDrizzleSchema(graph, options = {}) {
51
+ resolvePostgresCapabilities(options.profileId);
52
+ const projection = getProjection(graph, options.projectionId);
53
+ if (projection.platform !== "db_postgres") {
54
+ throw new Error(`Drizzle schema generation currently supports db_postgres projections only, found '${projection.platform}'`);
55
+ }
56
+
57
+ const contract = buildDbProjectionContract(graph, projection);
58
+ const snapshot = normalizeDbSchemaSnapshot(contract);
59
+ const tableVarByName = new Map(snapshot.tables.map((table) => [table.table, `${table.table}Table`]));
60
+ const imports = new Set(["pgTable", "text", "uuid", "integer", "doublePrecision", "boolean", "timestamp", "index", "uniqueIndex"]);
61
+ const lines = [];
62
+
63
+ lines.push(`import { ${[...imports].sort().join(", ")} } from "drizzle-orm/pg-core";`);
64
+ lines.push("");
65
+
66
+ for (const table of snapshot.tables) {
67
+ const tableVar = tableVarByName.get(table.table);
68
+ const relationByField = new Map((table.relations || []).map((entry) => [entry.field, entry]));
69
+ lines.push(`export const ${tableVar} = pgTable("${table.table}", {`);
70
+ for (const column of table.columns) {
71
+ const relation = relationByField.get(column.name);
72
+ const targetTableVar = relation ? tableVarByName.get(defaultTableName(relation.target.id)) : null;
73
+ let builder = drizzleColumnBuilder(column, relation, targetTableVar);
74
+ if (table.primaryKey.length === 1 && table.primaryKey[0] === column.name) {
75
+ builder += ".primaryKey()";
76
+ }
77
+ lines.push(` ${column.sourceField}: ${builder},`);
78
+ }
79
+ lines.push("}, (table) => ({");
80
+ for (const index of table.indexes || []) {
81
+ const fn = index.type === "unique" ? "uniqueIndex" : "index";
82
+ const name = `${table.table}_${index.fields.join("_")}_${index.type}`;
83
+ const refs = index.fields.map((field) => `table.${(table.columns.find((column) => column.name === field) || { sourceField: field }).sourceField}`).join(", ");
84
+ lines.push(` ${name}: ${fn}("${name}").on(${refs}),`);
85
+ }
86
+ for (const fields of table.uniques || []) {
87
+ if (fields.length === 1 && (table.indexes || []).some((entry) => entry.type === "unique" && fieldSetKey(entry.fields) === fieldSetKey(fields))) {
88
+ continue;
89
+ }
90
+ const name = `${table.table}_${fields.join("_")}_unique`;
91
+ const refs = fields.map((field) => `table.${(table.columns.find((column) => column.name === field) || { sourceField: field }).sourceField}`).join(", ");
92
+ lines.push(` ${name}: uniqueIndex("${name}").on(${refs}),`);
93
+ }
94
+ lines.push("}));");
95
+ lines.push("");
96
+ }
97
+
98
+ return `${lines.join("\n").trimEnd()}\n`;
99
+ }
@@ -0,0 +1,9 @@
1
+ export { resolvePostgresCapabilities } from "./capabilities.js";
2
+ export { generatePostgresSqlSchema } from "./sql-schema.js";
3
+ export { generatePostgresSqlMigration } from "./sql-migration.js";
4
+ export { generatePostgresPrismaSchema } from "./prisma.js";
5
+ export { generatePostgresDrizzleSchema } from "./drizzle.js";
6
+ export {
7
+ generatePostgresDbLifecycleBundle,
8
+ generatePostgresDbLifecyclePlan
9
+ } from "./lifecycle.js";
@@ -0,0 +1,16 @@
1
+ import {
2
+ generateDbLifecycleBundleForProjection,
3
+ generateDbLifecyclePlanForProjection
4
+ } from "../lifecycle-shared.js";
5
+ import { getProjection } from "../shared.js";
6
+ import { resolvePostgresCapabilities } from "./capabilities.js";
7
+
8
+ export function generatePostgresDbLifecyclePlan(graph, options = {}) {
9
+ resolvePostgresCapabilities(options.profileId);
10
+ return generateDbLifecyclePlanForProjection(graph, getProjection(graph, options.projectionId), options);
11
+ }
12
+
13
+ export function generatePostgresDbLifecycleBundle(graph, options = {}) {
14
+ resolvePostgresCapabilities(options.profileId);
15
+ return generateDbLifecycleBundleForProjection(graph, getProjection(graph, options.projectionId), options);
16
+ }
@@ -0,0 +1,159 @@
1
+ import { buildDbProjectionContract, defaultTableName, findEnumStatement, getProjection, indexGraphStatements, toPascalCase } from "../shared.js";
2
+ import { normalizeDbSchemaSnapshot } from "../snapshot.js";
3
+ import { resolvePostgresCapabilities } from "./capabilities.js";
4
+
5
+ function prismaScalarForColumn(column, byId) {
6
+ const enumStatement = findEnumStatement(byId, column.fieldType);
7
+ if (enumStatement) {
8
+ return { type: toPascalCase(enumStatement.id.replace(/^enum_/, "")), enumStatement };
9
+ }
10
+
11
+ switch (column.fieldType) {
12
+ case "uuid":
13
+ return { type: "String", db: "@db.Uuid" };
14
+ case "integer":
15
+ return { type: "Int" };
16
+ case "number":
17
+ return { type: "Float" };
18
+ case "boolean":
19
+ return { type: "Boolean" };
20
+ case "datetime":
21
+ return { type: "DateTime", db: "@db.Timestamptz(3)" };
22
+ default:
23
+ return { type: "String" };
24
+ }
25
+ }
26
+
27
+ function prismaDefaultForColumn(column, byId) {
28
+ if (column.defaultValue == null) {
29
+ return null;
30
+ }
31
+ if (column.fieldType === "boolean" || column.fieldType === "integer" || column.fieldType === "number") {
32
+ return `@default(${column.defaultValue})`;
33
+ }
34
+ if (byId.has(column.fieldType) && byId.get(column.fieldType)?.kind === "enum") {
35
+ return `@default(${String(column.defaultValue)})`;
36
+ }
37
+ return `@default("${String(column.defaultValue).replace(/"/g, '\\"')}")`;
38
+ }
39
+
40
+ export function generatePostgresPrismaSchema(graph, options = {}) {
41
+ resolvePostgresCapabilities(options.profileId);
42
+ const projection = getProjection(graph, options.projectionId);
43
+ if (projection.platform !== "db_postgres") {
44
+ throw new Error(`Prisma schema generation currently supports db_postgres projections only, found '${projection.platform}'`);
45
+ }
46
+
47
+ const byId = indexGraphStatements(graph);
48
+ const snapshot = normalizeDbSchemaSnapshot(buildDbProjectionContract(graph, projection));
49
+ const enumStatements = new Map();
50
+ for (const table of snapshot.tables) {
51
+ for (const column of table.columns) {
52
+ const enumStatement = findEnumStatement(byId, column.fieldType);
53
+ if (enumStatement) {
54
+ enumStatements.set(enumStatement.id, enumStatement);
55
+ }
56
+ }
57
+ }
58
+
59
+ const relationBackrefs = new Map();
60
+ for (const table of snapshot.tables) {
61
+ for (const relation of table.relations || []) {
62
+ const targetTable = defaultTableName(relation.target.id);
63
+ if (!relationBackrefs.has(targetTable)) {
64
+ relationBackrefs.set(targetTable, []);
65
+ }
66
+ relationBackrefs.get(targetTable).push({
67
+ fromTable: table.table,
68
+ fromModel: toPascalCase(table.entity.id.replace(/^entity_/, "")),
69
+ field: relation.field,
70
+ relationName: `${toPascalCase(table.entity.id.replace(/^entity_/, ""))}_${relation.field}_to_${toPascalCase(relation.target.id.replace(/^entity_/, ""))}`
71
+ });
72
+ }
73
+ }
74
+
75
+ const lines = [];
76
+ lines.push('generator client {');
77
+ lines.push(' provider = "prisma-client-js"');
78
+ lines.push("}");
79
+ lines.push("");
80
+ lines.push("datasource db {");
81
+ lines.push(' provider = "postgresql"');
82
+ lines.push(' url = env("DATABASE_URL")');
83
+ lines.push("}");
84
+ lines.push("");
85
+
86
+ for (const enumStatement of [...enumStatements.values()].sort((a, b) => a.id.localeCompare(b.id))) {
87
+ lines.push(`enum ${toPascalCase(enumStatement.id.replace(/^enum_/, ""))} {`);
88
+ for (const value of enumStatement.values) {
89
+ lines.push(` ${value}`);
90
+ }
91
+ lines.push("}");
92
+ lines.push("");
93
+ }
94
+
95
+ for (const table of snapshot.tables) {
96
+ const modelName = toPascalCase(table.entity.id.replace(/^entity_/, ""));
97
+ const pk = table.primaryKey || [];
98
+ const relationFields = new Map((table.relations || []).map((entry) => [entry.field, entry]));
99
+
100
+ lines.push(`model ${modelName} {`);
101
+ for (const column of table.columns) {
102
+ const scalar = prismaScalarForColumn(column, byId);
103
+ const optional = column.required ? "" : "?";
104
+ const attrs = [];
105
+ if (pk.length === 1 && pk[0] === column.name) {
106
+ attrs.push("@id");
107
+ }
108
+ const uniqueMatch = (table.uniques || []).find((fields) => fields.length === 1 && fields[0] === column.name);
109
+ if (uniqueMatch) {
110
+ attrs.push("@unique");
111
+ }
112
+ const defaultAttr = prismaDefaultForColumn(column, byId);
113
+ if (defaultAttr) {
114
+ attrs.push(defaultAttr);
115
+ }
116
+ if (scalar.db) {
117
+ attrs.push(scalar.db);
118
+ }
119
+ if (column.name !== column.sourceField) {
120
+ attrs.push(`@map("${column.name}")`);
121
+ }
122
+ lines.push(` ${column.sourceField} ${scalar.type}${optional}${attrs.length > 0 ? ` ${attrs.join(" ")}` : ""}`);
123
+
124
+ const relation = relationFields.get(column.name);
125
+ if (relation) {
126
+ const targetModel = toPascalCase(relation.target.id.replace(/^entity_/, ""));
127
+ const relationName = `${modelName}_${column.sourceField}_to_${targetModel}`;
128
+ const optionalRelation = column.required ? "" : "?";
129
+ lines.push(` ${column.sourceField.replace(/_id$/, "")} ${targetModel}${optionalRelation} @relation("${relationName}", fields: [${column.sourceField}], references: [${relation.target.field}])`);
130
+ }
131
+ }
132
+
133
+ for (const backref of relationBackrefs.get(table.table) || []) {
134
+ const relationFieldName = `${backref.fromTable}`;
135
+ lines.push(` ${relationFieldName} ${backref.fromModel}[] @relation("${backref.relationName}")`);
136
+ }
137
+
138
+ if (pk.length > 1) {
139
+ lines.push(` @@id([${pk.join(", ")}])`);
140
+ }
141
+ for (const fields of table.uniques || []) {
142
+ if (fields.length > 1) {
143
+ lines.push(` @@unique([${fields.join(", ")}])`);
144
+ }
145
+ }
146
+ for (const index of table.indexes || []) {
147
+ if (index.type === "index") {
148
+ lines.push(` @@index([${index.fields.join(", ")}])`);
149
+ }
150
+ }
151
+ if (table.table !== modelName) {
152
+ lines.push(` @@map("${table.table}")`);
153
+ }
154
+ lines.push("}");
155
+ lines.push("");
156
+ }
157
+
158
+ return `${lines.join("\n").trimEnd()}\n`;
159
+ }
@@ -0,0 +1,102 @@
1
+ import { buildDbMigrationPlan } from "../migration-plan.js";
2
+ import { generateDbSchemaSnapshot, getTableFromSnapshot } from "../snapshot.js";
3
+ import {
4
+ defaultTableName,
5
+ indexGraphStatements,
6
+ renderAddColumn,
7
+ renderAddEnumValue,
8
+ renderCreateEnumType,
9
+ renderCreateTable
10
+ } from "../shared.js";
11
+ import { resolvePostgresCapabilities } from "./capabilities.js";
12
+
13
+ function renderSqlMigration(plan, snapshot) {
14
+ if (!plan.supported) {
15
+ throw new Error(`Migration plan for '${plan.projection.id}' requires manual intervention`);
16
+ }
17
+
18
+ const statements = [];
19
+ const byId = plan.graph ? indexGraphStatements(plan.graph) : null;
20
+ const enumsById = new Map((snapshot.enums || []).map((entry) => [entry.id, entry]));
21
+ const createdTables = new Set();
22
+ const pendingForeignKeys = [];
23
+ for (const operation of plan.operations) {
24
+ if (operation.type === "create_enum") {
25
+ const enumStatement = enumsById.get(operation.enum) || (byId ? byId.get(operation.enum) : null);
26
+ if (enumStatement) {
27
+ statements.push(renderCreateEnumType(enumStatement));
28
+ }
29
+ continue;
30
+ }
31
+
32
+ if (operation.type === "add_enum_value") {
33
+ const enumStatement = enumsById.get(operation.enum) || (byId ? byId.get(operation.enum) : null);
34
+ if (enumStatement) {
35
+ statements.push(renderAddEnumValue(enumStatement, operation));
36
+ }
37
+ continue;
38
+ }
39
+
40
+ if (operation.type === "create_table") {
41
+ const table = getTableFromSnapshot(snapshot, operation.table);
42
+ createdTables.add(operation.table);
43
+ statements.push(renderCreateTable(table, "postgres", { includeRelations: false, byId }));
44
+ for (const relation of table?.relations || []) {
45
+ if (relation.target?.id && relation.target?.field) {
46
+ pendingForeignKeys.push({
47
+ table: operation.table,
48
+ field: relation.field,
49
+ target: relation.target,
50
+ onDelete: relation.onDelete || null
51
+ });
52
+ }
53
+ }
54
+ continue;
55
+ }
56
+
57
+ if (operation.type === "add_column") {
58
+ const table = getTableFromSnapshot(snapshot, operation.table);
59
+ const column = table?.columns.find((entry) => entry.name === operation.column);
60
+ statements.push(renderAddColumn(operation.table, column));
61
+ continue;
62
+ }
63
+
64
+ if (operation.type === "add_unique_constraint") {
65
+ statements.push(`alter table ${operation.table} add unique (${operation.fields.join(", ")});`);
66
+ continue;
67
+ }
68
+
69
+ if (operation.type === "add_index" || operation.type === "add_unique_index") {
70
+ const unique = operation.type === "add_unique_index" ? "unique " : "";
71
+ const name = `${operation.table}_${operation.fields.join("_")}_${operation.type === "add_unique_index" ? "unique" : "index"}`;
72
+ statements.push(`create ${unique}index ${name} on ${operation.table} (${operation.fields.join(", ")});`);
73
+ continue;
74
+ }
75
+
76
+ if (operation.type === "add_foreign_key") {
77
+ const targetTable = defaultTableName(operation.target.id);
78
+ statements.push(`alter table ${operation.table} add foreign key (${operation.field}) references ${targetTable}(${operation.target.field})${operation.onDelete ? ` on delete ${operation.onDelete.replace("_", " ")}` : ""};`);
79
+ }
80
+ }
81
+
82
+ for (const relation of pendingForeignKeys) {
83
+ const targetTable = defaultTableName(relation.target.id);
84
+ statements.push(`alter table ${relation.table} add foreign key (${relation.field}) references ${targetTable}(${relation.target.field})${relation.onDelete ? ` on delete ${relation.onDelete.replace("_", " ")}` : ""};`);
85
+ }
86
+
87
+ return `${statements.join("\n\n").trimEnd()}\n`;
88
+ }
89
+
90
+ export function generatePostgresSqlMigration(graph, options = {}) {
91
+ resolvePostgresCapabilities(options.profileId);
92
+ if (!options.projectionId) {
93
+ throw new Error("sql-migration requires --projection");
94
+ }
95
+ if (!options.fromSnapshot) {
96
+ throw new Error("sql-migration requires --from-snapshot");
97
+ }
98
+
99
+ const toSnapshot = generateDbSchemaSnapshot(graph, options);
100
+ const plan = buildDbMigrationPlan(options.fromSnapshot, toSnapshot, options);
101
+ return renderSqlMigration({ ...plan, graph }, toSnapshot);
102
+ }