@restforgejs/platform 5.0.0 → 5.0.3
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/build-info.json +2 -2
- package/cli/consumer-deploy.js +1 -1
- package/cli/consumer.js +1 -1
- package/generators/cli/init.js +4 -104
- package/generators/cli/payload/migrate.js +1 -1
- package/generators/cli/schema/list.js +82 -18
- package/generators/cli/schema/migrate.js +23 -3
- package/generators/lib/dbschema-kit/diff-engine.js +715 -715
- package/generators/lib/migrate/field-type-resolver.js +9 -3
- package/generators/lib/migrate/migrate-runner.js +244 -38
- package/generators/lib/migrate/naming.js +9 -0
- package/generators/lib/payload/payload-runner.js +20 -0
- package/generators/lib/templates/dashboard-catalog.js +1 -1
- package/generators/lib/templates/db-connection-env.js +1 -1
- package/generators/lib/templates/dbschema-catalog.js +1 -1
- package/generators/lib/templates/field-validation-catalog.js +1 -1
- package/generators/lib/templates/mysql-template.js +1 -1
- package/generators/lib/templates/oracle-template.js +1 -1
- package/generators/lib/templates/postgres-template.js +1 -1
- package/generators/lib/templates/query-declarative-catalog.js +1 -1
- package/generators/lib/templates/sqlite-template.js +1 -1
- package/integrity-manifest.json +18 -18
- 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
|
@@ -1,715 +1,715 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Diff Engine — compare dua IR (Intermediate Representation) dari dbschema-kit
|
|
5
|
-
* dan hasilkan structured delta. Operasi bersifat semantic-level, bukan
|
|
6
|
-
* text-level: nama field, tipe ter-normalisasi, length, nullability, primary
|
|
7
|
-
* key, index, unique constraint, dan foreign key di-compare langsung.
|
|
8
|
-
*
|
|
9
|
-
* Input IR diharapkan kompatibel dengan output `defineModel()` (SDF) maupun
|
|
10
|
-
* `mapTableMetaToIR()` (database introspection). Diff engine tidak melakukan
|
|
11
|
-
* I/O dan tidak memodifikasi input.
|
|
12
|
-
*
|
|
13
|
-
* @module lib/dbschema-kit/diff-engine
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
// ─────────────────────────────────────────────────────────────
|
|
17
|
-
// Type normalization (alias mapping per dialect)
|
|
18
|
-
// ─────────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
// Mapping kanonikal dari semua alias type (SDF + DB) ke representasi
|
|
21
|
-
// internal yang dipakai saat compare. Mapping ini intentionally konservatif:
|
|
22
|
-
// hanya alias yang umum di-emit oleh dialect driver yang di-cover.
|
|
23
|
-
const TYPE_ALIAS_MAP = {
|
|
24
|
-
// String varieties
|
|
25
|
-
'string': 'string',
|
|
26
|
-
'varchar': 'string',
|
|
27
|
-
'character varying': 'string',
|
|
28
|
-
'character': 'string',
|
|
29
|
-
'char': 'string',
|
|
30
|
-
'nvarchar': 'string',
|
|
31
|
-
'nchar': 'string',
|
|
32
|
-
// Text
|
|
33
|
-
'text': 'text',
|
|
34
|
-
'clob': 'text',
|
|
35
|
-
'nclob': 'text',
|
|
36
|
-
'mediumtext': 'text',
|
|
37
|
-
'longtext': 'text',
|
|
38
|
-
// Integer
|
|
39
|
-
'integer': 'integer',
|
|
40
|
-
'int': 'integer',
|
|
41
|
-
'int4': 'integer',
|
|
42
|
-
'int2': 'integer',
|
|
43
|
-
'smallint': 'integer',
|
|
44
|
-
'mediumint': 'integer',
|
|
45
|
-
// Bigint
|
|
46
|
-
'bigint': 'bigint',
|
|
47
|
-
'int8': 'bigint',
|
|
48
|
-
// Decimal / numeric
|
|
49
|
-
'decimal': 'decimal',
|
|
50
|
-
'numeric': 'decimal',
|
|
51
|
-
'number': 'decimal',
|
|
52
|
-
// Boolean
|
|
53
|
-
'boolean': 'boolean',
|
|
54
|
-
'bool': 'boolean',
|
|
55
|
-
'tinyint(1)': 'boolean',
|
|
56
|
-
// Date / time
|
|
57
|
-
'date': 'date',
|
|
58
|
-
'timestamp': 'timestamp',
|
|
59
|
-
'timestamp without time zone': 'timestamp',
|
|
60
|
-
'timestamp with time zone': 'timestamp',
|
|
61
|
-
'timestamptz': 'timestamp',
|
|
62
|
-
'datetime': 'timestamp',
|
|
63
|
-
// UUID
|
|
64
|
-
'uuid': 'uuid',
|
|
65
|
-
// JSON
|
|
66
|
-
'json': 'json',
|
|
67
|
-
'jsonb': 'json'
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
function normalizeType(rawType) {
|
|
71
|
-
if (rawType === undefined || rawType === null) return null;
|
|
72
|
-
const key = String(rawType).toLowerCase().trim();
|
|
73
|
-
if (TYPE_ALIAS_MAP[key]) return TYPE_ALIAS_MAP[key];
|
|
74
|
-
return key;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ─────────────────────────────────────────────────────────────
|
|
78
|
-
// Helpers
|
|
79
|
-
// ─────────────────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
function isPlainObject(v) {
|
|
82
|
-
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function arrayEquals(a, b) {
|
|
86
|
-
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
87
|
-
if (a.length !== b.length) return false;
|
|
88
|
-
for (let i = 0; i < a.length; i++) {
|
|
89
|
-
if (a[i] !== b[i]) return false;
|
|
90
|
-
}
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function sortedCopy(arr) {
|
|
95
|
-
return Array.isArray(arr) ? arr.slice().sort() : [];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function isNullable(field) {
|
|
99
|
-
// notnull=true berarti tidak nullable; notnull=undefined berarti default nullable.
|
|
100
|
-
return field && field.notnull === true ? false : true;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function resolvePrimaryKey(ir) {
|
|
104
|
-
// IR convention: primaryKey array hanya terisi untuk composite PK.
|
|
105
|
-
// Single PK ditandai via fields[name].pk = true.
|
|
106
|
-
if (!ir || !isPlainObject(ir)) return [];
|
|
107
|
-
if (Array.isArray(ir.primaryKey) && ir.primaryKey.length > 0) {
|
|
108
|
-
return ir.primaryKey.slice();
|
|
109
|
-
}
|
|
110
|
-
const out = [];
|
|
111
|
-
const fields = isPlainObject(ir.fields) ? ir.fields : {};
|
|
112
|
-
for (const [name, field] of Object.entries(fields)) {
|
|
113
|
-
if (field && field.pk === true) out.push(name);
|
|
114
|
-
}
|
|
115
|
-
return out;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ─────────────────────────────────────────────────────────────
|
|
119
|
-
// Field comparison
|
|
120
|
-
// ─────────────────────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
function buildFieldSummary(name, field) {
|
|
123
|
-
const out = { name };
|
|
124
|
-
if (field) {
|
|
125
|
-
if (field.type !== undefined) out.type = field.type;
|
|
126
|
-
if (field.length !== undefined) out.length = field.length;
|
|
127
|
-
if (field.precision !== undefined) out.precision = field.precision;
|
|
128
|
-
if (field.scale !== undefined) out.scale = field.scale;
|
|
129
|
-
out.nullable = isNullable(field);
|
|
130
|
-
} else {
|
|
131
|
-
out.nullable = true;
|
|
132
|
-
}
|
|
133
|
-
return out;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function compareFieldDetails(sdfField, dbField) {
|
|
137
|
-
const reasons = [];
|
|
138
|
-
|
|
139
|
-
const sdfType = normalizeType(sdfField && sdfField.type);
|
|
140
|
-
const dbType = normalizeType(dbField && dbField.type);
|
|
141
|
-
if (sdfType !== dbType) {
|
|
142
|
-
reasons.push(`type: SDF='${sdfField && sdfField.type}' vs DB='${dbField && dbField.type}'`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Length: relevan hanya untuk type=string. Untuk type lain (text/integer/dll.)
|
|
146
|
-
// length umumnya tidak digunakan dan diabaikan agar tidak false-positive.
|
|
147
|
-
if (sdfType === 'string' && dbType === 'string') {
|
|
148
|
-
const sdfLen = sdfField && sdfField.length;
|
|
149
|
-
const dbLen = dbField && dbField.length;
|
|
150
|
-
if (sdfLen !== undefined && dbLen !== undefined && Number(sdfLen) !== Number(dbLen)) {
|
|
151
|
-
reasons.push(`length: SDF=${sdfLen} vs DB=${dbLen}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Precision / scale untuk decimal.
|
|
156
|
-
if (sdfType === 'decimal' && dbType === 'decimal') {
|
|
157
|
-
const sdfP = sdfField && sdfField.precision;
|
|
158
|
-
const dbP = dbField && dbField.precision;
|
|
159
|
-
if (sdfP !== undefined && dbP !== undefined && Number(sdfP) !== Number(dbP)) {
|
|
160
|
-
reasons.push(`precision: SDF=${sdfP} vs DB=${dbP}`);
|
|
161
|
-
}
|
|
162
|
-
const sdfS = sdfField && sdfField.scale;
|
|
163
|
-
const dbS = dbField && dbField.scale;
|
|
164
|
-
if (sdfS !== undefined && dbS !== undefined && Number(sdfS) !== Number(dbS)) {
|
|
165
|
-
reasons.push(`scale: SDF=${sdfS} vs DB=${dbS}`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const sdfNullable = isNullable(sdfField);
|
|
170
|
-
const dbNullable = isNullable(dbField);
|
|
171
|
-
if (sdfNullable !== dbNullable) {
|
|
172
|
-
reasons.push(`nullable: SDF=${sdfNullable} vs DB=${dbNullable}`);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const defaultReason = compareDefaultValue(sdfField, dbField);
|
|
176
|
-
if (defaultReason) reasons.push(defaultReason);
|
|
177
|
-
|
|
178
|
-
return reasons;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ─────────────────────────────────────────────────────────────
|
|
182
|
-
// Default value comparison
|
|
183
|
-
// ─────────────────────────────────────────────────────────────
|
|
184
|
-
|
|
185
|
-
// Kanonikal payload dari struct default { kind, value, name, args } untuk
|
|
186
|
-
// strict compare. Output adalah string yang stable (deterministic) sehingga
|
|
187
|
-
// equality check cukup dengan ===.
|
|
188
|
-
function canonicalDefaultPayload(def) {
|
|
189
|
-
if (!def || typeof def !== 'object') return '';
|
|
190
|
-
if (def.kind === 'literal') {
|
|
191
|
-
// JSON.stringify menjaga distingsi tipe (string vs number vs boolean).
|
|
192
|
-
return JSON.stringify(def.value);
|
|
193
|
-
}
|
|
194
|
-
if (def.kind === 'function') {
|
|
195
|
-
const name = String(def.name || '').toLowerCase();
|
|
196
|
-
const args = Array.isArray(def.args) ? def.args : [];
|
|
197
|
-
return `${name}(${args.join(',')})`;
|
|
198
|
-
}
|
|
199
|
-
if (def.kind === 'constant') {
|
|
200
|
-
return String(def.value == null ? '' : def.value).toLowerCase();
|
|
201
|
-
}
|
|
202
|
-
return '';
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Weak normalization untuk _defaultComment dan fallback string compare.
|
|
206
|
-
// Whitespace di-collapse, lowercase, lalu trim agar variasi cosmetic tidak
|
|
207
|
-
// memicu false-positive.
|
|
208
|
-
function normalizeWeakDefault(str) {
|
|
209
|
-
return String(str == null ? '' : str).toLowerCase().replace(/\s+/g, ' ').trim();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function describeDefault(def, comment) {
|
|
213
|
-
if (def && typeof def === 'object') {
|
|
214
|
-
return canonicalDefaultPayload(def);
|
|
215
|
-
}
|
|
216
|
-
if (comment) return `(comment) ${normalizeWeakDefault(comment)}`;
|
|
217
|
-
return '(none)';
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function compareDefaultValue(sdfField, dbField) {
|
|
221
|
-
const sdfDef = sdfField && sdfField.default;
|
|
222
|
-
const dbDef = dbField && dbField.default;
|
|
223
|
-
const sdfComment = sdfField && sdfField._defaultComment;
|
|
224
|
-
const dbComment = dbField && dbField._defaultComment;
|
|
225
|
-
|
|
226
|
-
const sdfHasStruct = sdfDef !== undefined && sdfDef !== null;
|
|
227
|
-
const dbHasStruct = dbDef !== undefined && dbDef !== null;
|
|
228
|
-
const sdfHasComment = !!sdfComment;
|
|
229
|
-
const dbHasComment = !!dbComment;
|
|
230
|
-
|
|
231
|
-
// Case 1: keduanya tidak punya default sama sekali → match.
|
|
232
|
-
if (!sdfHasStruct && !dbHasStruct && !sdfHasComment && !dbHasComment) {
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Case 2: salah satu sisi punya signal default (struct atau comment),
|
|
237
|
-
// sisi lain kosong total → drift.
|
|
238
|
-
const sdfHasAny = sdfHasStruct || sdfHasComment;
|
|
239
|
-
const dbHasAny = dbHasStruct || dbHasComment;
|
|
240
|
-
if (sdfHasAny !== dbHasAny) {
|
|
241
|
-
return `default: SDF=${describeDefault(sdfDef, sdfComment)} vs DB=${describeDefault(dbDef, dbComment)}`;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Case 3: keduanya punya struktur kanonikal → strict compare kind + value.
|
|
245
|
-
if (sdfHasStruct && dbHasStruct) {
|
|
246
|
-
if (sdfDef.kind !== dbDef.kind) {
|
|
247
|
-
return `default kind: SDF='${sdfDef.kind}' vs DB='${dbDef.kind}'`;
|
|
248
|
-
}
|
|
249
|
-
const sdfPayload = canonicalDefaultPayload(sdfDef);
|
|
250
|
-
const dbPayload = canonicalDefaultPayload(dbDef);
|
|
251
|
-
if (sdfPayload !== dbPayload) {
|
|
252
|
-
return `default: SDF=${sdfPayload} vs DB=${dbPayload}`;
|
|
253
|
-
}
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Case 4: minimal salah satu sisi hanya punya _defaultComment (atau struktur
|
|
258
|
-
// di satu sisi, comment di sisi lain). Fallback ke weak compare.
|
|
259
|
-
const sdfStr = normalizeWeakDefault(sdfHasStruct ? canonicalDefaultPayload(sdfDef) : sdfComment);
|
|
260
|
-
const dbStr = normalizeWeakDefault(dbHasStruct ? canonicalDefaultPayload(dbDef) : dbComment);
|
|
261
|
-
if (sdfStr !== dbStr) {
|
|
262
|
-
return `default (weak): SDF='${sdfStr}' vs DB='${dbStr}'`;
|
|
263
|
-
}
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function diffFields(sdfFields, dbFields) {
|
|
268
|
-
const sdf = isPlainObject(sdfFields) ? sdfFields : {};
|
|
269
|
-
const db = isPlainObject(dbFields) ? dbFields : {};
|
|
270
|
-
|
|
271
|
-
const sdfNames = Object.keys(sdf);
|
|
272
|
-
const dbNames = Object.keys(db);
|
|
273
|
-
|
|
274
|
-
const onlyInSdf = [];
|
|
275
|
-
const onlyInDb = [];
|
|
276
|
-
const mismatched = [];
|
|
277
|
-
|
|
278
|
-
for (const name of sdfNames) {
|
|
279
|
-
if (!(name in db)) {
|
|
280
|
-
onlyInSdf.push(buildFieldSummary(name, sdf[name]));
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
for (const name of dbNames) {
|
|
285
|
-
if (!(name in sdf)) {
|
|
286
|
-
onlyInDb.push(buildFieldSummary(name, db[name]));
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
for (const name of sdfNames) {
|
|
291
|
-
if (!(name in db)) continue;
|
|
292
|
-
const reasons = compareFieldDetails(sdf[name], db[name]);
|
|
293
|
-
if (reasons.length > 0) {
|
|
294
|
-
mismatched.push({
|
|
295
|
-
name,
|
|
296
|
-
sdf: buildFieldSummary(name, sdf[name]),
|
|
297
|
-
db: buildFieldSummary(name, db[name]),
|
|
298
|
-
reasons
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return { onlyInSdf, onlyInDb, mismatched };
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// ─────────────────────────────────────────────────────────────
|
|
307
|
-
// Primary key comparison
|
|
308
|
-
// ─────────────────────────────────────────────────────────────
|
|
309
|
-
|
|
310
|
-
function diffPrimaryKey(sdfIR, dbIR) {
|
|
311
|
-
const sdf = resolvePrimaryKey(sdfIR);
|
|
312
|
-
const db = resolvePrimaryKey(dbIR);
|
|
313
|
-
// PK match jika kolom sama persis (order-sensitive: PK order matters di SQL).
|
|
314
|
-
const match = arrayEquals(sdf, db);
|
|
315
|
-
return { sdf, db, match };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// ─────────────────────────────────────────────────────────────
|
|
319
|
-
// Collection diff (indexes, uniques, foreign keys)
|
|
320
|
-
// ─────────────────────────────────────────────────────────────
|
|
321
|
-
|
|
322
|
-
// Build map dari kolom set kanonikal. Bila item punya nama dan unik, name
|
|
323
|
-
// digunakan sebagai key; selain itu dipakai gabungan kolom yang ter-sort agar
|
|
324
|
-
// stable order columns tidak dianggap berbeda.
|
|
325
|
-
function buildItemMap(items, getKey) {
|
|
326
|
-
const map = new Map();
|
|
327
|
-
if (!Array.isArray(items)) return map;
|
|
328
|
-
for (const item of items) {
|
|
329
|
-
if (!item) continue;
|
|
330
|
-
const key = getKey(item);
|
|
331
|
-
if (!key) continue;
|
|
332
|
-
if (!map.has(key)) map.set(key, item);
|
|
333
|
-
}
|
|
334
|
-
return map;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function columnsKey(item) {
|
|
338
|
-
if (!item || !Array.isArray(item.columns)) return null;
|
|
339
|
-
return item.columns.slice().sort().join('|');
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function indexKey(item) {
|
|
343
|
-
return columnsKey(item);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function diffIndexes(sdfIndexes, dbIndexes) {
|
|
347
|
-
const sdfMap = buildItemMap(sdfIndexes, indexKey);
|
|
348
|
-
const dbMap = buildItemMap(dbIndexes, indexKey);
|
|
349
|
-
|
|
350
|
-
const onlyInSdf = [];
|
|
351
|
-
const onlyInDb = [];
|
|
352
|
-
const mismatched = [];
|
|
353
|
-
|
|
354
|
-
for (const [key, item] of sdfMap.entries()) {
|
|
355
|
-
if (!dbMap.has(key)) {
|
|
356
|
-
onlyInSdf.push({ columns: Array.isArray(item.columns) ? item.columns.slice() : [] });
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
for (const [key, item] of dbMap.entries()) {
|
|
360
|
-
if (!sdfMap.has(key)) {
|
|
361
|
-
onlyInDb.push({ columns: Array.isArray(item.columns) ? item.columns.slice() : [] });
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
// Mismatch: same column-set tapi order berbeda. MVP-nya kita treat
|
|
365
|
-
// matching column-set sebagai match dan tidak menandai order sebagai drift,
|
|
366
|
-
// karena index column order biasanya ditentukan oleh dialect optimizer.
|
|
367
|
-
// (Kebijakan ini dapat di-revisi fase berikutnya.)
|
|
368
|
-
|
|
369
|
-
return { onlyInSdf, onlyInDb, mismatched };
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function diffUniques(sdfUniques, dbUniques) {
|
|
373
|
-
// Strategi sama dengan index: match by column-set kanonikal.
|
|
374
|
-
const sdfMap = buildItemMap(sdfUniques, columnsKey);
|
|
375
|
-
const dbMap = buildItemMap(dbUniques, columnsKey);
|
|
376
|
-
|
|
377
|
-
const onlyInSdf = [];
|
|
378
|
-
const onlyInDb = [];
|
|
379
|
-
const mismatched = [];
|
|
380
|
-
|
|
381
|
-
for (const [key, item] of sdfMap.entries()) {
|
|
382
|
-
if (!dbMap.has(key)) {
|
|
383
|
-
onlyInSdf.push({ columns: Array.isArray(item.columns) ? item.columns.slice() : [] });
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
for (const [key, item] of dbMap.entries()) {
|
|
387
|
-
if (!sdfMap.has(key)) {
|
|
388
|
-
onlyInDb.push({ columns: Array.isArray(item.columns) ? item.columns.slice() : [] });
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return { onlyInSdf, onlyInDb, mismatched };
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Foreign key normalization: ambil triplet (localKey, target, references) plus
|
|
396
|
-
// onDelete/onUpdate canonical. introspect-mapper.mapReferentialAction() sudah
|
|
397
|
-
// menormalisasi raw DB action ke canonical (cascade/setNull/restrict), dan SDF
|
|
398
|
-
// menyimpan canonical lewat ir-builder.normalizeRelation(). Triplet dipakai
|
|
399
|
-
// untuk FK matching; action di-compare terpisah di compareFkActions().
|
|
400
|
-
function extractForeignKeys(ir) {
|
|
401
|
-
const out = [];
|
|
402
|
-
if (!ir || !isPlainObject(ir)) return out;
|
|
403
|
-
const relations = isPlainObject(ir.relations) ? ir.relations : {};
|
|
404
|
-
for (const [name, rel] of Object.entries(relations)) {
|
|
405
|
-
if (!rel || rel.type !== 'belongsTo') continue;
|
|
406
|
-
if (!rel.localKey || !rel.target) continue;
|
|
407
|
-
out.push({
|
|
408
|
-
name,
|
|
409
|
-
localKey: rel.localKey,
|
|
410
|
-
target: stripSchemaPrefix(rel.target),
|
|
411
|
-
references: rel.references || null,
|
|
412
|
-
onDelete: rel.onDelete,
|
|
413
|
-
onUpdate: rel.onUpdate,
|
|
414
|
-
// Carry the actual DB constraint name (introspect-mapper sets it on the
|
|
415
|
-
// rel) past this whitelist so DROP/MODIFY can target the real conname.
|
|
416
|
-
// Undefined for SDF-side FKs, which never emit a DROP.
|
|
417
|
-
_dbConstraintName: rel._dbConstraintName
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
return out;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function stripSchemaPrefix(name) {
|
|
424
|
-
if (typeof name !== 'string') return name;
|
|
425
|
-
const dot = name.indexOf('.');
|
|
426
|
-
if (dot === -1) return name;
|
|
427
|
-
return name.slice(dot + 1);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function fkKey(item) {
|
|
431
|
-
if (!item) return null;
|
|
432
|
-
return `${item.localKey}->${item.target}.${item.references || ''}`;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// SQL standard: FK tanpa ON DELETE/UPDATE eksplisit berperilaku seperti NO ACTION
|
|
436
|
-
// (alias dari RESTRICT di kebanyakan dialect dan di canonical map kita).
|
|
437
|
-
// Normalisasi ini wajib agar SDF yang skip declare action tidak dianggap drift
|
|
438
|
-
// terhadap DB yang punya RESTRICT eksplisit.
|
|
439
|
-
function normalizeFkAction(value) {
|
|
440
|
-
if (value === undefined || value === null || value === '') {
|
|
441
|
-
return 'restrict';
|
|
442
|
-
}
|
|
443
|
-
return String(value).toLowerCase();
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function compareFkActions(sdfFk, dbFk) {
|
|
447
|
-
const reasons = [];
|
|
448
|
-
const sdfOnDelete = normalizeFkAction(sdfFk && sdfFk.onDelete);
|
|
449
|
-
const dbOnDelete = normalizeFkAction(dbFk && dbFk.onDelete);
|
|
450
|
-
if (sdfOnDelete !== dbOnDelete) {
|
|
451
|
-
reasons.push(`onDelete: SDF='${sdfOnDelete}' vs DB='${dbOnDelete}'`);
|
|
452
|
-
}
|
|
453
|
-
const sdfOnUpdate = normalizeFkAction(sdfFk && sdfFk.onUpdate);
|
|
454
|
-
const dbOnUpdate = normalizeFkAction(dbFk && dbFk.onUpdate);
|
|
455
|
-
if (sdfOnUpdate !== dbOnUpdate) {
|
|
456
|
-
reasons.push(`onUpdate: SDF='${sdfOnUpdate}' vs DB='${dbOnUpdate}'`);
|
|
457
|
-
}
|
|
458
|
-
return reasons;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function formatFkEntry(fk) {
|
|
462
|
-
return {
|
|
463
|
-
name: fk.name,
|
|
464
|
-
localKey: fk.localKey,
|
|
465
|
-
target: fk.target,
|
|
466
|
-
references: fk.references,
|
|
467
|
-
onDelete: normalizeFkAction(fk.onDelete),
|
|
468
|
-
onUpdate: normalizeFkAction(fk.onUpdate),
|
|
469
|
-
// Actual DB constraint name for onlyInDb (DROP); undefined for onlyInSdf (ADD).
|
|
470
|
-
dbConstraintName: fk._dbConstraintName
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function formatFkActions(fk) {
|
|
475
|
-
return {
|
|
476
|
-
onDelete: normalizeFkAction(fk.onDelete),
|
|
477
|
-
onUpdate: normalizeFkAction(fk.onUpdate)
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function diffForeignKeys(sdfIR, dbIR) {
|
|
482
|
-
const sdf = extractForeignKeys(sdfIR);
|
|
483
|
-
const db = extractForeignKeys(dbIR);
|
|
484
|
-
|
|
485
|
-
const sdfMap = new Map();
|
|
486
|
-
for (const fk of sdf) sdfMap.set(fkKey(fk), fk);
|
|
487
|
-
const dbMap = new Map();
|
|
488
|
-
for (const fk of db) dbMap.set(fkKey(fk), fk);
|
|
489
|
-
|
|
490
|
-
const onlyInSdf = [];
|
|
491
|
-
const onlyInDb = [];
|
|
492
|
-
const mismatched = [];
|
|
493
|
-
|
|
494
|
-
for (const [key, sdfFk] of sdfMap.entries()) {
|
|
495
|
-
if (!dbMap.has(key)) {
|
|
496
|
-
onlyInSdf.push(formatFkEntry(sdfFk));
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
const dbFk = dbMap.get(key);
|
|
500
|
-
const reasons = compareFkActions(sdfFk, dbFk);
|
|
501
|
-
if (reasons.length > 0) {
|
|
502
|
-
mismatched.push({
|
|
503
|
-
name: sdfFk.name,
|
|
504
|
-
dbName: dbFk.name,
|
|
505
|
-
// Actual DB constraint name (conname) from the DB side, used to DROP
|
|
506
|
-
// the existing constraint before re-adding with SDF-derived naming.
|
|
507
|
-
dbConstraintName: dbFk._dbConstraintName,
|
|
508
|
-
localKey: sdfFk.localKey,
|
|
509
|
-
target: sdfFk.target,
|
|
510
|
-
references: sdfFk.references,
|
|
511
|
-
sdf: formatFkActions(sdfFk),
|
|
512
|
-
db: formatFkActions(dbFk),
|
|
513
|
-
reasons
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
for (const [key, dbFk] of dbMap.entries()) {
|
|
518
|
-
if (!sdfMap.has(key)) {
|
|
519
|
-
onlyInDb.push(formatFkEntry(dbFk));
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
return { onlyInSdf, onlyInDb, mismatched };
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// ─────────────────────────────────────────────────────────────
|
|
527
|
-
// Check constraint comparison (set-based match-by-content)
|
|
528
|
-
// ─────────────────────────────────────────────────────────────
|
|
529
|
-
|
|
530
|
-
// Hash kanonikal untuk check item:
|
|
531
|
-
// - Parsed: ${field}:${op}:${valueKey} (IN-list value di-sort agar order
|
|
532
|
-
// tidak signifikan).
|
|
533
|
-
// - Unparsable: 'unparsable:' + normalized expression string.
|
|
534
|
-
function checkKey(c) {
|
|
535
|
-
if (!c) return null;
|
|
536
|
-
if (c._unparsable) {
|
|
537
|
-
return `unparsable:${normalizeWeakDefault(c.expression)}`;
|
|
538
|
-
}
|
|
539
|
-
if (typeof c.field !== 'string' || typeof c.op !== 'string') return null;
|
|
540
|
-
let valueKey;
|
|
541
|
-
if (Array.isArray(c.value)) {
|
|
542
|
-
valueKey = c.value
|
|
543
|
-
.slice()
|
|
544
|
-
.map((v) => JSON.stringify(v))
|
|
545
|
-
.sort()
|
|
546
|
-
.join(',');
|
|
547
|
-
} else {
|
|
548
|
-
valueKey = JSON.stringify(c.value);
|
|
549
|
-
}
|
|
550
|
-
return `${c.field}:${c.op}:${valueKey}`;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
function formatCheck(c) {
|
|
554
|
-
if (!c) return null;
|
|
555
|
-
if (c._unparsable) return { expression: c.expression, _unparsable: true };
|
|
556
|
-
const out = { field: c.field, op: c.op, value: c.value };
|
|
557
|
-
if (c.name) out.name = c.name;
|
|
558
|
-
return out;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function diffChecks(sdfChecks, dbChecks) {
|
|
562
|
-
const sdfMap = new Map();
|
|
563
|
-
if (Array.isArray(sdfChecks)) {
|
|
564
|
-
for (const c of sdfChecks) {
|
|
565
|
-
const k = checkKey(c);
|
|
566
|
-
if (k && !sdfMap.has(k)) sdfMap.set(k, c);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
const dbMap = new Map();
|
|
570
|
-
if (Array.isArray(dbChecks)) {
|
|
571
|
-
for (const c of dbChecks) {
|
|
572
|
-
const k = checkKey(c);
|
|
573
|
-
if (k && !dbMap.has(k)) dbMap.set(k, c);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const onlyInSdf = [];
|
|
578
|
-
const onlyInDb = [];
|
|
579
|
-
const mismatched = [];
|
|
580
|
-
|
|
581
|
-
for (const [k, c] of sdfMap.entries()) {
|
|
582
|
-
if (!dbMap.has(k)) onlyInSdf.push(formatCheck(c));
|
|
583
|
-
}
|
|
584
|
-
for (const [k, c] of dbMap.entries()) {
|
|
585
|
-
if (!sdfMap.has(k)) onlyInDb.push(formatCheck(c));
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
return { onlyInSdf, onlyInDb, mismatched };
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// ─────────────────────────────────────────────────────────────
|
|
592
|
-
// Top-level API
|
|
593
|
-
// ─────────────────────────────────────────────────────────────
|
|
594
|
-
|
|
595
|
-
function hasAnyDrift(delta) {
|
|
596
|
-
if (!delta) return false;
|
|
597
|
-
if (delta.fields.onlyInSdf.length > 0) return true;
|
|
598
|
-
if (delta.fields.onlyInDb.length > 0) return true;
|
|
599
|
-
if (delta.fields.mismatched.length > 0) return true;
|
|
600
|
-
if (!delta.primaryKey.match) return true;
|
|
601
|
-
if (delta.indexes.onlyInSdf.length > 0) return true;
|
|
602
|
-
if (delta.indexes.onlyInDb.length > 0) return true;
|
|
603
|
-
if (delta.indexes.mismatched.length > 0) return true;
|
|
604
|
-
if (delta.uniques.onlyInSdf.length > 0) return true;
|
|
605
|
-
if (delta.uniques.onlyInDb.length > 0) return true;
|
|
606
|
-
if (delta.uniques.mismatched.length > 0) return true;
|
|
607
|
-
if (delta.foreignKeys.onlyInSdf.length > 0) return true;
|
|
608
|
-
if (delta.foreignKeys.onlyInDb.length > 0) return true;
|
|
609
|
-
if (delta.foreignKeys.mismatched.length > 0) return true;
|
|
610
|
-
if (delta.checks && delta.checks.onlyInSdf.length > 0) return true;
|
|
611
|
-
if (delta.checks && delta.checks.onlyInDb.length > 0) return true;
|
|
612
|
-
if (delta.checks && delta.checks.mismatched.length > 0) return true;
|
|
613
|
-
return false;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function diffModel(sdfIR, dbIR) {
|
|
617
|
-
if (!isPlainObject(sdfIR)) {
|
|
618
|
-
throw new Error('diffModel: sdfIR must be a plain object IR');
|
|
619
|
-
}
|
|
620
|
-
if (!isPlainObject(dbIR)) {
|
|
621
|
-
throw new Error('diffModel: dbIR must be a plain object IR');
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const tableName = sdfIR.tableName || dbIR.tableName || null;
|
|
625
|
-
|
|
626
|
-
const delta = {
|
|
627
|
-
tableName,
|
|
628
|
-
hasDrift: false,
|
|
629
|
-
fields: diffFields(sdfIR.fields, dbIR.fields),
|
|
630
|
-
primaryKey: diffPrimaryKey(sdfIR, dbIR),
|
|
631
|
-
indexes: diffIndexes(sdfIR.indexes, dbIR.indexes),
|
|
632
|
-
uniques: diffUniques(sdfIR.uniques, dbIR.uniques),
|
|
633
|
-
foreignKeys: diffForeignKeys(sdfIR, dbIR),
|
|
634
|
-
checks: diffChecks(sdfIR.checks, dbIR.checks)
|
|
635
|
-
};
|
|
636
|
-
delta.hasDrift = hasAnyDrift(delta);
|
|
637
|
-
|
|
638
|
-
return delta;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function diffModels(sdfModels, dbModels) {
|
|
642
|
-
const sdfMap = toMap(sdfModels);
|
|
643
|
-
const dbMap = toMap(dbModels);
|
|
644
|
-
|
|
645
|
-
const results = [];
|
|
646
|
-
const visited = new Set();
|
|
647
|
-
|
|
648
|
-
for (const [key, sdfIR] of sdfMap.entries()) {
|
|
649
|
-
visited.add(key);
|
|
650
|
-
const dbIR = dbMap.get(key) || emptyIR(sdfIR.tableName || key);
|
|
651
|
-
results.push(diffModel(sdfIR, dbIR));
|
|
652
|
-
}
|
|
653
|
-
for (const [key, dbIR] of dbMap.entries()) {
|
|
654
|
-
if (visited.has(key)) continue;
|
|
655
|
-
results.push(diffModel(emptyIR(dbIR.tableName || key), dbIR));
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
return results;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function emptyIR(tableName) {
|
|
662
|
-
return {
|
|
663
|
-
tableName: tableName || '',
|
|
664
|
-
schemaName: null,
|
|
665
|
-
qualifiedName: tableName || '',
|
|
666
|
-
fields: {},
|
|
667
|
-
primaryKey: [],
|
|
668
|
-
indexes: [],
|
|
669
|
-
uniques: [],
|
|
670
|
-
relations: {},
|
|
671
|
-
checks: []
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function toMap(input) {
|
|
676
|
-
if (input instanceof Map) return input;
|
|
677
|
-
const map = new Map();
|
|
678
|
-
if (Array.isArray(input)) {
|
|
679
|
-
for (const ir of input) {
|
|
680
|
-
if (ir && (ir.qualifiedName || ir.tableName)) {
|
|
681
|
-
map.set(ir.qualifiedName || ir.tableName, ir);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
return map;
|
|
685
|
-
}
|
|
686
|
-
if (isPlainObject(input)) {
|
|
687
|
-
for (const [key, ir] of Object.entries(input)) {
|
|
688
|
-
map.set(key, ir);
|
|
689
|
-
}
|
|
690
|
-
return map;
|
|
691
|
-
}
|
|
692
|
-
return map;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
module.exports = {
|
|
696
|
-
diffModel,
|
|
697
|
-
diffModels,
|
|
698
|
-
normalizeType,
|
|
699
|
-
// exported for unit test transparency
|
|
700
|
-
_internal: {
|
|
701
|
-
resolvePrimaryKey,
|
|
702
|
-
diffFields,
|
|
703
|
-
diffPrimaryKey,
|
|
704
|
-
diffIndexes,
|
|
705
|
-
diffUniques,
|
|
706
|
-
diffForeignKeys,
|
|
707
|
-
extractForeignKeys,
|
|
708
|
-
diffChecks,
|
|
709
|
-
compareDefaultValue,
|
|
710
|
-
canonicalDefaultPayload,
|
|
711
|
-
normalizeWeakDefault,
|
|
712
|
-
normalizeFkAction,
|
|
713
|
-
compareFkActions
|
|
714
|
-
}
|
|
715
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Diff Engine — compare dua IR (Intermediate Representation) dari dbschema-kit
|
|
5
|
+
* dan hasilkan structured delta. Operasi bersifat semantic-level, bukan
|
|
6
|
+
* text-level: nama field, tipe ter-normalisasi, length, nullability, primary
|
|
7
|
+
* key, index, unique constraint, dan foreign key di-compare langsung.
|
|
8
|
+
*
|
|
9
|
+
* Input IR diharapkan kompatibel dengan output `defineModel()` (SDF) maupun
|
|
10
|
+
* `mapTableMetaToIR()` (database introspection). Diff engine tidak melakukan
|
|
11
|
+
* I/O dan tidak memodifikasi input.
|
|
12
|
+
*
|
|
13
|
+
* @module lib/dbschema-kit/diff-engine
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ─────────────────────────────────────────────────────────────
|
|
17
|
+
// Type normalization (alias mapping per dialect)
|
|
18
|
+
// ─────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
// Mapping kanonikal dari semua alias type (SDF + DB) ke representasi
|
|
21
|
+
// internal yang dipakai saat compare. Mapping ini intentionally konservatif:
|
|
22
|
+
// hanya alias yang umum di-emit oleh dialect driver yang di-cover.
|
|
23
|
+
const TYPE_ALIAS_MAP = {
|
|
24
|
+
// String varieties
|
|
25
|
+
'string': 'string',
|
|
26
|
+
'varchar': 'string',
|
|
27
|
+
'character varying': 'string',
|
|
28
|
+
'character': 'string',
|
|
29
|
+
'char': 'string',
|
|
30
|
+
'nvarchar': 'string',
|
|
31
|
+
'nchar': 'string',
|
|
32
|
+
// Text
|
|
33
|
+
'text': 'text',
|
|
34
|
+
'clob': 'text',
|
|
35
|
+
'nclob': 'text',
|
|
36
|
+
'mediumtext': 'text',
|
|
37
|
+
'longtext': 'text',
|
|
38
|
+
// Integer
|
|
39
|
+
'integer': 'integer',
|
|
40
|
+
'int': 'integer',
|
|
41
|
+
'int4': 'integer',
|
|
42
|
+
'int2': 'integer',
|
|
43
|
+
'smallint': 'integer',
|
|
44
|
+
'mediumint': 'integer',
|
|
45
|
+
// Bigint
|
|
46
|
+
'bigint': 'bigint',
|
|
47
|
+
'int8': 'bigint',
|
|
48
|
+
// Decimal / numeric
|
|
49
|
+
'decimal': 'decimal',
|
|
50
|
+
'numeric': 'decimal',
|
|
51
|
+
'number': 'decimal',
|
|
52
|
+
// Boolean
|
|
53
|
+
'boolean': 'boolean',
|
|
54
|
+
'bool': 'boolean',
|
|
55
|
+
'tinyint(1)': 'boolean',
|
|
56
|
+
// Date / time
|
|
57
|
+
'date': 'date',
|
|
58
|
+
'timestamp': 'timestamp',
|
|
59
|
+
'timestamp without time zone': 'timestamp',
|
|
60
|
+
'timestamp with time zone': 'timestamp',
|
|
61
|
+
'timestamptz': 'timestamp',
|
|
62
|
+
'datetime': 'timestamp',
|
|
63
|
+
// UUID
|
|
64
|
+
'uuid': 'uuid',
|
|
65
|
+
// JSON
|
|
66
|
+
'json': 'json',
|
|
67
|
+
'jsonb': 'json'
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function normalizeType(rawType) {
|
|
71
|
+
if (rawType === undefined || rawType === null) return null;
|
|
72
|
+
const key = String(rawType).toLowerCase().trim();
|
|
73
|
+
if (TYPE_ALIAS_MAP[key]) return TYPE_ALIAS_MAP[key];
|
|
74
|
+
return key;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─────────────────────────────────────────────────────────────
|
|
78
|
+
// Helpers
|
|
79
|
+
// ─────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function isPlainObject(v) {
|
|
82
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function arrayEquals(a, b) {
|
|
86
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
87
|
+
if (a.length !== b.length) return false;
|
|
88
|
+
for (let i = 0; i < a.length; i++) {
|
|
89
|
+
if (a[i] !== b[i]) return false;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function sortedCopy(arr) {
|
|
95
|
+
return Array.isArray(arr) ? arr.slice().sort() : [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isNullable(field) {
|
|
99
|
+
// notnull=true berarti tidak nullable; notnull=undefined berarti default nullable.
|
|
100
|
+
return field && field.notnull === true ? false : true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolvePrimaryKey(ir) {
|
|
104
|
+
// IR convention: primaryKey array hanya terisi untuk composite PK.
|
|
105
|
+
// Single PK ditandai via fields[name].pk = true.
|
|
106
|
+
if (!ir || !isPlainObject(ir)) return [];
|
|
107
|
+
if (Array.isArray(ir.primaryKey) && ir.primaryKey.length > 0) {
|
|
108
|
+
return ir.primaryKey.slice();
|
|
109
|
+
}
|
|
110
|
+
const out = [];
|
|
111
|
+
const fields = isPlainObject(ir.fields) ? ir.fields : {};
|
|
112
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
113
|
+
if (field && field.pk === true) out.push(name);
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─────────────────────────────────────────────────────────────
|
|
119
|
+
// Field comparison
|
|
120
|
+
// ─────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
function buildFieldSummary(name, field) {
|
|
123
|
+
const out = { name };
|
|
124
|
+
if (field) {
|
|
125
|
+
if (field.type !== undefined) out.type = field.type;
|
|
126
|
+
if (field.length !== undefined) out.length = field.length;
|
|
127
|
+
if (field.precision !== undefined) out.precision = field.precision;
|
|
128
|
+
if (field.scale !== undefined) out.scale = field.scale;
|
|
129
|
+
out.nullable = isNullable(field);
|
|
130
|
+
} else {
|
|
131
|
+
out.nullable = true;
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function compareFieldDetails(sdfField, dbField) {
|
|
137
|
+
const reasons = [];
|
|
138
|
+
|
|
139
|
+
const sdfType = normalizeType(sdfField && sdfField.type);
|
|
140
|
+
const dbType = normalizeType(dbField && dbField.type);
|
|
141
|
+
if (sdfType !== dbType) {
|
|
142
|
+
reasons.push(`type: SDF='${sdfField && sdfField.type}' vs DB='${dbField && dbField.type}'`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Length: relevan hanya untuk type=string. Untuk type lain (text/integer/dll.)
|
|
146
|
+
// length umumnya tidak digunakan dan diabaikan agar tidak false-positive.
|
|
147
|
+
if (sdfType === 'string' && dbType === 'string') {
|
|
148
|
+
const sdfLen = sdfField && sdfField.length;
|
|
149
|
+
const dbLen = dbField && dbField.length;
|
|
150
|
+
if (sdfLen !== undefined && dbLen !== undefined && Number(sdfLen) !== Number(dbLen)) {
|
|
151
|
+
reasons.push(`length: SDF=${sdfLen} vs DB=${dbLen}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Precision / scale untuk decimal.
|
|
156
|
+
if (sdfType === 'decimal' && dbType === 'decimal') {
|
|
157
|
+
const sdfP = sdfField && sdfField.precision;
|
|
158
|
+
const dbP = dbField && dbField.precision;
|
|
159
|
+
if (sdfP !== undefined && dbP !== undefined && Number(sdfP) !== Number(dbP)) {
|
|
160
|
+
reasons.push(`precision: SDF=${sdfP} vs DB=${dbP}`);
|
|
161
|
+
}
|
|
162
|
+
const sdfS = sdfField && sdfField.scale;
|
|
163
|
+
const dbS = dbField && dbField.scale;
|
|
164
|
+
if (sdfS !== undefined && dbS !== undefined && Number(sdfS) !== Number(dbS)) {
|
|
165
|
+
reasons.push(`scale: SDF=${sdfS} vs DB=${dbS}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const sdfNullable = isNullable(sdfField);
|
|
170
|
+
const dbNullable = isNullable(dbField);
|
|
171
|
+
if (sdfNullable !== dbNullable) {
|
|
172
|
+
reasons.push(`nullable: SDF=${sdfNullable} vs DB=${dbNullable}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const defaultReason = compareDefaultValue(sdfField, dbField);
|
|
176
|
+
if (defaultReason) reasons.push(defaultReason);
|
|
177
|
+
|
|
178
|
+
return reasons;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─────────────────────────────────────────────────────────────
|
|
182
|
+
// Default value comparison
|
|
183
|
+
// ─────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
// Kanonikal payload dari struct default { kind, value, name, args } untuk
|
|
186
|
+
// strict compare. Output adalah string yang stable (deterministic) sehingga
|
|
187
|
+
// equality check cukup dengan ===.
|
|
188
|
+
function canonicalDefaultPayload(def) {
|
|
189
|
+
if (!def || typeof def !== 'object') return '';
|
|
190
|
+
if (def.kind === 'literal') {
|
|
191
|
+
// JSON.stringify menjaga distingsi tipe (string vs number vs boolean).
|
|
192
|
+
return JSON.stringify(def.value);
|
|
193
|
+
}
|
|
194
|
+
if (def.kind === 'function') {
|
|
195
|
+
const name = String(def.name || '').toLowerCase();
|
|
196
|
+
const args = Array.isArray(def.args) ? def.args : [];
|
|
197
|
+
return `${name}(${args.join(',')})`;
|
|
198
|
+
}
|
|
199
|
+
if (def.kind === 'constant') {
|
|
200
|
+
return String(def.value == null ? '' : def.value).toLowerCase();
|
|
201
|
+
}
|
|
202
|
+
return '';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Weak normalization untuk _defaultComment dan fallback string compare.
|
|
206
|
+
// Whitespace di-collapse, lowercase, lalu trim agar variasi cosmetic tidak
|
|
207
|
+
// memicu false-positive.
|
|
208
|
+
function normalizeWeakDefault(str) {
|
|
209
|
+
return String(str == null ? '' : str).toLowerCase().replace(/\s+/g, ' ').trim();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function describeDefault(def, comment) {
|
|
213
|
+
if (def && typeof def === 'object') {
|
|
214
|
+
return canonicalDefaultPayload(def);
|
|
215
|
+
}
|
|
216
|
+
if (comment) return `(comment) ${normalizeWeakDefault(comment)}`;
|
|
217
|
+
return '(none)';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function compareDefaultValue(sdfField, dbField) {
|
|
221
|
+
const sdfDef = sdfField && sdfField.default;
|
|
222
|
+
const dbDef = dbField && dbField.default;
|
|
223
|
+
const sdfComment = sdfField && sdfField._defaultComment;
|
|
224
|
+
const dbComment = dbField && dbField._defaultComment;
|
|
225
|
+
|
|
226
|
+
const sdfHasStruct = sdfDef !== undefined && sdfDef !== null;
|
|
227
|
+
const dbHasStruct = dbDef !== undefined && dbDef !== null;
|
|
228
|
+
const sdfHasComment = !!sdfComment;
|
|
229
|
+
const dbHasComment = !!dbComment;
|
|
230
|
+
|
|
231
|
+
// Case 1: keduanya tidak punya default sama sekali → match.
|
|
232
|
+
if (!sdfHasStruct && !dbHasStruct && !sdfHasComment && !dbHasComment) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Case 2: salah satu sisi punya signal default (struct atau comment),
|
|
237
|
+
// sisi lain kosong total → drift.
|
|
238
|
+
const sdfHasAny = sdfHasStruct || sdfHasComment;
|
|
239
|
+
const dbHasAny = dbHasStruct || dbHasComment;
|
|
240
|
+
if (sdfHasAny !== dbHasAny) {
|
|
241
|
+
return `default: SDF=${describeDefault(sdfDef, sdfComment)} vs DB=${describeDefault(dbDef, dbComment)}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Case 3: keduanya punya struktur kanonikal → strict compare kind + value.
|
|
245
|
+
if (sdfHasStruct && dbHasStruct) {
|
|
246
|
+
if (sdfDef.kind !== dbDef.kind) {
|
|
247
|
+
return `default kind: SDF='${sdfDef.kind}' vs DB='${dbDef.kind}'`;
|
|
248
|
+
}
|
|
249
|
+
const sdfPayload = canonicalDefaultPayload(sdfDef);
|
|
250
|
+
const dbPayload = canonicalDefaultPayload(dbDef);
|
|
251
|
+
if (sdfPayload !== dbPayload) {
|
|
252
|
+
return `default: SDF=${sdfPayload} vs DB=${dbPayload}`;
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Case 4: minimal salah satu sisi hanya punya _defaultComment (atau struktur
|
|
258
|
+
// di satu sisi, comment di sisi lain). Fallback ke weak compare.
|
|
259
|
+
const sdfStr = normalizeWeakDefault(sdfHasStruct ? canonicalDefaultPayload(sdfDef) : sdfComment);
|
|
260
|
+
const dbStr = normalizeWeakDefault(dbHasStruct ? canonicalDefaultPayload(dbDef) : dbComment);
|
|
261
|
+
if (sdfStr !== dbStr) {
|
|
262
|
+
return `default (weak): SDF='${sdfStr}' vs DB='${dbStr}'`;
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function diffFields(sdfFields, dbFields) {
|
|
268
|
+
const sdf = isPlainObject(sdfFields) ? sdfFields : {};
|
|
269
|
+
const db = isPlainObject(dbFields) ? dbFields : {};
|
|
270
|
+
|
|
271
|
+
const sdfNames = Object.keys(sdf);
|
|
272
|
+
const dbNames = Object.keys(db);
|
|
273
|
+
|
|
274
|
+
const onlyInSdf = [];
|
|
275
|
+
const onlyInDb = [];
|
|
276
|
+
const mismatched = [];
|
|
277
|
+
|
|
278
|
+
for (const name of sdfNames) {
|
|
279
|
+
if (!(name in db)) {
|
|
280
|
+
onlyInSdf.push(buildFieldSummary(name, sdf[name]));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const name of dbNames) {
|
|
285
|
+
if (!(name in sdf)) {
|
|
286
|
+
onlyInDb.push(buildFieldSummary(name, db[name]));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
for (const name of sdfNames) {
|
|
291
|
+
if (!(name in db)) continue;
|
|
292
|
+
const reasons = compareFieldDetails(sdf[name], db[name]);
|
|
293
|
+
if (reasons.length > 0) {
|
|
294
|
+
mismatched.push({
|
|
295
|
+
name,
|
|
296
|
+
sdf: buildFieldSummary(name, sdf[name]),
|
|
297
|
+
db: buildFieldSummary(name, db[name]),
|
|
298
|
+
reasons
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return { onlyInSdf, onlyInDb, mismatched };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ─────────────────────────────────────────────────────────────
|
|
307
|
+
// Primary key comparison
|
|
308
|
+
// ─────────────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
function diffPrimaryKey(sdfIR, dbIR) {
|
|
311
|
+
const sdf = resolvePrimaryKey(sdfIR);
|
|
312
|
+
const db = resolvePrimaryKey(dbIR);
|
|
313
|
+
// PK match jika kolom sama persis (order-sensitive: PK order matters di SQL).
|
|
314
|
+
const match = arrayEquals(sdf, db);
|
|
315
|
+
return { sdf, db, match };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ─────────────────────────────────────────────────────────────
|
|
319
|
+
// Collection diff (indexes, uniques, foreign keys)
|
|
320
|
+
// ─────────────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
// Build map dari kolom set kanonikal. Bila item punya nama dan unik, name
|
|
323
|
+
// digunakan sebagai key; selain itu dipakai gabungan kolom yang ter-sort agar
|
|
324
|
+
// stable order columns tidak dianggap berbeda.
|
|
325
|
+
function buildItemMap(items, getKey) {
|
|
326
|
+
const map = new Map();
|
|
327
|
+
if (!Array.isArray(items)) return map;
|
|
328
|
+
for (const item of items) {
|
|
329
|
+
if (!item) continue;
|
|
330
|
+
const key = getKey(item);
|
|
331
|
+
if (!key) continue;
|
|
332
|
+
if (!map.has(key)) map.set(key, item);
|
|
333
|
+
}
|
|
334
|
+
return map;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function columnsKey(item) {
|
|
338
|
+
if (!item || !Array.isArray(item.columns)) return null;
|
|
339
|
+
return item.columns.slice().sort().join('|');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function indexKey(item) {
|
|
343
|
+
return columnsKey(item);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function diffIndexes(sdfIndexes, dbIndexes) {
|
|
347
|
+
const sdfMap = buildItemMap(sdfIndexes, indexKey);
|
|
348
|
+
const dbMap = buildItemMap(dbIndexes, indexKey);
|
|
349
|
+
|
|
350
|
+
const onlyInSdf = [];
|
|
351
|
+
const onlyInDb = [];
|
|
352
|
+
const mismatched = [];
|
|
353
|
+
|
|
354
|
+
for (const [key, item] of sdfMap.entries()) {
|
|
355
|
+
if (!dbMap.has(key)) {
|
|
356
|
+
onlyInSdf.push({ columns: Array.isArray(item.columns) ? item.columns.slice() : [] });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const [key, item] of dbMap.entries()) {
|
|
360
|
+
if (!sdfMap.has(key)) {
|
|
361
|
+
onlyInDb.push({ columns: Array.isArray(item.columns) ? item.columns.slice() : [] });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// Mismatch: same column-set tapi order berbeda. MVP-nya kita treat
|
|
365
|
+
// matching column-set sebagai match dan tidak menandai order sebagai drift,
|
|
366
|
+
// karena index column order biasanya ditentukan oleh dialect optimizer.
|
|
367
|
+
// (Kebijakan ini dapat di-revisi fase berikutnya.)
|
|
368
|
+
|
|
369
|
+
return { onlyInSdf, onlyInDb, mismatched };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function diffUniques(sdfUniques, dbUniques) {
|
|
373
|
+
// Strategi sama dengan index: match by column-set kanonikal.
|
|
374
|
+
const sdfMap = buildItemMap(sdfUniques, columnsKey);
|
|
375
|
+
const dbMap = buildItemMap(dbUniques, columnsKey);
|
|
376
|
+
|
|
377
|
+
const onlyInSdf = [];
|
|
378
|
+
const onlyInDb = [];
|
|
379
|
+
const mismatched = [];
|
|
380
|
+
|
|
381
|
+
for (const [key, item] of sdfMap.entries()) {
|
|
382
|
+
if (!dbMap.has(key)) {
|
|
383
|
+
onlyInSdf.push({ columns: Array.isArray(item.columns) ? item.columns.slice() : [] });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
for (const [key, item] of dbMap.entries()) {
|
|
387
|
+
if (!sdfMap.has(key)) {
|
|
388
|
+
onlyInDb.push({ columns: Array.isArray(item.columns) ? item.columns.slice() : [] });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return { onlyInSdf, onlyInDb, mismatched };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Foreign key normalization: ambil triplet (localKey, target, references) plus
|
|
396
|
+
// onDelete/onUpdate canonical. introspect-mapper.mapReferentialAction() sudah
|
|
397
|
+
// menormalisasi raw DB action ke canonical (cascade/setNull/restrict), dan SDF
|
|
398
|
+
// menyimpan canonical lewat ir-builder.normalizeRelation(). Triplet dipakai
|
|
399
|
+
// untuk FK matching; action di-compare terpisah di compareFkActions().
|
|
400
|
+
function extractForeignKeys(ir) {
|
|
401
|
+
const out = [];
|
|
402
|
+
if (!ir || !isPlainObject(ir)) return out;
|
|
403
|
+
const relations = isPlainObject(ir.relations) ? ir.relations : {};
|
|
404
|
+
for (const [name, rel] of Object.entries(relations)) {
|
|
405
|
+
if (!rel || rel.type !== 'belongsTo') continue;
|
|
406
|
+
if (!rel.localKey || !rel.target) continue;
|
|
407
|
+
out.push({
|
|
408
|
+
name,
|
|
409
|
+
localKey: rel.localKey,
|
|
410
|
+
target: stripSchemaPrefix(rel.target),
|
|
411
|
+
references: rel.references || null,
|
|
412
|
+
onDelete: rel.onDelete,
|
|
413
|
+
onUpdate: rel.onUpdate,
|
|
414
|
+
// Carry the actual DB constraint name (introspect-mapper sets it on the
|
|
415
|
+
// rel) past this whitelist so DROP/MODIFY can target the real conname.
|
|
416
|
+
// Undefined for SDF-side FKs, which never emit a DROP.
|
|
417
|
+
_dbConstraintName: rel._dbConstraintName
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function stripSchemaPrefix(name) {
|
|
424
|
+
if (typeof name !== 'string') return name;
|
|
425
|
+
const dot = name.indexOf('.');
|
|
426
|
+
if (dot === -1) return name;
|
|
427
|
+
return name.slice(dot + 1);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function fkKey(item) {
|
|
431
|
+
if (!item) return null;
|
|
432
|
+
return `${item.localKey}->${item.target}.${item.references || ''}`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// SQL standard: FK tanpa ON DELETE/UPDATE eksplisit berperilaku seperti NO ACTION
|
|
436
|
+
// (alias dari RESTRICT di kebanyakan dialect dan di canonical map kita).
|
|
437
|
+
// Normalisasi ini wajib agar SDF yang skip declare action tidak dianggap drift
|
|
438
|
+
// terhadap DB yang punya RESTRICT eksplisit.
|
|
439
|
+
function normalizeFkAction(value) {
|
|
440
|
+
if (value === undefined || value === null || value === '') {
|
|
441
|
+
return 'restrict';
|
|
442
|
+
}
|
|
443
|
+
return String(value).toLowerCase();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function compareFkActions(sdfFk, dbFk) {
|
|
447
|
+
const reasons = [];
|
|
448
|
+
const sdfOnDelete = normalizeFkAction(sdfFk && sdfFk.onDelete);
|
|
449
|
+
const dbOnDelete = normalizeFkAction(dbFk && dbFk.onDelete);
|
|
450
|
+
if (sdfOnDelete !== dbOnDelete) {
|
|
451
|
+
reasons.push(`onDelete: SDF='${sdfOnDelete}' vs DB='${dbOnDelete}'`);
|
|
452
|
+
}
|
|
453
|
+
const sdfOnUpdate = normalizeFkAction(sdfFk && sdfFk.onUpdate);
|
|
454
|
+
const dbOnUpdate = normalizeFkAction(dbFk && dbFk.onUpdate);
|
|
455
|
+
if (sdfOnUpdate !== dbOnUpdate) {
|
|
456
|
+
reasons.push(`onUpdate: SDF='${sdfOnUpdate}' vs DB='${dbOnUpdate}'`);
|
|
457
|
+
}
|
|
458
|
+
return reasons;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function formatFkEntry(fk) {
|
|
462
|
+
return {
|
|
463
|
+
name: fk.name,
|
|
464
|
+
localKey: fk.localKey,
|
|
465
|
+
target: fk.target,
|
|
466
|
+
references: fk.references,
|
|
467
|
+
onDelete: normalizeFkAction(fk.onDelete),
|
|
468
|
+
onUpdate: normalizeFkAction(fk.onUpdate),
|
|
469
|
+
// Actual DB constraint name for onlyInDb (DROP); undefined for onlyInSdf (ADD).
|
|
470
|
+
dbConstraintName: fk._dbConstraintName
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function formatFkActions(fk) {
|
|
475
|
+
return {
|
|
476
|
+
onDelete: normalizeFkAction(fk.onDelete),
|
|
477
|
+
onUpdate: normalizeFkAction(fk.onUpdate)
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function diffForeignKeys(sdfIR, dbIR) {
|
|
482
|
+
const sdf = extractForeignKeys(sdfIR);
|
|
483
|
+
const db = extractForeignKeys(dbIR);
|
|
484
|
+
|
|
485
|
+
const sdfMap = new Map();
|
|
486
|
+
for (const fk of sdf) sdfMap.set(fkKey(fk), fk);
|
|
487
|
+
const dbMap = new Map();
|
|
488
|
+
for (const fk of db) dbMap.set(fkKey(fk), fk);
|
|
489
|
+
|
|
490
|
+
const onlyInSdf = [];
|
|
491
|
+
const onlyInDb = [];
|
|
492
|
+
const mismatched = [];
|
|
493
|
+
|
|
494
|
+
for (const [key, sdfFk] of sdfMap.entries()) {
|
|
495
|
+
if (!dbMap.has(key)) {
|
|
496
|
+
onlyInSdf.push(formatFkEntry(sdfFk));
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
const dbFk = dbMap.get(key);
|
|
500
|
+
const reasons = compareFkActions(sdfFk, dbFk);
|
|
501
|
+
if (reasons.length > 0) {
|
|
502
|
+
mismatched.push({
|
|
503
|
+
name: sdfFk.name,
|
|
504
|
+
dbName: dbFk.name,
|
|
505
|
+
// Actual DB constraint name (conname) from the DB side, used to DROP
|
|
506
|
+
// the existing constraint before re-adding with SDF-derived naming.
|
|
507
|
+
dbConstraintName: dbFk._dbConstraintName,
|
|
508
|
+
localKey: sdfFk.localKey,
|
|
509
|
+
target: sdfFk.target,
|
|
510
|
+
references: sdfFk.references,
|
|
511
|
+
sdf: formatFkActions(sdfFk),
|
|
512
|
+
db: formatFkActions(dbFk),
|
|
513
|
+
reasons
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
for (const [key, dbFk] of dbMap.entries()) {
|
|
518
|
+
if (!sdfMap.has(key)) {
|
|
519
|
+
onlyInDb.push(formatFkEntry(dbFk));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return { onlyInSdf, onlyInDb, mismatched };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ─────────────────────────────────────────────────────────────
|
|
527
|
+
// Check constraint comparison (set-based match-by-content)
|
|
528
|
+
// ─────────────────────────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
// Hash kanonikal untuk check item:
|
|
531
|
+
// - Parsed: ${field}:${op}:${valueKey} (IN-list value di-sort agar order
|
|
532
|
+
// tidak signifikan).
|
|
533
|
+
// - Unparsable: 'unparsable:' + normalized expression string.
|
|
534
|
+
function checkKey(c) {
|
|
535
|
+
if (!c) return null;
|
|
536
|
+
if (c._unparsable) {
|
|
537
|
+
return `unparsable:${normalizeWeakDefault(c.expression)}`;
|
|
538
|
+
}
|
|
539
|
+
if (typeof c.field !== 'string' || typeof c.op !== 'string') return null;
|
|
540
|
+
let valueKey;
|
|
541
|
+
if (Array.isArray(c.value)) {
|
|
542
|
+
valueKey = c.value
|
|
543
|
+
.slice()
|
|
544
|
+
.map((v) => JSON.stringify(v))
|
|
545
|
+
.sort()
|
|
546
|
+
.join(',');
|
|
547
|
+
} else {
|
|
548
|
+
valueKey = JSON.stringify(c.value);
|
|
549
|
+
}
|
|
550
|
+
return `${c.field}:${c.op}:${valueKey}`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function formatCheck(c) {
|
|
554
|
+
if (!c) return null;
|
|
555
|
+
if (c._unparsable) return { expression: c.expression, _unparsable: true };
|
|
556
|
+
const out = { field: c.field, op: c.op, value: c.value };
|
|
557
|
+
if (c.name) out.name = c.name;
|
|
558
|
+
return out;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function diffChecks(sdfChecks, dbChecks) {
|
|
562
|
+
const sdfMap = new Map();
|
|
563
|
+
if (Array.isArray(sdfChecks)) {
|
|
564
|
+
for (const c of sdfChecks) {
|
|
565
|
+
const k = checkKey(c);
|
|
566
|
+
if (k && !sdfMap.has(k)) sdfMap.set(k, c);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const dbMap = new Map();
|
|
570
|
+
if (Array.isArray(dbChecks)) {
|
|
571
|
+
for (const c of dbChecks) {
|
|
572
|
+
const k = checkKey(c);
|
|
573
|
+
if (k && !dbMap.has(k)) dbMap.set(k, c);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const onlyInSdf = [];
|
|
578
|
+
const onlyInDb = [];
|
|
579
|
+
const mismatched = [];
|
|
580
|
+
|
|
581
|
+
for (const [k, c] of sdfMap.entries()) {
|
|
582
|
+
if (!dbMap.has(k)) onlyInSdf.push(formatCheck(c));
|
|
583
|
+
}
|
|
584
|
+
for (const [k, c] of dbMap.entries()) {
|
|
585
|
+
if (!sdfMap.has(k)) onlyInDb.push(formatCheck(c));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return { onlyInSdf, onlyInDb, mismatched };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ─────────────────────────────────────────────────────────────
|
|
592
|
+
// Top-level API
|
|
593
|
+
// ─────────────────────────────────────────────────────────────
|
|
594
|
+
|
|
595
|
+
function hasAnyDrift(delta) {
|
|
596
|
+
if (!delta) return false;
|
|
597
|
+
if (delta.fields.onlyInSdf.length > 0) return true;
|
|
598
|
+
if (delta.fields.onlyInDb.length > 0) return true;
|
|
599
|
+
if (delta.fields.mismatched.length > 0) return true;
|
|
600
|
+
if (!delta.primaryKey.match) return true;
|
|
601
|
+
if (delta.indexes.onlyInSdf.length > 0) return true;
|
|
602
|
+
if (delta.indexes.onlyInDb.length > 0) return true;
|
|
603
|
+
if (delta.indexes.mismatched.length > 0) return true;
|
|
604
|
+
if (delta.uniques.onlyInSdf.length > 0) return true;
|
|
605
|
+
if (delta.uniques.onlyInDb.length > 0) return true;
|
|
606
|
+
if (delta.uniques.mismatched.length > 0) return true;
|
|
607
|
+
if (delta.foreignKeys.onlyInSdf.length > 0) return true;
|
|
608
|
+
if (delta.foreignKeys.onlyInDb.length > 0) return true;
|
|
609
|
+
if (delta.foreignKeys.mismatched.length > 0) return true;
|
|
610
|
+
if (delta.checks && delta.checks.onlyInSdf.length > 0) return true;
|
|
611
|
+
if (delta.checks && delta.checks.onlyInDb.length > 0) return true;
|
|
612
|
+
if (delta.checks && delta.checks.mismatched.length > 0) return true;
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function diffModel(sdfIR, dbIR) {
|
|
617
|
+
if (!isPlainObject(sdfIR)) {
|
|
618
|
+
throw new Error('diffModel: sdfIR must be a plain object IR');
|
|
619
|
+
}
|
|
620
|
+
if (!isPlainObject(dbIR)) {
|
|
621
|
+
throw new Error('diffModel: dbIR must be a plain object IR');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const tableName = sdfIR.tableName || dbIR.tableName || null;
|
|
625
|
+
|
|
626
|
+
const delta = {
|
|
627
|
+
tableName,
|
|
628
|
+
hasDrift: false,
|
|
629
|
+
fields: diffFields(sdfIR.fields, dbIR.fields),
|
|
630
|
+
primaryKey: diffPrimaryKey(sdfIR, dbIR),
|
|
631
|
+
indexes: diffIndexes(sdfIR.indexes, dbIR.indexes),
|
|
632
|
+
uniques: diffUniques(sdfIR.uniques, dbIR.uniques),
|
|
633
|
+
foreignKeys: diffForeignKeys(sdfIR, dbIR),
|
|
634
|
+
checks: diffChecks(sdfIR.checks, dbIR.checks)
|
|
635
|
+
};
|
|
636
|
+
delta.hasDrift = hasAnyDrift(delta);
|
|
637
|
+
|
|
638
|
+
return delta;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function diffModels(sdfModels, dbModels) {
|
|
642
|
+
const sdfMap = toMap(sdfModels);
|
|
643
|
+
const dbMap = toMap(dbModels);
|
|
644
|
+
|
|
645
|
+
const results = [];
|
|
646
|
+
const visited = new Set();
|
|
647
|
+
|
|
648
|
+
for (const [key, sdfIR] of sdfMap.entries()) {
|
|
649
|
+
visited.add(key);
|
|
650
|
+
const dbIR = dbMap.get(key) || emptyIR(sdfIR.tableName || key);
|
|
651
|
+
results.push(diffModel(sdfIR, dbIR));
|
|
652
|
+
}
|
|
653
|
+
for (const [key, dbIR] of dbMap.entries()) {
|
|
654
|
+
if (visited.has(key)) continue;
|
|
655
|
+
results.push(diffModel(emptyIR(dbIR.tableName || key), dbIR));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return results;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function emptyIR(tableName) {
|
|
662
|
+
return {
|
|
663
|
+
tableName: tableName || '',
|
|
664
|
+
schemaName: null,
|
|
665
|
+
qualifiedName: tableName || '',
|
|
666
|
+
fields: {},
|
|
667
|
+
primaryKey: [],
|
|
668
|
+
indexes: [],
|
|
669
|
+
uniques: [],
|
|
670
|
+
relations: {},
|
|
671
|
+
checks: []
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function toMap(input) {
|
|
676
|
+
if (input instanceof Map) return input;
|
|
677
|
+
const map = new Map();
|
|
678
|
+
if (Array.isArray(input)) {
|
|
679
|
+
for (const ir of input) {
|
|
680
|
+
if (ir && (ir.qualifiedName || ir.tableName)) {
|
|
681
|
+
map.set(ir.qualifiedName || ir.tableName, ir);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return map;
|
|
685
|
+
}
|
|
686
|
+
if (isPlainObject(input)) {
|
|
687
|
+
for (const [key, ir] of Object.entries(input)) {
|
|
688
|
+
map.set(key, ir);
|
|
689
|
+
}
|
|
690
|
+
return map;
|
|
691
|
+
}
|
|
692
|
+
return map;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
module.exports = {
|
|
696
|
+
diffModel,
|
|
697
|
+
diffModels,
|
|
698
|
+
normalizeType,
|
|
699
|
+
// exported for unit test transparency
|
|
700
|
+
_internal: {
|
|
701
|
+
resolvePrimaryKey,
|
|
702
|
+
diffFields,
|
|
703
|
+
diffPrimaryKey,
|
|
704
|
+
diffIndexes,
|
|
705
|
+
diffUniques,
|
|
706
|
+
diffForeignKeys,
|
|
707
|
+
extractForeignKeys,
|
|
708
|
+
diffChecks,
|
|
709
|
+
compareDefaultValue,
|
|
710
|
+
canonicalDefaultPayload,
|
|
711
|
+
normalizeWeakDefault,
|
|
712
|
+
normalizeFkAction,
|
|
713
|
+
compareFkActions
|
|
714
|
+
}
|
|
715
|
+
};
|