@restforgejs/platform 4.1.1 → 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.
- package/bin/sdf-tools.exe +0 -0
- package/build-info.json +2 -2
- package/cli/consumer-deploy.js +1 -1
- package/cli/consumer.js +1 -1
- package/generators/cli/endpoint/create.js +42 -3
- package/generators/cli/schema/apply.js +525 -0
- package/generators/cli/schema/diff.js +321 -0
- package/generators/cli/schema/generate-ddl.js +7 -10
- package/generators/cli/schema/init.js +95 -172
- package/generators/cli/schema/migrate.js +10 -16
- package/generators/cli/schema/models.js +8 -12
- package/generators/cli/schema/template.js +222 -0
- package/generators/cli/schema/validate.js +8 -12
- package/generators/cli-entry.js +17 -2
- package/generators/lib/dbschema-kit/apply-engine.js +582 -0
- package/generators/lib/dbschema-kit/diff-engine.js +703 -0
- package/generators/lib/dbschema-kit/diff-reporter.js +272 -0
- package/generators/lib/dbschema-kit/emitters/alter-table.js +275 -0
- package/generators/lib/payload/endpoint-schema-validator.js +171 -0
- package/generators/lib/payload/payload-runner.js +137 -220
- package/generators/lib/payload/schema-diff.js +277 -0
- package/generators/lib/utils/audit-columns.js +181 -0
- package/generators/lib/utils/cli-output.js +17 -0
- package/generators/lib/utils/database-introspector.js +16 -13
- package/integrity-manifest.json +8 -8
- package/package.json +1 -1
- package/scripts/verify-integrity.js +1 -1
- package/server.js +1 -1
- package/src/components/handlers/adjust_handler.js +1 -1
- package/src/components/handlers/audit_handler.js +1 -1
- package/src/components/handlers/delete_handler.js +1 -1
- package/src/components/handlers/export_handler.js +1 -1
- package/src/components/handlers/import_handler.js +1 -1
- package/src/components/handlers/insert_handler.js +1 -1
- package/src/components/handlers/update_handler.js +1 -1
- package/src/components/handlers/upload_handler.js +1 -1
- package/src/components/handlers/workflow_handler.js +1 -1
- package/src/components/integrations/webhook.js +1 -1
- package/src/consumers/baseConsumer.js +1 -1
- package/src/consumers/declarativeMapper.js +1 -1
- package/src/consumers/handlers/apiHandler.js +1 -1
- package/src/consumers/handlers/consoleHandler.js +1 -1
- package/src/consumers/handlers/databaseHandler.js +1 -1
- package/src/consumers/handlers/index.js +1 -1
- package/src/consumers/handlers/kafkaHandler.js +1 -1
- package/src/consumers/index.js +1 -1
- package/src/consumers/messageTransformer.js +1 -1
- package/src/consumers/validator.js +1 -1
- package/src/core/db/dialect/base-dialect.js +1 -1
- package/src/core/db/dialect/index.js +1 -1
- package/src/core/db/dialect/mysql-dialect.js +1 -1
- package/src/core/db/dialect/oracle-dialect.js +1 -1
- package/src/core/db/dialect/postgres-dialect.js +1 -1
- package/src/core/db/dialect/sqlite-dialect.js +1 -1
- package/src/core/db/flatten-helper.js +1 -1
- package/src/core/db/query-builder-error.js +1 -1
- package/src/core/db/query-builder.js +1 -1
- package/src/core/db/relation-helper.js +1 -1
- package/src/core/handlers/delete_handler.js +1 -1
- package/src/core/handlers/insert_handler.js +1 -1
- package/src/core/handlers/update_handler.js +1 -1
- package/src/core/models/base-model.js +1 -1
- package/src/core/utils/cache-manager.js +1 -1
- package/src/core/utils/component-engine.js +1 -1
- package/src/core/utils/context-builder.js +1 -1
- package/src/core/utils/datetime-formatter.js +1 -1
- package/src/core/utils/datetime-parser.js +1 -1
- package/src/core/utils/db.js +1 -1
- package/src/core/utils/logger.js +1 -1
- package/src/core/utils/payload-loader.js +1 -1
- package/src/core/utils/security-checks.js +1 -1
- package/src/middleware/body-options.js +1 -1
- package/src/middleware/cors.js +1 -1
- package/src/middleware/idempotency.js +1 -1
- package/src/middleware/rate-limiter.js +1 -1
- package/src/middleware/request-logger.js +1 -1
- package/src/middleware/security-headers.js +1 -1
- package/src/models/base-model-mysql.js +1 -1
- package/src/models/base-model-oracle.js +1 -1
- package/src/models/base-model-sqlite.js +1 -1
- package/src/models/base-model.js +1 -1
- package/src/pro/caching/redis-client.js +1 -1
- package/src/pro/caching/redis-helper.js +1 -1
- package/src/pro/consumers/baseConsumer.js +1 -1
- package/src/pro/consumers/declarativeMapper.js +1 -1
- package/src/pro/consumers/handlers/apiHandler.js +1 -1
- package/src/pro/consumers/handlers/consoleHandler.js +1 -1
- package/src/pro/consumers/handlers/databaseHandler.js +1 -1
- package/src/pro/consumers/handlers/index.js +1 -1
- package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
- package/src/pro/consumers/index.js +1 -1
- package/src/pro/consumers/messageTransformer.js +1 -1
- package/src/pro/consumers/validator.js +1 -1
- package/src/pro/database/base-model-mysql.js +1 -1
- package/src/pro/database/base-model-oracle.js +1 -1
- package/src/pro/database/base-model-sqlite.js +1 -1
- package/src/pro/database/db-mysql.js +1 -1
- package/src/pro/database/db-oracle.js +1 -1
- package/src/pro/database/db-sqlite.js +1 -1
- package/src/pro/excel/excel-generator.js +1 -1
- package/src/pro/excel/excel-parser.js +1 -1
- package/src/pro/excel/export-service.js +1 -1
- package/src/pro/excel/export_handler.js +1 -1
- package/src/pro/excel/import-service.js +1 -1
- package/src/pro/excel/import-validator.js +1 -1
- package/src/pro/excel/import_handler.js +1 -1
- package/src/pro/excel/upsert-builder.js +1 -1
- package/src/pro/idgen/idgen-routes.js +1 -1
- package/src/pro/integrations/lookup-resolver.js +1 -1
- package/src/pro/integrations/upload-handler-v2.js +1 -1
- package/src/pro/integrations/upload-handler.js +1 -1
- package/src/pro/integrations/webhook.js +1 -1
- package/src/pro/locking/lock-routes.js +1 -1
- package/src/pro/locking/resource-lock-manager.js +1 -1
- package/src/pro/messaging/kafkaConsumerService.js +1 -1
- package/src/pro/messaging/kafkaService.js +1 -1
- package/src/pro/messaging/messagehubService.js +1 -1
- package/src/pro/messaging/rabbitmqService.js +1 -1
- package/src/pro/scheduler/job-manager.js +1 -1
- package/src/pro/scheduler/job-routes.js +1 -1
- package/src/pro/scheduler/job-validator.js +1 -1
- package/src/pro/storage/base-storage-provider.js +1 -1
- package/src/pro/storage/file-metadata-helper.js +1 -1
- package/src/pro/storage/index.js +1 -1
- package/src/pro/storage/local-storage-provider.js +1 -1
- package/src/pro/storage/s3-storage-provider.js +1 -1
- package/src/pro/storage/upload-cleanup-job.js +1 -1
- package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
- package/src/pro/storage/upload-pending-tracker.js +1 -1
- package/src/pro/websocket/broadcast-helper.js +1 -1
- package/src/pro/websocket/index.js +1 -1
- package/src/pro/websocket/livesync-server.js +1 -1
- package/src/pro/websocket/ws-broadcaster.js +1 -1
- package/src/services/export-service.js +1 -1
- package/src/services/import-service.js +1 -1
- package/src/services/kafkaConsumerService.js +1 -1
- package/src/services/kafkaService.js +1 -1
- package/src/services/messagehubService.js +1 -1
- package/src/services/rabbitmqService.js +1 -1
- package/src/utils/cache-invalidation-registry.js +1 -1
- package/src/utils/cache-manager.js +1 -1
- package/src/utils/component-engine.js +1 -1
- package/src/utils/config-extractor.js +1 -1
- package/src/utils/consumerLogger.js +1 -1
- package/src/utils/context-builder.js +1 -1
- package/src/utils/dashboard-helpers.js +1 -1
- package/src/utils/dateHelper.js +1 -1
- package/src/utils/datetime-formatter.js +1 -1
- package/src/utils/datetime-parser.js +1 -1
- package/src/utils/db-bootstrap.js +1 -1
- package/src/utils/db-mysql.js +1 -1
- package/src/utils/db-oracle.js +1 -1
- package/src/utils/db-sqlite.js +1 -1
- package/src/utils/db.js +1 -1
- package/src/utils/demo-generator.js +1 -1
- package/src/utils/excel-generator.js +1 -1
- package/src/utils/excel-parser.js +1 -1
- package/src/utils/file-watcher.js +1 -1
- package/src/utils/id-generator.js +1 -1
- package/src/utils/idempotency-manager.js +1 -1
- package/src/utils/import-validator.js +1 -1
- package/src/utils/license-client.js +1 -1
- package/src/utils/lock-manager.js +1 -1
- package/src/utils/logger.js +1 -1
- package/src/utils/lookup-resolver.js +1 -1
- package/src/utils/payload-loader.js +1 -1
- package/src/utils/processor-response.js +1 -1
- package/src/utils/rabbitmq.js +1 -1
- package/src/utils/redis-client.js +1 -1
- package/src/utils/redis-helper.js +1 -1
- package/src/utils/request-scope.js +1 -1
- package/src/utils/security-checks.js +1 -1
- package/src/utils/service-resolver.js +1 -1
- package/src/utils/shutdown-coordinator.js +1 -1
- package/src/utils/trusted-keys.js +1 -1
- package/src/utils/upload-handler.js +1 -1
- package/src/utils/upsert-builder.js +1 -1
- 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
|
+
};
|