@restforgejs/platform 4.1.0 → 4.2.8

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 (181) hide show
  1. package/LICENSE.md +5 -1
  2. package/README.md +30 -14
  3. package/bin/sdf-tools.exe +0 -0
  4. package/build-info.json +2 -2
  5. package/cli/consumer-deploy.js +1 -1
  6. package/cli/consumer.js +1 -1
  7. package/generators/cli/endpoint/create.js +42 -3
  8. package/generators/cli/schema/apply.js +525 -0
  9. package/generators/cli/schema/diff.js +321 -0
  10. package/generators/cli/schema/generate-ddl.js +7 -10
  11. package/generators/cli/schema/init.js +95 -172
  12. package/generators/cli/schema/migrate.js +10 -16
  13. package/generators/cli/schema/models.js +8 -12
  14. package/generators/cli/schema/template.js +222 -0
  15. package/generators/cli/schema/validate.js +8 -12
  16. package/generators/cli-entry.js +17 -2
  17. package/generators/lib/dbschema-kit/apply-engine.js +582 -0
  18. package/generators/lib/dbschema-kit/diff-engine.js +703 -0
  19. package/generators/lib/dbschema-kit/diff-reporter.js +272 -0
  20. package/generators/lib/dbschema-kit/emitters/alter-table.js +275 -0
  21. package/generators/lib/payload/endpoint-schema-validator.js +171 -0
  22. package/generators/lib/payload/payload-runner.js +137 -220
  23. package/generators/lib/payload/schema-diff.js +277 -0
  24. package/generators/lib/utils/audit-columns.js +181 -0
  25. package/generators/lib/utils/cli-output.js +17 -0
  26. package/generators/lib/utils/database-introspector.js +16 -13
  27. package/integrity-manifest.json +8 -8
  28. package/package.json +4 -4
  29. package/scripts/check-install.js +45 -45
  30. package/scripts/verify-integrity.js +1 -1
  31. package/server.js +1 -1
  32. package/src/components/handlers/adjust_handler.js +1 -1
  33. package/src/components/handlers/audit_handler.js +1 -1
  34. package/src/components/handlers/delete_handler.js +1 -1
  35. package/src/components/handlers/export_handler.js +1 -1
  36. package/src/components/handlers/import_handler.js +1 -1
  37. package/src/components/handlers/insert_handler.js +1 -1
  38. package/src/components/handlers/update_handler.js +1 -1
  39. package/src/components/handlers/upload_handler.js +1 -1
  40. package/src/components/handlers/workflow_handler.js +1 -1
  41. package/src/components/integrations/webhook.js +1 -1
  42. package/src/consumers/baseConsumer.js +1 -1
  43. package/src/consumers/declarativeMapper.js +1 -1
  44. package/src/consumers/handlers/apiHandler.js +1 -1
  45. package/src/consumers/handlers/consoleHandler.js +1 -1
  46. package/src/consumers/handlers/databaseHandler.js +1 -1
  47. package/src/consumers/handlers/index.js +1 -1
  48. package/src/consumers/handlers/kafkaHandler.js +1 -1
  49. package/src/consumers/index.js +1 -1
  50. package/src/consumers/messageTransformer.js +1 -1
  51. package/src/consumers/validator.js +1 -1
  52. package/src/core/db/dialect/base-dialect.js +1 -1
  53. package/src/core/db/dialect/index.js +1 -1
  54. package/src/core/db/dialect/mysql-dialect.js +1 -1
  55. package/src/core/db/dialect/oracle-dialect.js +1 -1
  56. package/src/core/db/dialect/postgres-dialect.js +1 -1
  57. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  58. package/src/core/db/flatten-helper.js +1 -1
  59. package/src/core/db/query-builder-error.js +1 -1
  60. package/src/core/db/query-builder.js +1 -1
  61. package/src/core/db/relation-helper.js +1 -1
  62. package/src/core/handlers/delete_handler.js +1 -1
  63. package/src/core/handlers/insert_handler.js +1 -1
  64. package/src/core/handlers/update_handler.js +1 -1
  65. package/src/core/models/base-model.js +1 -1
  66. package/src/core/utils/cache-manager.js +1 -1
  67. package/src/core/utils/component-engine.js +1 -1
  68. package/src/core/utils/context-builder.js +1 -1
  69. package/src/core/utils/datetime-formatter.js +1 -1
  70. package/src/core/utils/datetime-parser.js +1 -1
  71. package/src/core/utils/db.js +1 -1
  72. package/src/core/utils/logger.js +1 -1
  73. package/src/core/utils/payload-loader.js +1 -1
  74. package/src/core/utils/security-checks.js +1 -1
  75. package/src/middleware/body-options.js +1 -1
  76. package/src/middleware/cors.js +1 -1
  77. package/src/middleware/idempotency.js +1 -1
  78. package/src/middleware/rate-limiter.js +1 -1
  79. package/src/middleware/request-logger.js +1 -1
  80. package/src/middleware/security-headers.js +1 -1
  81. package/src/models/base-model-mysql.js +1 -1
  82. package/src/models/base-model-oracle.js +1 -1
  83. package/src/models/base-model-sqlite.js +1 -1
  84. package/src/models/base-model.js +1 -1
  85. package/src/pro/caching/redis-client.js +1 -1
  86. package/src/pro/caching/redis-helper.js +1 -1
  87. package/src/pro/consumers/baseConsumer.js +1 -1
  88. package/src/pro/consumers/declarativeMapper.js +1 -1
  89. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  90. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  91. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  92. package/src/pro/consumers/handlers/index.js +1 -1
  93. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  94. package/src/pro/consumers/index.js +1 -1
  95. package/src/pro/consumers/messageTransformer.js +1 -1
  96. package/src/pro/consumers/validator.js +1 -1
  97. package/src/pro/database/base-model-mysql.js +1 -1
  98. package/src/pro/database/base-model-oracle.js +1 -1
  99. package/src/pro/database/base-model-sqlite.js +1 -1
  100. package/src/pro/database/db-mysql.js +1 -1
  101. package/src/pro/database/db-oracle.js +1 -1
  102. package/src/pro/database/db-sqlite.js +1 -1
  103. package/src/pro/excel/excel-generator.js +1 -1
  104. package/src/pro/excel/excel-parser.js +1 -1
  105. package/src/pro/excel/export-service.js +1 -1
  106. package/src/pro/excel/export_handler.js +1 -1
  107. package/src/pro/excel/import-service.js +1 -1
  108. package/src/pro/excel/import-validator.js +1 -1
  109. package/src/pro/excel/import_handler.js +1 -1
  110. package/src/pro/excel/upsert-builder.js +1 -1
  111. package/src/pro/idgen/idgen-routes.js +1 -1
  112. package/src/pro/integrations/lookup-resolver.js +1 -1
  113. package/src/pro/integrations/upload-handler-v2.js +1 -1
  114. package/src/pro/integrations/upload-handler.js +1 -1
  115. package/src/pro/integrations/webhook.js +1 -1
  116. package/src/pro/locking/lock-routes.js +1 -1
  117. package/src/pro/locking/resource-lock-manager.js +1 -1
  118. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  119. package/src/pro/messaging/kafkaService.js +1 -1
  120. package/src/pro/messaging/messagehubService.js +1 -1
  121. package/src/pro/messaging/rabbitmqService.js +1 -1
  122. package/src/pro/scheduler/job-manager.js +1 -1
  123. package/src/pro/scheduler/job-routes.js +1 -1
  124. package/src/pro/scheduler/job-validator.js +1 -1
  125. package/src/pro/storage/base-storage-provider.js +1 -1
  126. package/src/pro/storage/file-metadata-helper.js +1 -1
  127. package/src/pro/storage/index.js +1 -1
  128. package/src/pro/storage/local-storage-provider.js +1 -1
  129. package/src/pro/storage/s3-storage-provider.js +1 -1
  130. package/src/pro/storage/upload-cleanup-job.js +1 -1
  131. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  132. package/src/pro/storage/upload-pending-tracker.js +1 -1
  133. package/src/pro/websocket/broadcast-helper.js +1 -1
  134. package/src/pro/websocket/index.js +1 -1
  135. package/src/pro/websocket/livesync-server.js +1 -1
  136. package/src/pro/websocket/ws-broadcaster.js +1 -1
  137. package/src/services/export-service.js +1 -1
  138. package/src/services/import-service.js +1 -1
  139. package/src/services/kafkaConsumerService.js +1 -1
  140. package/src/services/kafkaService.js +1 -1
  141. package/src/services/messagehubService.js +1 -1
  142. package/src/services/rabbitmqService.js +1 -1
  143. package/src/utils/cache-invalidation-registry.js +1 -1
  144. package/src/utils/cache-manager.js +1 -1
  145. package/src/utils/component-engine.js +1 -1
  146. package/src/utils/config-extractor.js +1 -1
  147. package/src/utils/consumerLogger.js +1 -1
  148. package/src/utils/context-builder.js +1 -1
  149. package/src/utils/dashboard-helpers.js +1 -1
  150. package/src/utils/dateHelper.js +1 -1
  151. package/src/utils/datetime-formatter.js +1 -1
  152. package/src/utils/datetime-parser.js +1 -1
  153. package/src/utils/db-bootstrap.js +1 -1
  154. package/src/utils/db-mysql.js +1 -1
  155. package/src/utils/db-oracle.js +1 -1
  156. package/src/utils/db-sqlite.js +1 -1
  157. package/src/utils/db.js +1 -1
  158. package/src/utils/demo-generator.js +1 -1
  159. package/src/utils/excel-generator.js +1 -1
  160. package/src/utils/excel-parser.js +1 -1
  161. package/src/utils/file-watcher.js +1 -1
  162. package/src/utils/id-generator.js +1 -1
  163. package/src/utils/idempotency-manager.js +1 -1
  164. package/src/utils/import-validator.js +1 -1
  165. package/src/utils/license-client.js +1 -1
  166. package/src/utils/lock-manager.js +1 -1
  167. package/src/utils/logger.js +1 -1
  168. package/src/utils/lookup-resolver.js +1 -1
  169. package/src/utils/payload-loader.js +1 -1
  170. package/src/utils/processor-response.js +1 -1
  171. package/src/utils/rabbitmq.js +1 -1
  172. package/src/utils/redis-client.js +1 -1
  173. package/src/utils/redis-helper.js +1 -1
  174. package/src/utils/request-scope.js +1 -1
  175. package/src/utils/security-checks.js +1 -1
  176. package/src/utils/service-resolver.js +1 -1
  177. package/src/utils/shutdown-coordinator.js +1 -1
  178. package/src/utils/trusted-keys.js +1 -1
  179. package/src/utils/upload-handler.js +1 -1
  180. package/src/utils/upsert-builder.js +1 -1
  181. package/src/utils/workflow-hook-executor.js +1 -1
@@ -0,0 +1,582 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Apply Engine — translate delta dari diff-engine menjadi `ALTER TABLE`
5
+ * statement incremental. Komplemen `ddl-generator.js` yang menghasilkan full
6
+ * CREATE TABLE.
7
+ *
8
+ * Operasi yang di-handle (MVP):
9
+ * - ADD COLUMN (additive, default)
10
+ * - DROP COLUMN (destruktif, butuh allowDrop)
11
+ * - MODIFY COLUMN (length / nullable, butuh allowModify)
12
+ * - CREATE INDEX (additive)
13
+ * - DROP INDEX (destruktif, butuh allowDrop)
14
+ * - ADD CONSTRAINT UNIQUE (additive)
15
+ * - DROP CONSTRAINT UNIQUE (destruktif, butuh allowDrop)
16
+ *
17
+ * Operasi yang TIDAK di-handle (deferred):
18
+ * - ALTER COLUMN type change (butuh data conversion strategy per dialect)
19
+ * - PK changes (butuh rebuild table)
20
+ * - FK changes (butuh cross-model validation + dialect syntax kompleks)
21
+ * - DEFAULT value changes (defer ke fase berikutnya)
22
+ * - CHECK constraint changes
23
+ *
24
+ * SQLite tidak mendukung ALTER COLUMN / DROP COLUMN tanpa rebuild table.
25
+ * Untuk dialect ini, MODIFY dan DROP COLUMN otomatis di-skip dengan reason
26
+ * 'sqlite limitation' walaupun flag opt-in aktif.
27
+ *
28
+ * Output dipesan dengan urutan aman untuk dependency:
29
+ * 1. ADD COLUMN (no dependencies)
30
+ * 2. CREATE INDEX (depend on column existence)
31
+ * 3. ADD CONSTRAINT UNIQUE (depend on column existence)
32
+ * 4. ALTER/MODIFY COLUMN (modify after additive done)
33
+ * 5. DROP CONSTRAINT (before drop column to avoid dependency error)
34
+ * 6. DROP INDEX (before drop column)
35
+ * 7. DROP COLUMN (last)
36
+ *
37
+ * @module lib/dbschema-kit/apply-engine
38
+ */
39
+
40
+ const { getDialect } = require('./dialect');
41
+ const {
42
+ emitAddColumn,
43
+ emitDropColumn,
44
+ emitModifyColumn,
45
+ emitCreateIndex,
46
+ emitDropIndex,
47
+ emitAddUnique,
48
+ emitDropUnique
49
+ } = require('./emitters/alter-table');
50
+
51
+ const VALID_DIALECTS = ['postgres', 'mysql', 'oracle', 'sqlite'];
52
+
53
+ // Kategori reason dari diff-engine compareFieldDetails. Reason berbentuk string
54
+ // dengan prefix "type:", "length:", "precision:", "scale:", "nullable:",
55
+ // "default:", "default kind:", atau "default (weak):". Kategorisasi ini
56
+ // menentukan apakah modify aman di-emit (length/nullable) atau di-skip
57
+ // (type/default — deferred).
58
+ function classifyMismatchReasons(reasons) {
59
+ const result = {
60
+ hasType: false,
61
+ hasLength: false,
62
+ hasPrecision: false,
63
+ hasScale: false,
64
+ hasNullable: false,
65
+ hasDefault: false
66
+ };
67
+ if (!Array.isArray(reasons)) return result;
68
+ for (const r of reasons) {
69
+ if (typeof r !== 'string') continue;
70
+ if (r.startsWith('type:')) result.hasType = true;
71
+ else if (r.startsWith('length:')) result.hasLength = true;
72
+ else if (r.startsWith('precision:')) result.hasPrecision = true;
73
+ else if (r.startsWith('scale:')) result.hasScale = true;
74
+ else if (r.startsWith('nullable:')) result.hasNullable = true;
75
+ else if (r.startsWith('default')) result.hasDefault = true;
76
+ }
77
+ return result;
78
+ }
79
+
80
+ // Resolve IR field definition dari delta summary. Delta engine menyimpan
81
+ // summary terbatas (name, type, length, precision, scale, nullable). Untuk
82
+ // emitter kita perlu shape kompatibel dengan dialect.mapType + notnull flag.
83
+ function buildFieldFromSummary(summary) {
84
+ const field = {};
85
+ if (summary.type !== undefined) field.type = summary.type;
86
+ if (summary.length !== undefined) field.length = summary.length;
87
+ if (summary.precision !== undefined) field.precision = summary.precision;
88
+ if (summary.scale !== undefined) field.scale = summary.scale;
89
+ if (summary.nullable === false) field.notnull = true;
90
+ return field;
91
+ }
92
+
93
+ // Resolve field IR dari SDF map bila tersedia (untuk default value dan
94
+ // notnull yang akurat). Bila tidak ditemukan, fallback ke summary.
95
+ function resolveFieldIR(sdfModels, tableName, fieldName, fallbackSummary) {
96
+ if (sdfModels) {
97
+ const ir = lookupSdfIR(sdfModels, tableName);
98
+ if (ir && ir.fields && ir.fields[fieldName]) {
99
+ return ir.fields[fieldName];
100
+ }
101
+ }
102
+ return buildFieldFromSummary(fallbackSummary);
103
+ }
104
+
105
+ function lookupSdfIR(sdfModels, tableName) {
106
+ if (!sdfModels) return null;
107
+ if (sdfModels instanceof Map) {
108
+ for (const ir of sdfModels.values()) {
109
+ if (ir && ir.tableName === tableName) return ir;
110
+ }
111
+ return null;
112
+ }
113
+ if (Array.isArray(sdfModels)) {
114
+ for (const ir of sdfModels) {
115
+ if (ir && ir.tableName === tableName) return ir;
116
+ }
117
+ return null;
118
+ }
119
+ if (typeof sdfModels === 'object') {
120
+ for (const key of Object.keys(sdfModels)) {
121
+ const ir = sdfModels[key];
122
+ if (ir && ir.tableName === tableName) return ir;
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+
128
+ function resolveTableIR(sdfModels, delta) {
129
+ const fromSdf = lookupSdfIR(sdfModels, delta.tableName);
130
+ if (fromSdf) {
131
+ return { tableName: fromSdf.tableName, schemaName: fromSdf.schemaName || null };
132
+ }
133
+ return { tableName: delta.tableName, schemaName: null };
134
+ }
135
+
136
+ // ─────────────────────────────────────────────────────────────
137
+ // Statement bucketing per kategori (untuk ordering aman)
138
+ // ─────────────────────────────────────────────────────────────
139
+
140
+ function buildBuckets() {
141
+ return {
142
+ addColumns: [],
143
+ createIndexes: [],
144
+ addUniques: [],
145
+ modifyColumns: [],
146
+ dropConstraints: [],
147
+ dropIndexes: [],
148
+ dropColumns: []
149
+ };
150
+ }
151
+
152
+ function flattenBuckets(buckets) {
153
+ return [
154
+ ...buckets.addColumns,
155
+ ...buckets.createIndexes,
156
+ ...buckets.addUniques,
157
+ ...buckets.modifyColumns,
158
+ ...buckets.dropConstraints,
159
+ ...buckets.dropIndexes,
160
+ ...buckets.dropColumns
161
+ ];
162
+ }
163
+
164
+ // ─────────────────────────────────────────────────────────────
165
+ // Per-delta processing
166
+ // ─────────────────────────────────────────────────────────────
167
+
168
+ function processFieldsOnlyInSdf(delta, tableIR, dialect, buckets, skipped, sdfModels, summary) {
169
+ const list = delta.fields && Array.isArray(delta.fields.onlyInSdf) ? delta.fields.onlyInSdf : [];
170
+ for (const fSummary of list) {
171
+ const fieldIR = resolveFieldIR(sdfModels, delta.tableName, fSummary.name, fSummary);
172
+ try {
173
+ buckets.addColumns.push(emitAddColumn(tableIR, fSummary.name, fieldIR, dialect));
174
+ summary.totalAdditions++;
175
+ } catch (err) {
176
+ skipped.push({
177
+ table: delta.tableName,
178
+ kind: 'add-column',
179
+ target: fSummary.name,
180
+ reason: 'emit-error',
181
+ description: err && err.message ? err.message : String(err)
182
+ });
183
+ }
184
+ }
185
+ }
186
+
187
+ function processFieldsOnlyInDb(delta, tableIR, dialect, options, buckets, skipped, summary) {
188
+ const list = delta.fields && Array.isArray(delta.fields.onlyInDb) ? delta.fields.onlyInDb : [];
189
+ for (const fSummary of list) {
190
+ if (!options.allowDrop) {
191
+ skipped.push({
192
+ table: delta.tableName,
193
+ kind: 'drop-column',
194
+ target: fSummary.name,
195
+ reason: 'requires --allow-drop',
196
+ description: `${delta.tableName}.${fSummary.name} exists in DB but not in SDF`
197
+ });
198
+ continue;
199
+ }
200
+ if (dialect.name === 'sqlite') {
201
+ skipped.push({
202
+ table: delta.tableName,
203
+ kind: 'drop-column',
204
+ target: fSummary.name,
205
+ reason: 'sqlite limitation',
206
+ description: `SQLite does not support DROP COLUMN without table rebuild (${delta.tableName}.${fSummary.name})`
207
+ });
208
+ continue;
209
+ }
210
+ try {
211
+ buckets.dropColumns.push(emitDropColumn(tableIR, fSummary.name, dialect));
212
+ summary.totalDeletions++;
213
+ } catch (err) {
214
+ skipped.push({
215
+ table: delta.tableName,
216
+ kind: 'drop-column',
217
+ target: fSummary.name,
218
+ reason: 'emit-error',
219
+ description: err && err.message ? err.message : String(err)
220
+ });
221
+ }
222
+ }
223
+ }
224
+
225
+ function processFieldsMismatched(delta, tableIR, dialect, options, buckets, skipped, sdfModels, summary) {
226
+ const list = delta.fields && Array.isArray(delta.fields.mismatched) ? delta.fields.mismatched : [];
227
+ for (const m of list) {
228
+ const cls = classifyMismatchReasons(m.reasons);
229
+
230
+ // Type change: defer (butuh data conversion strategy)
231
+ if (cls.hasType) {
232
+ skipped.push({
233
+ table: delta.tableName,
234
+ kind: 'modify-type',
235
+ target: m.name,
236
+ reason: 'deferred',
237
+ description: `Type change for ${delta.tableName}.${m.name} requires explicit data conversion strategy (deferred from MVP)`
238
+ });
239
+ continue;
240
+ }
241
+ // Default change: defer
242
+ if (cls.hasDefault) {
243
+ skipped.push({
244
+ table: delta.tableName,
245
+ kind: 'modify-default',
246
+ target: m.name,
247
+ reason: 'deferred',
248
+ description: `Default value change for ${delta.tableName}.${m.name} deferred from MVP`
249
+ });
250
+ continue;
251
+ }
252
+ // Precision / scale: defer (mirip type change)
253
+ if (cls.hasPrecision || cls.hasScale) {
254
+ skipped.push({
255
+ table: delta.tableName,
256
+ kind: 'modify-precision',
257
+ target: m.name,
258
+ reason: 'deferred',
259
+ description: `Precision/scale change for ${delta.tableName}.${m.name} deferred from MVP`
260
+ });
261
+ continue;
262
+ }
263
+
264
+ const changeNullable = cls.hasNullable;
265
+ const changeLength = cls.hasLength;
266
+ if (!changeNullable && !changeLength) {
267
+ // Nothing actionable
268
+ continue;
269
+ }
270
+
271
+ if (!options.allowModify) {
272
+ const detailParts = [];
273
+ if (changeLength) detailParts.push('length');
274
+ if (changeNullable) detailParts.push('nullable');
275
+ skipped.push({
276
+ table: delta.tableName,
277
+ kind: 'modify-column',
278
+ target: m.name,
279
+ reason: 'requires --allow-modify',
280
+ description: `${delta.tableName}.${m.name} ${detailParts.join('/')} drift requires --allow-modify`
281
+ });
282
+ continue;
283
+ }
284
+
285
+ if (dialect.name === 'sqlite') {
286
+ skipped.push({
287
+ table: delta.tableName,
288
+ kind: 'modify-column',
289
+ target: m.name,
290
+ reason: 'sqlite limitation',
291
+ description: `SQLite does not support ALTER COLUMN without table rebuild (${delta.tableName}.${m.name})`
292
+ });
293
+ continue;
294
+ }
295
+
296
+ const fieldIR = resolveFieldIR(sdfModels, delta.tableName, m.name, m.sdf);
297
+ try {
298
+ const statements = emitModifyColumn(tableIR, m.name, fieldIR, { changeNullable, changeLength }, dialect);
299
+ for (const s of statements) {
300
+ buckets.modifyColumns.push(s);
301
+ summary.totalModifications++;
302
+ }
303
+ } catch (err) {
304
+ skipped.push({
305
+ table: delta.tableName,
306
+ kind: 'modify-column',
307
+ target: m.name,
308
+ reason: 'emit-error',
309
+ description: err && err.message ? err.message : String(err)
310
+ });
311
+ }
312
+ }
313
+ }
314
+
315
+ function processIndexes(delta, tableIR, dialect, options, buckets, skipped, summary) {
316
+ if (!delta.indexes) return;
317
+
318
+ const addList = Array.isArray(delta.indexes.onlyInSdf) ? delta.indexes.onlyInSdf : [];
319
+ for (const idx of addList) {
320
+ const cols = Array.isArray(idx.columns) ? idx.columns : [];
321
+ if (cols.length === 0) continue;
322
+ try {
323
+ buckets.createIndexes.push(emitCreateIndex(tableIR, cols, dialect));
324
+ summary.totalAdditions++;
325
+ } catch (err) {
326
+ skipped.push({
327
+ table: delta.tableName,
328
+ kind: 'create-index',
329
+ target: cols.join(','),
330
+ reason: 'emit-error',
331
+ description: err && err.message ? err.message : String(err)
332
+ });
333
+ }
334
+ }
335
+
336
+ const dropList = Array.isArray(delta.indexes.onlyInDb) ? delta.indexes.onlyInDb : [];
337
+ for (const idx of dropList) {
338
+ const cols = Array.isArray(idx.columns) ? idx.columns : [];
339
+ if (cols.length === 0) continue;
340
+ if (!options.allowDrop) {
341
+ skipped.push({
342
+ table: delta.tableName,
343
+ kind: 'drop-index',
344
+ target: cols.join(','),
345
+ reason: 'requires --allow-drop',
346
+ description: `Index on ${delta.tableName}(${cols.join(', ')}) exists in DB but not in SDF`
347
+ });
348
+ continue;
349
+ }
350
+ try {
351
+ buckets.dropIndexes.push(emitDropIndex(tableIR, cols, dialect, { name: idx.name }));
352
+ summary.totalDeletions++;
353
+ } catch (err) {
354
+ skipped.push({
355
+ table: delta.tableName,
356
+ kind: 'drop-index',
357
+ target: cols.join(','),
358
+ reason: 'emit-error',
359
+ description: err && err.message ? err.message : String(err)
360
+ });
361
+ }
362
+ }
363
+ }
364
+
365
+ function processUniques(delta, tableIR, dialect, options, buckets, skipped, summary) {
366
+ if (!delta.uniques) return;
367
+
368
+ const addList = Array.isArray(delta.uniques.onlyInSdf) ? delta.uniques.onlyInSdf : [];
369
+ for (const uq of addList) {
370
+ const cols = Array.isArray(uq.columns) ? uq.columns : [];
371
+ if (cols.length === 0) continue;
372
+ try {
373
+ buckets.addUniques.push(emitAddUnique(tableIR, cols, dialect));
374
+ summary.totalAdditions++;
375
+ } catch (err) {
376
+ skipped.push({
377
+ table: delta.tableName,
378
+ kind: 'add-unique',
379
+ target: cols.join(','),
380
+ reason: 'emit-error',
381
+ description: err && err.message ? err.message : String(err)
382
+ });
383
+ }
384
+ }
385
+
386
+ const dropList = Array.isArray(delta.uniques.onlyInDb) ? delta.uniques.onlyInDb : [];
387
+ for (const uq of dropList) {
388
+ const cols = Array.isArray(uq.columns) ? uq.columns : [];
389
+ if (cols.length === 0) continue;
390
+ if (!options.allowDrop) {
391
+ skipped.push({
392
+ table: delta.tableName,
393
+ kind: 'drop-unique',
394
+ target: cols.join(','),
395
+ reason: 'requires --allow-drop',
396
+ description: `Unique on ${delta.tableName}(${cols.join(', ')}) exists in DB but not in SDF`
397
+ });
398
+ continue;
399
+ }
400
+ try {
401
+ buckets.dropConstraints.push(emitDropUnique(tableIR, cols, dialect, { name: uq.name }));
402
+ summary.totalDeletions++;
403
+ } catch (err) {
404
+ skipped.push({
405
+ table: delta.tableName,
406
+ kind: 'drop-unique',
407
+ target: cols.join(','),
408
+ reason: 'emit-error',
409
+ description: err && err.message ? err.message : String(err)
410
+ });
411
+ }
412
+ }
413
+ }
414
+
415
+ function noteDeferredSections(delta, skipped) {
416
+ // PK changes — deferred dari MVP karena butuh rebuild table.
417
+ if (delta.primaryKey && delta.primaryKey.match === false) {
418
+ const sdf = Array.isArray(delta.primaryKey.sdf) ? delta.primaryKey.sdf : [];
419
+ const db = Array.isArray(delta.primaryKey.db) ? delta.primaryKey.db : [];
420
+ skipped.push({
421
+ table: delta.tableName,
422
+ kind: 'primary-key',
423
+ target: `pk(${sdf.join(',') || '∅'} vs ${db.join(',') || '∅'})`,
424
+ reason: 'deferred',
425
+ description: `Primary key changes for ${delta.tableName} deferred from MVP (requires table rebuild)`
426
+ });
427
+ }
428
+
429
+ if (delta.foreignKeys) {
430
+ const fkOnlySdf = Array.isArray(delta.foreignKeys.onlyInSdf) ? delta.foreignKeys.onlyInSdf : [];
431
+ const fkOnlyDb = Array.isArray(delta.foreignKeys.onlyInDb) ? delta.foreignKeys.onlyInDb : [];
432
+ const fkMismatch = Array.isArray(delta.foreignKeys.mismatched) ? delta.foreignKeys.mismatched : [];
433
+ for (const fk of fkOnlySdf) {
434
+ skipped.push({
435
+ table: delta.tableName,
436
+ kind: 'foreign-key',
437
+ target: `${fk.localKey} -> ${fk.target}.${fk.references}`,
438
+ reason: 'deferred',
439
+ description: `Foreign key changes deferred from MVP (additive ${delta.tableName}.${fk.localKey})`
440
+ });
441
+ }
442
+ for (const fk of fkOnlyDb) {
443
+ skipped.push({
444
+ table: delta.tableName,
445
+ kind: 'foreign-key',
446
+ target: `${fk.localKey} -> ${fk.target}.${fk.references}`,
447
+ reason: 'deferred',
448
+ description: `Foreign key changes deferred from MVP (drop ${delta.tableName}.${fk.localKey})`
449
+ });
450
+ }
451
+ for (const fk of fkMismatch) {
452
+ skipped.push({
453
+ table: delta.tableName,
454
+ kind: 'foreign-key',
455
+ target: `${fk.localKey} -> ${fk.target}.${fk.references}`,
456
+ reason: 'deferred',
457
+ description: `Foreign key action change deferred from MVP (${delta.tableName}.${fk.localKey})`
458
+ });
459
+ }
460
+ }
461
+
462
+ if (delta.checks) {
463
+ const cOnlySdf = Array.isArray(delta.checks.onlyInSdf) ? delta.checks.onlyInSdf : [];
464
+ const cOnlyDb = Array.isArray(delta.checks.onlyInDb) ? delta.checks.onlyInDb : [];
465
+ for (const c of cOnlySdf) {
466
+ skipped.push({
467
+ table: delta.tableName,
468
+ kind: 'check-constraint',
469
+ target: c.field || c.expression || '(check)',
470
+ reason: 'deferred',
471
+ description: `Check constraint changes deferred from MVP (additive ${delta.tableName})`
472
+ });
473
+ }
474
+ for (const c of cOnlyDb) {
475
+ skipped.push({
476
+ table: delta.tableName,
477
+ kind: 'check-constraint',
478
+ target: c.field || c.expression || '(check)',
479
+ reason: 'deferred',
480
+ description: `Check constraint changes deferred from MVP (drop ${delta.tableName})`
481
+ });
482
+ }
483
+ }
484
+ }
485
+
486
+ // ─────────────────────────────────────────────────────────────
487
+ // Public API
488
+ // ─────────────────────────────────────────────────────────────
489
+
490
+ /**
491
+ * Generate ALTER statement dari delta diff-engine per model.
492
+ *
493
+ * @param {Array<Object>} deltas - Output dari diffModels()
494
+ * @param {Object} options
495
+ * @param {string} options.dialect - 'postgres' | 'mysql' | 'oracle' | 'sqlite'
496
+ * @param {boolean} [options.allowDrop=false]
497
+ * @param {boolean} [options.allowModify=false]
498
+ * @param {Map|Object|Array} [options.sdfModels] - SDF model map untuk resolve field IR penuh
499
+ * @returns {{
500
+ * statements: Array<{ table: string, sql: string }>,
501
+ * statementsByTable: Object<string, string[]>,
502
+ * skipped: Array<{ table, kind, target, reason, description }>,
503
+ * summary: { totalAdditions, totalModifications, totalDeletions, totalSkipped, tablesAffected }
504
+ * }}
505
+ */
506
+ function generateAlterStatements(deltas, options) {
507
+ if (!Array.isArray(deltas)) {
508
+ throw new Error('generateAlterStatements: deltas must be an array');
509
+ }
510
+ if (!options || typeof options !== 'object') {
511
+ throw new Error('generateAlterStatements: options object is required');
512
+ }
513
+ if (!VALID_DIALECTS.includes(options.dialect)) {
514
+ throw new Error(
515
+ `generateAlterStatements: invalid dialect '${options.dialect}'. ` +
516
+ `Supported: ${VALID_DIALECTS.join(', ')}`
517
+ );
518
+ }
519
+
520
+ const allowDrop = options.allowDrop === true;
521
+ const allowModify = options.allowModify === true;
522
+ const sdfModels = options.sdfModels || null;
523
+ const dialect = getDialect(options.dialect);
524
+
525
+ const statements = [];
526
+ const statementsByTable = {};
527
+ const skipped = [];
528
+ const summary = {
529
+ totalAdditions: 0,
530
+ totalModifications: 0,
531
+ totalDeletions: 0,
532
+ totalSkipped: 0,
533
+ tablesAffected: 0
534
+ };
535
+
536
+ const tablesWithStatements = new Set();
537
+ const localOptions = { allowDrop, allowModify };
538
+
539
+ for (const delta of deltas) {
540
+ if (!delta || typeof delta.tableName !== 'string') continue;
541
+ if (!delta.hasDrift) continue;
542
+
543
+ const tableIR = resolveTableIR(sdfModels, delta);
544
+ const buckets = buildBuckets();
545
+
546
+ processFieldsOnlyInSdf(delta, tableIR, dialect, buckets, skipped, sdfModels, summary);
547
+ processFieldsOnlyInDb(delta, tableIR, dialect, localOptions, buckets, skipped, summary);
548
+ processFieldsMismatched(delta, tableIR, dialect, localOptions, buckets, skipped, sdfModels, summary);
549
+ processIndexes(delta, tableIR, dialect, localOptions, buckets, skipped, summary);
550
+ processUniques(delta, tableIR, dialect, localOptions, buckets, skipped, summary);
551
+ noteDeferredSections(delta, skipped);
552
+
553
+ const flat = flattenBuckets(buckets);
554
+ if (flat.length > 0) {
555
+ tablesWithStatements.add(delta.tableName);
556
+ statementsByTable[delta.tableName] = flat.slice();
557
+ for (const sql of flat) {
558
+ statements.push({ table: delta.tableName, sql });
559
+ }
560
+ }
561
+ }
562
+
563
+ summary.totalSkipped = skipped.length;
564
+ summary.tablesAffected = tablesWithStatements.size;
565
+
566
+ return {
567
+ statements,
568
+ statementsByTable,
569
+ skipped,
570
+ summary
571
+ };
572
+ }
573
+
574
+ module.exports = {
575
+ generateAlterStatements,
576
+ _internal: {
577
+ classifyMismatchReasons,
578
+ buildFieldFromSummary,
579
+ resolveTableIR,
580
+ lookupSdfIR
581
+ }
582
+ };