@restforgejs/platform 5.2.16 → 5.3.5
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/endpoint/create.js +69 -6
- package/generators/cli/payload/sync.js +16 -6
- package/generators/cli/project/auth.js +2 -2
- package/generators/cli/project/sdk.js +112 -0
- package/generators/lib/arg-parser.js +6 -0
- package/generators/lib/auth/processor-generator.js +5 -3
- package/generators/lib/auth/templates/processor/google.js.tmpl +178 -0
- package/generators/lib/auth/templates/processor/login.js.tmpl +8 -8
- package/generators/lib/auth/templates/processor/logout.js.tmpl +2 -2
- package/generators/lib/auth/templates/processor/me.js.tmpl +2 -2
- package/generators/lib/auth/templates/processor/refresh.js.tmpl +6 -6
- package/generators/lib/auth/templates/processor/register.js.tmpl +4 -4
- package/generators/lib/auth/templates/processor/reset-password.js.tmpl +7 -7
- package/generators/lib/auth/templates/rfx_auth.js.tmpl +3 -0
- package/generators/lib/generators/model-generator.js +46 -59
- package/generators/lib/help-generator.js +41 -3
- package/generators/lib/payload/endpoint-schema-validator.js +8 -3
- package/generators/lib/payload/field-projections.js +116 -0
- package/generators/lib/payload/payload-runner.js +164 -48
- package/generators/lib/payload/schema-diff.js +108 -0
- package/generators/lib/sdk/generator.js +719 -0
- package/generators/lib/sdk/naming.js +48 -0
- package/generators/lib/sdk/runtime/README.md.tmpl +207 -0
- package/generators/lib/sdk/runtime/auth-client.js +186 -0
- package/generators/lib/sdk/runtime/deploy.mjs.tmpl +85 -0
- package/generators/lib/sdk/runtime/http-client.js +81 -0
- package/generators/lib/sdk/runtime/resource-client.js +59 -0
- package/generators/lib/sdk/runtime/storage.js +31 -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/generators/lib/utils/cli-output.js +40 -0
- package/generators/lib/utils/config-resolver.js +61 -0
- package/generators/lib/utils/database-introspector.js +28 -5
- 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/soft-delete-dashboard-guard.js +1 -1
- package/src/utils/sql-table-extractor.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
|
@@ -125,7 +125,7 @@ function parseFkColumns(fkColumnsStr) {
|
|
|
125
125
|
if (typeof fkColumnsStr !== 'string' || fkColumnsStr.trim().length === 0) {
|
|
126
126
|
throw new Error(
|
|
127
127
|
'--fk-columns is required when --expand-fk is set ' +
|
|
128
|
-
'(format:
|
|
128
|
+
'(format: ref_table.column or local_col:ref_table.column)'
|
|
129
129
|
);
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -137,18 +137,32 @@ function parseFkColumns(fkColumnsStr) {
|
|
|
137
137
|
const specs = [];
|
|
138
138
|
const seen = new Set();
|
|
139
139
|
for (const raw of entries) {
|
|
140
|
-
|
|
140
|
+
let localColumn = null;
|
|
141
|
+
let qualified = raw;
|
|
142
|
+
|
|
143
|
+
const colonIdx = raw.indexOf(':');
|
|
144
|
+
if (colonIdx !== -1) {
|
|
145
|
+
localColumn = raw.slice(0, colonIdx).trim();
|
|
146
|
+
qualified = raw.slice(colonIdx + 1).trim();
|
|
147
|
+
if (!localColumn) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Invalid --fk-columns entry "${raw}": local column name before ':' is empty`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const parts = qualified.split('.');
|
|
141
155
|
if (parts.length !== 2 || !parts[0].trim() || !parts[1].trim()) {
|
|
142
156
|
throw new Error(
|
|
143
|
-
`Invalid --fk-columns entry "${raw}":
|
|
157
|
+
`Invalid --fk-columns entry "${raw}": must be ref_table.column or local_col:ref_table.column`
|
|
144
158
|
);
|
|
145
159
|
}
|
|
146
160
|
const table = parts[0].trim();
|
|
147
161
|
const column = parts[1].trim();
|
|
148
|
-
const
|
|
149
|
-
if (seen.has(
|
|
150
|
-
seen.add(
|
|
151
|
-
specs.push({ table, column, raw });
|
|
162
|
+
const dedupeKey = `${localColumn ? localColumn.toLowerCase() + ':' : ''}${table.toLowerCase()}.${column.toLowerCase()}`;
|
|
163
|
+
if (seen.has(dedupeKey)) continue;
|
|
164
|
+
seen.add(dedupeKey);
|
|
165
|
+
specs.push({ table, column, localColumn, raw });
|
|
152
166
|
}
|
|
153
167
|
return specs;
|
|
154
168
|
}
|
|
@@ -180,10 +194,13 @@ function deriveTableAlias(tableName) {
|
|
|
180
194
|
* @param {{references: {schema?: string, table: string}}} fk
|
|
181
195
|
* @returns {string}
|
|
182
196
|
*/
|
|
183
|
-
function qualifiedRefTable(fk) {
|
|
197
|
+
function qualifiedRefTable(fk, dbType) {
|
|
184
198
|
const schema = fk && fk.references && fk.references.schema;
|
|
185
199
|
const table = fk && fk.references && fk.references.table;
|
|
186
|
-
|
|
200
|
+
if (!schema) return table;
|
|
201
|
+
if (dbType === 'oracle') return table;
|
|
202
|
+
if (schema === 'public') return table;
|
|
203
|
+
return `${schema}.${table}`;
|
|
187
204
|
}
|
|
188
205
|
|
|
189
206
|
/**
|
|
@@ -257,7 +274,7 @@ function pickDisplayColumn(refColumns, primaryKey) {
|
|
|
257
274
|
* @param {string[]} baseColumns - kolom fisik base table (non-audit)
|
|
258
275
|
* @returns {{updatedPayload: Object, sqlRelPath: string, sqlContent: string}}
|
|
259
276
|
*/
|
|
260
|
-
function buildForeignKeyExpansion(payload, fkSpec, foreignKeys, refColumnsMap, baseColumns) {
|
|
277
|
+
function buildForeignKeyExpansion(payload, fkSpec, foreignKeys, refColumnsMap, baseColumns, dbType, joinType) {
|
|
261
278
|
const tableName = payload.tableName;
|
|
262
279
|
|
|
263
280
|
// Base fields = fieldName yang benar-benar kolom fisik base table.
|
|
@@ -268,12 +285,16 @@ function buildForeignKeyExpansion(payload, fkSpec, foreignKeys, refColumnsMap, b
|
|
|
268
285
|
// Map refTable(lower) -> FK; deteksi FK ganda ke tabel referensi yang sama.
|
|
269
286
|
const fkByRefTable = new Map();
|
|
270
287
|
const dupRefTables = new Set();
|
|
288
|
+
const fkByLocalCol = new Map();
|
|
271
289
|
for (const fk of (foreignKeys || [])) {
|
|
272
290
|
const rt = fk && fk.references && fk.references.table ? String(fk.references.table) : '';
|
|
273
291
|
if (!rt) continue;
|
|
274
292
|
const key = rt.toLowerCase();
|
|
275
293
|
if (fkByRefTable.has(key)) dupRefTables.add(key);
|
|
276
294
|
else fkByRefTable.set(key, fk);
|
|
295
|
+
for (const localCol of (fk.columns || [])) {
|
|
296
|
+
fkByLocalCol.set(String(localCol).toLowerCase(), fk);
|
|
297
|
+
}
|
|
277
298
|
}
|
|
278
299
|
|
|
279
300
|
if (fkByRefTable.size === 0) {
|
|
@@ -292,25 +313,47 @@ function buildForeignKeyExpansion(payload, fkSpec, foreignKeys, refColumnsMap, b
|
|
|
292
313
|
};
|
|
293
314
|
const baseAlias = uniqueAlias(deriveTableAlias(tableName));
|
|
294
315
|
|
|
295
|
-
const refTableAlias = new Map(); //
|
|
296
|
-
const refSelections = []; // { alias, column, refTableName }
|
|
316
|
+
const refTableAlias = new Map(); // aliasKey -> { alias, fk, refTableName }
|
|
317
|
+
const refSelections = []; // { alias, column, refTableName, localColumn }
|
|
297
318
|
for (const spec of fkSpec) {
|
|
298
319
|
const key = spec.table.toLowerCase();
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
);
|
|
320
|
+
|
|
321
|
+
let fk;
|
|
322
|
+
if (spec.localColumn) {
|
|
323
|
+
const localKey = spec.localColumn.toLowerCase();
|
|
324
|
+
if (!fkByLocalCol.has(localKey)) {
|
|
325
|
+
const available = [...fkByLocalCol.keys()].join(', ') || '(none)';
|
|
326
|
+
throw new Error(
|
|
327
|
+
`Local FK column "${spec.localColumn}" not found in foreign keys of "${tableName}". ` +
|
|
328
|
+
`Local FK columns available: ${available}`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
fk = fkByLocalCol.get(localKey);
|
|
332
|
+
const refTableKey = fk.references.table.toLowerCase();
|
|
333
|
+
if (refTableKey !== key) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`Local FK column "${spec.localColumn}" references table "${fk.references.table}", ` +
|
|
336
|
+
`not "${spec.table}" as specified`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
if (!fkByRefTable.has(key)) {
|
|
341
|
+
const available = [...fkByRefTable.keys()].join(', ') || '(none)';
|
|
342
|
+
throw new Error(
|
|
343
|
+
`Foreign key reference table "${spec.table}" not found for "${tableName}". ` +
|
|
344
|
+
`Referenced tables available: ${available}`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
if (dupRefTables.has(key)) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
`Table "${tableName}" has multiple foreign keys referencing "${spec.table}"; ` +
|
|
350
|
+
'cannot disambiguate by table name'
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
fk = fkByRefTable.get(key);
|
|
311
354
|
}
|
|
312
355
|
|
|
313
|
-
const
|
|
356
|
+
const aliasKey = spec.localColumn ? spec.localColumn.toLowerCase() : key;
|
|
314
357
|
const refTableName = fk.references.table;
|
|
315
358
|
const refCols = refColumnsMap[key] || refColumnsMap[refTableName] || [];
|
|
316
359
|
const refColsLower = refCols.map(c => String(c).toLowerCase());
|
|
@@ -321,17 +364,17 @@ function buildForeignKeyExpansion(payload, fkSpec, foreignKeys, refColumnsMap, b
|
|
|
321
364
|
);
|
|
322
365
|
}
|
|
323
366
|
|
|
324
|
-
if (!refTableAlias.has(
|
|
325
|
-
refTableAlias.set(
|
|
367
|
+
if (!refTableAlias.has(aliasKey)) {
|
|
368
|
+
refTableAlias.set(aliasKey, {
|
|
326
369
|
alias: uniqueAlias(deriveTableAlias(refTableName)),
|
|
327
370
|
fk,
|
|
328
371
|
refTableName,
|
|
329
372
|
// Dipakai khusus untuk klausa JOIN (FROM/LEFT JOIN); refTableName (bare)
|
|
330
373
|
// tetap dipakai untuk alias derivation & penamaan kolom output.
|
|
331
|
-
refQualified: qualifiedRefTable(fk)
|
|
374
|
+
refQualified: qualifiedRefTable(fk, dbType)
|
|
332
375
|
});
|
|
333
376
|
}
|
|
334
|
-
refSelections.push({ alias: refTableAlias.get(
|
|
377
|
+
refSelections.push({ alias: refTableAlias.get(aliasKey).alias, column: spec.column, refTableName, localColumn: spec.localColumn || null });
|
|
335
378
|
}
|
|
336
379
|
|
|
337
380
|
// Output name + collision auto-prefix. Hitung occurrence nama tentatif
|
|
@@ -347,7 +390,7 @@ function buildForeignKeyExpansion(payload, fkSpec, foreignKeys, refColumnsMap, b
|
|
|
347
390
|
for (const r of refSelections) {
|
|
348
391
|
let output = r.column;
|
|
349
392
|
if ((nameCount.get(r.column) || 0) > 1) {
|
|
350
|
-
output = `${r.refTableName}_${r.column}`.toLowerCase();
|
|
393
|
+
output = `${r.localColumn || r.refTableName}_${r.column}`.toLowerCase();
|
|
351
394
|
}
|
|
352
395
|
if (finalNames.has(output)) {
|
|
353
396
|
throw new Error(
|
|
@@ -380,7 +423,8 @@ function buildForeignKeyExpansion(payload, fkSpec, foreignKeys, refColumnsMap, b
|
|
|
380
423
|
for (let i = 0; i < pairCount; i++) {
|
|
381
424
|
conds.push(`${info.alias}.${refCols[i]} = ${baseAlias}.${localCols[i]}`);
|
|
382
425
|
}
|
|
383
|
-
|
|
426
|
+
const joinKeyword = joinType === 'inner' ? 'INNER JOIN' : 'LEFT JOIN';
|
|
427
|
+
sqlLines.push(`${joinKeyword} ${info.refQualified} ${info.alias} ON ${conds.join(' AND ')}`);
|
|
384
428
|
}
|
|
385
429
|
const sqlContent = `${sqlLines.join('\n')}\n`;
|
|
386
430
|
|
|
@@ -1181,7 +1225,7 @@ function buildPayloadAdvisories(payload, info) {
|
|
|
1181
1225
|
type: 'fk',
|
|
1182
1226
|
table,
|
|
1183
1227
|
message: `has ${fkCount} foreign key(s) not surfaced as JOIN`,
|
|
1184
|
-
command: `npx restforge payload sync --table=${table} --expand-fk`
|
|
1228
|
+
command: `npx restforge payload sync --table=${table} --expand-fk=both`
|
|
1185
1229
|
});
|
|
1186
1230
|
}
|
|
1187
1231
|
|
|
@@ -1768,7 +1812,7 @@ class PayloadGenerator {
|
|
|
1768
1812
|
let result;
|
|
1769
1813
|
if (args.sync) {
|
|
1770
1814
|
result = await validator.runSync(targetTable, this, {
|
|
1771
|
-
|
|
1815
|
+
expandFkMode: args.expandFkMode || null,
|
|
1772
1816
|
fkColumns: args.fkColumns || null
|
|
1773
1817
|
});
|
|
1774
1818
|
} else if (args.diff) {
|
|
@@ -2439,12 +2483,12 @@ class SchemaValidator {
|
|
|
2439
2483
|
* @param {string|null} tableName - Nama table spesifik, atau null untuk semua
|
|
2440
2484
|
* @param {PayloadGenerator} generator - Instance PayloadGenerator untuk re-generate
|
|
2441
2485
|
* @param {Object} [options]
|
|
2442
|
-
* @param {
|
|
2486
|
+
* @param {'both'|'datatables-only'|null} [options.expandFkMode] - Mode FK JOIN expansion
|
|
2443
2487
|
* @param {string} [options.fkColumns] - Daftar kolom referensi `table.column,...`
|
|
2444
2488
|
* @returns {Promise<Object>} { total, synced, skipped, error }
|
|
2445
2489
|
*/
|
|
2446
2490
|
async runSync(tableName, generator, options = {}) {
|
|
2447
|
-
const
|
|
2491
|
+
const expandFkMode = options.expandFkMode || null;
|
|
2448
2492
|
const fkColumns = options.fkColumns || null;
|
|
2449
2493
|
|
|
2450
2494
|
console.log();
|
|
@@ -2455,7 +2499,7 @@ class SchemaValidator {
|
|
|
2455
2499
|
|
|
2456
2500
|
// Mode expand-fk butuh target tabel tunggal dan --fk-columns yang valid.
|
|
2457
2501
|
// Validasi upfront agar gagal cepat dengan pesan jelas.
|
|
2458
|
-
if (
|
|
2502
|
+
if (expandFkMode) {
|
|
2459
2503
|
if (!tableName) {
|
|
2460
2504
|
console.log(' [ERROR] --expand-fk requires --table=<table>');
|
|
2461
2505
|
console.log();
|
|
@@ -2515,7 +2559,7 @@ class SchemaValidator {
|
|
|
2515
2559
|
// dari sync biasa, mode ini tetap memproses meski kolom fisik sudah
|
|
2516
2560
|
// in-sync (status 'ok'); bila ada drift kolom fisik, rekonsiliasi dulu
|
|
2517
2561
|
// lewat regeneratePayload baru ekspansi FK diterapkan di atasnya.
|
|
2518
|
-
if (
|
|
2562
|
+
if (expandFkMode) {
|
|
2519
2563
|
let workingPayload = payload;
|
|
2520
2564
|
if (comparison.status === 'drift') {
|
|
2521
2565
|
try {
|
|
@@ -2530,6 +2574,9 @@ class SchemaValidator {
|
|
|
2530
2574
|
let expansion;
|
|
2531
2575
|
try {
|
|
2532
2576
|
expansion = await this.applyForeignKeyExpansion(workingPayload, fkColumns, { fileName });
|
|
2577
|
+
if (expandFkMode === 'datatables-only') {
|
|
2578
|
+
expansion.updatedPayload.viewQuery = workingPayload.viewQuery;
|
|
2579
|
+
}
|
|
2533
2580
|
} catch (e) {
|
|
2534
2581
|
console.log(` [ERROR] ${fileName} - expand-fk failed: ${e.message}`);
|
|
2535
2582
|
errorCount++;
|
|
@@ -2638,16 +2685,27 @@ class SchemaValidator {
|
|
|
2638
2685
|
const refColumnsMap = {};
|
|
2639
2686
|
let fkSpec;
|
|
2640
2687
|
|
|
2688
|
+
// Deteksi tabel referensi yang dirujuk lebih dari satu FK (ambiguous).
|
|
2689
|
+
// Dipakai di kedua mode (auto dan hybrid) untuk penentuan localColumn otomatis.
|
|
2690
|
+
const refTableCount = new Map();
|
|
2691
|
+
for (const fk of foreignKeys) {
|
|
2692
|
+
const rt = fk && fk.references && fk.references.table;
|
|
2693
|
+
if (!rt) continue;
|
|
2694
|
+
refTableCount.set(String(rt).toLowerCase(), (refTableCount.get(String(rt).toLowerCase()) || 0) + 1);
|
|
2695
|
+
}
|
|
2696
|
+
const dupRefTables = new Set(
|
|
2697
|
+
[...refTableCount.entries()].filter(([, n]) => n > 1).map(([k]) => k)
|
|
2698
|
+
);
|
|
2699
|
+
|
|
2641
2700
|
if (isAuto) {
|
|
2642
2701
|
// Auto-resolve: satu kolom display per foreign key.
|
|
2702
|
+
// FK ganda ke tabel yang sama di-disambiguasi otomatis via nama kolom FK
|
|
2703
|
+
// lokal sebagai prefix — tidak error, tidak butuh --fk-columns.
|
|
2643
2704
|
fkSpec = [];
|
|
2644
2705
|
for (const fk of foreignKeys) {
|
|
2645
2706
|
const rt = fk && fk.references && fk.references.table;
|
|
2646
2707
|
if (!rt) continue;
|
|
2647
2708
|
const key = String(rt).toLowerCase();
|
|
2648
|
-
// qualifiedRefTable() dipakai untuk query ke DB (perlu schema yang benar
|
|
2649
|
-
// bila parent bukan di schema 'public'); `rt` (bare) tetap dipakai sebagai
|
|
2650
|
-
// key map + identitas fkSpec (selaras format `--fk-columns=table.column`).
|
|
2651
2709
|
const qualifiedRt = qualifiedRefTable(fk);
|
|
2652
2710
|
if (!refColumnsMap[key]) {
|
|
2653
2711
|
refColumnsMap[key] = await this.db.getColumns(qualifiedRt);
|
|
@@ -2662,25 +2720,83 @@ class SchemaValidator {
|
|
|
2662
2720
|
'(no name/code column and no primary key found). Provide --fk-columns explicitly.'
|
|
2663
2721
|
);
|
|
2664
2722
|
}
|
|
2665
|
-
|
|
2666
|
-
|
|
2723
|
+
const isAmbiguous = dupRefTables.has(key);
|
|
2724
|
+
const localColumn = isAmbiguous ? (fk.columns && fk.columns[0]) || null : null;
|
|
2725
|
+
fkSpec.push({ table: rt, column: display, localColumn, raw: `${rt}.${display}` });
|
|
2726
|
+
if (isAmbiguous && localColumn) {
|
|
2727
|
+
console.log(` [AUTO] ${tableName}.${localColumn} -> ${rt}.${display} (display column, auto-disambiguated)`);
|
|
2728
|
+
} else {
|
|
2729
|
+
console.log(` [AUTO] ${tableName} -> ${rt}.${display} (display column)`);
|
|
2730
|
+
}
|
|
2667
2731
|
}
|
|
2668
2732
|
} else {
|
|
2669
|
-
//
|
|
2670
|
-
|
|
2671
|
-
const
|
|
2733
|
+
// Hybrid: --fk-columns berlaku sebagai override/disambiguasi untuk FK yang
|
|
2734
|
+
// disebut; FK lain yang tidak disebut tetap di-auto-resolve.
|
|
2735
|
+
const explicitSpecs = parseFkColumns(fkColumnsStr);
|
|
2736
|
+
|
|
2737
|
+
// Index explicit specs: by localColumn (untuk FK ganda) atau by refTable (untuk FK tunggal).
|
|
2738
|
+
const explicitByLocalCol = new Map();
|
|
2739
|
+
const explicitByTable = new Map();
|
|
2740
|
+
for (const spec of explicitSpecs) {
|
|
2741
|
+
if (spec.localColumn) {
|
|
2742
|
+
explicitByLocalCol.set(spec.localColumn.toLowerCase(), spec);
|
|
2743
|
+
} else {
|
|
2744
|
+
const k = spec.table.toLowerCase();
|
|
2745
|
+
if (!explicitByTable.has(k)) explicitByTable.set(k, spec);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
fkSpec = [];
|
|
2672
2750
|
for (const fk of foreignKeys) {
|
|
2673
2751
|
const rt = fk && fk.references && fk.references.table;
|
|
2674
2752
|
if (!rt) continue;
|
|
2675
2753
|
const key = String(rt).toLowerCase();
|
|
2676
|
-
|
|
2754
|
+
const localColRaw = fk.columns && fk.columns[0];
|
|
2755
|
+
const localColKey = localColRaw ? String(localColRaw).toLowerCase() : null;
|
|
2756
|
+
const isAmbiguous = dupRefTables.has(key);
|
|
2757
|
+
|
|
2758
|
+
// Cari explicit spec yang cocok dengan FK ini.
|
|
2759
|
+
let matchedSpec = null;
|
|
2760
|
+
if (localColKey && explicitByLocalCol.has(localColKey)) {
|
|
2761
|
+
matchedSpec = explicitByLocalCol.get(localColKey);
|
|
2762
|
+
} else if (!isAmbiguous && explicitByTable.has(key)) {
|
|
2763
|
+
matchedSpec = explicitByTable.get(key);
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
// Fetch kolom tabel referensi (dibutuhkan baik untuk explicit maupun auto).
|
|
2767
|
+
const qualifiedRt = qualifiedRefTable(fk);
|
|
2677
2768
|
if (!refColumnsMap[key]) {
|
|
2678
|
-
refColumnsMap[key] = await this.db.getColumns(
|
|
2769
|
+
refColumnsMap[key] = await this.db.getColumns(qualifiedRt);
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
if (matchedSpec) {
|
|
2773
|
+
fkSpec.push(matchedSpec);
|
|
2774
|
+
const prefix = matchedSpec.localColumn ? `${tableName}.${matchedSpec.localColumn}` : tableName;
|
|
2775
|
+
console.log(` [EXPLICIT] ${prefix} -> ${matchedSpec.table}.${matchedSpec.column}`);
|
|
2776
|
+
} else {
|
|
2777
|
+
// FK tidak disebut di --fk-columns: auto-resolve.
|
|
2778
|
+
const refPk = typeof this.db.getPrimaryKey === 'function'
|
|
2779
|
+
? await this.db.getPrimaryKey(qualifiedRt)
|
|
2780
|
+
: null;
|
|
2781
|
+
const display = pickDisplayColumn(refColumnsMap[key], refPk);
|
|
2782
|
+
if (!display) {
|
|
2783
|
+
throw new Error(
|
|
2784
|
+
`Could not auto-resolve a display column for referenced table "${rt}" ` +
|
|
2785
|
+
'(no name/code column and no primary key found). Provide --fk-columns explicitly.'
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
const localColumn = isAmbiguous ? localColRaw || null : null;
|
|
2789
|
+
fkSpec.push({ table: rt, column: display, localColumn, raw: `${rt}.${display}` });
|
|
2790
|
+
if (isAmbiguous && localColumn) {
|
|
2791
|
+
console.log(` [AUTO] ${tableName}.${localColumn} -> ${rt}.${display} (display column, auto-disambiguated)`);
|
|
2792
|
+
} else {
|
|
2793
|
+
console.log(` [AUTO] ${tableName} -> ${rt}.${display} (display column)`);
|
|
2794
|
+
}
|
|
2679
2795
|
}
|
|
2680
2796
|
}
|
|
2681
2797
|
}
|
|
2682
2798
|
|
|
2683
|
-
return buildForeignKeyExpansion(payload, fkSpec, foreignKeys, refColumnsMap, baseColumns);
|
|
2799
|
+
return buildForeignKeyExpansion(payload, fkSpec, foreignKeys, refColumnsMap, baseColumns, this.db.dbType, this.db.fkAutoJoin || 'left');
|
|
2684
2800
|
}
|
|
2685
2801
|
|
|
2686
2802
|
/**
|
|
@@ -190,9 +190,16 @@ async function compareSchemaStrict(payload, db, options = {}) {
|
|
|
190
190
|
|
|
191
191
|
const { explicit, audit } = resolveEffectiveFieldList(payload);
|
|
192
192
|
|
|
193
|
+
// Bila ada kegagalan resolusi query (file: SQL error), kolom non-fisik yang
|
|
194
|
+
// tidak ditemukan di validColumnSet kemungkinan kolom JOIN yang tidak bisa
|
|
195
|
+
// diverifikasi — bukan drift nyata. Jangan laporkan sebagai removed agar
|
|
196
|
+
// SQL error tetap menjadi satu-satunya blocker yang terlihat.
|
|
197
|
+
const hasQueryFailures = (querySourceResult.warnings || []).length > 0;
|
|
198
|
+
|
|
193
199
|
const explicitSet = new Set(explicit);
|
|
194
200
|
for (const field of explicit) {
|
|
195
201
|
if (!validColumnSet.has(field)) {
|
|
202
|
+
if (hasQueryFailures && !dbColumnSet.has(field)) continue;
|
|
196
203
|
result.removed.push({ column: field, source: 'payload' });
|
|
197
204
|
}
|
|
198
205
|
}
|
|
@@ -486,6 +493,106 @@ function substituteQueryPlaceholders(sql, tableName) {
|
|
|
486
493
|
return sql.replace(/\$\{tableName\}/g, tableName);
|
|
487
494
|
}
|
|
488
495
|
|
|
496
|
+
/**
|
|
497
|
+
* Resolve kolom per-sumber untuk derivasi field projections di Phase 02.
|
|
498
|
+
*
|
|
499
|
+
* Mengembalikan:
|
|
500
|
+
* - `physicalColumns`: nama kolom fisik tabel dari getDetailedColumnInfo
|
|
501
|
+
* - `readSourceColumns`: kolom dari sumber-baca yang menang resolusi
|
|
502
|
+
* (viewName ≠ tableName → viewName; ada viewQuery → viewQuery; else → physicalColumns)
|
|
503
|
+
* [] berarti sumber ada tapi introspeksi gagal (hard-fail trigger di deriveFieldProjections)
|
|
504
|
+
* - `datatablesColumns`: kolom dari datatablesQuery.
|
|
505
|
+
* null = tidak ada datatablesQuery; [] = ada tapi introspeksi gagal
|
|
506
|
+
*
|
|
507
|
+
* Dipanggil di alur `endpoint create` menggunakan db handle yang sama dengan
|
|
508
|
+
* compareSchemaStrict (reuse koneksi, tidak buka koneksi baru — D2).
|
|
509
|
+
*
|
|
510
|
+
* @param {Object} payload
|
|
511
|
+
* @param {Object} db - DatabaseIntrospector terkoneksi
|
|
512
|
+
* @param {Object} [options]
|
|
513
|
+
* @param {string} [options.payloadDir] - Folder payload untuk resolve file: reference
|
|
514
|
+
* @returns {Promise<{physicalColumns: string[], readSourceColumns: string[], datatablesColumns: string[]|null}>}
|
|
515
|
+
*/
|
|
516
|
+
async function resolveFieldProjectionInputs(payload, db, options = {}) {
|
|
517
|
+
const payloadDir = options.payloadDir || null;
|
|
518
|
+
const tableName = payload.tableName;
|
|
519
|
+
|
|
520
|
+
// 1. physicalColumns dari getDetailedColumnInfo
|
|
521
|
+
let physicalColumns = [];
|
|
522
|
+
try {
|
|
523
|
+
const dbCols = await db.getDetailedColumnInfo(tableName);
|
|
524
|
+
physicalColumns = Array.isArray(dbCols) ? dbCols.map(c => c.column_name) : [];
|
|
525
|
+
} catch (_err) {
|
|
526
|
+
// Gagal → physicalColumns tetap []
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// 2. readSourceColumns: priority viewName → viewQuery → tableName (physicalColumns)
|
|
530
|
+
const hasViewName = typeof payload.viewName === 'string' &&
|
|
531
|
+
payload.viewName.trim().length > 0 &&
|
|
532
|
+
payload.viewName.trim() !== tableName;
|
|
533
|
+
const hasViewQuery = typeof payload.viewQuery === 'string' &&
|
|
534
|
+
payload.viewQuery.trim().length > 0;
|
|
535
|
+
|
|
536
|
+
let readSourceColumns;
|
|
537
|
+
if (hasViewName) {
|
|
538
|
+
// P1: viewName berbeda dari tableName — introspeksi view
|
|
539
|
+
try {
|
|
540
|
+
const viewCols = await db.getDetailedColumnInfo(payload.viewName.trim());
|
|
541
|
+
readSourceColumns = Array.isArray(viewCols) ? viewCols.map(c => c.column_name) : [];
|
|
542
|
+
} catch (_err) {
|
|
543
|
+
readSourceColumns = [];
|
|
544
|
+
}
|
|
545
|
+
} else if (hasViewQuery) {
|
|
546
|
+
// P2: viewQuery — describe query
|
|
547
|
+
const resolved = resolveSqlSource(payload.viewQuery, payloadDir, 'viewQuery');
|
|
548
|
+
if (!resolved.ok) {
|
|
549
|
+
readSourceColumns = [];
|
|
550
|
+
} else {
|
|
551
|
+
const sql = substituteQueryPlaceholders(resolved.sql, tableName);
|
|
552
|
+
if (typeof db.describeQueryColumns !== 'function') {
|
|
553
|
+
readSourceColumns = [];
|
|
554
|
+
} else {
|
|
555
|
+
try {
|
|
556
|
+
const desc = await db.describeQueryColumns(sql);
|
|
557
|
+
readSourceColumns = (desc.ok && Array.isArray(desc.columns))
|
|
558
|
+
? desc.columns
|
|
559
|
+
: [];
|
|
560
|
+
} catch (_err) {
|
|
561
|
+
readSourceColumns = [];
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
} else {
|
|
566
|
+
// Fallback: read source = tableName → gunakan physicalColumns
|
|
567
|
+
readSourceColumns = physicalColumns;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 3. datatablesColumns dari datatablesQuery (null bila tidak ada)
|
|
571
|
+
let datatablesColumns = null;
|
|
572
|
+
if (typeof payload.datatablesQuery === 'string' && payload.datatablesQuery.trim().length > 0) {
|
|
573
|
+
const resolved = resolveSqlSource(payload.datatablesQuery, payloadDir, 'datatablesQuery');
|
|
574
|
+
if (!resolved.ok) {
|
|
575
|
+
datatablesColumns = [];
|
|
576
|
+
} else {
|
|
577
|
+
const sql = substituteQueryPlaceholders(resolved.sql, tableName);
|
|
578
|
+
if (typeof db.describeQueryColumns !== 'function') {
|
|
579
|
+
datatablesColumns = [];
|
|
580
|
+
} else {
|
|
581
|
+
try {
|
|
582
|
+
const desc = await db.describeQueryColumns(sql);
|
|
583
|
+
datatablesColumns = (desc.ok && Array.isArray(desc.columns))
|
|
584
|
+
? desc.columns
|
|
585
|
+
: [];
|
|
586
|
+
} catch (_err) {
|
|
587
|
+
datatablesColumns = [];
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return { physicalColumns, readSourceColumns, datatablesColumns };
|
|
594
|
+
}
|
|
595
|
+
|
|
489
596
|
module.exports = {
|
|
490
597
|
DEFAULT_EXCLUDED_COLUMNS,
|
|
491
598
|
normalizeDatabaseType,
|
|
@@ -493,5 +600,6 @@ module.exports = {
|
|
|
493
600
|
compareSchemaStrict,
|
|
494
601
|
formatDriftReport,
|
|
495
602
|
resolveQuerySourceColumns,
|
|
603
|
+
resolveFieldProjectionInputs,
|
|
496
604
|
resolveSqlSource
|
|
497
605
|
};
|