@restforgejs/platform 5.1.7 → 5.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/restforge-hwinfo-linux +0 -0
- package/bin/restforge-hwinfo.exe +0 -0
- package/build-info.json +2 -2
- package/cli/consumer-deploy.js +1 -1
- package/cli/consumer.js +1 -1
- package/generators/cli/payload/generate.js +10 -2
- package/generators/cli/schema/apply.js +6 -1
- package/generators/cli/schema/diff.js +6 -1
- package/generators/cli/schema/introspect.js +32 -11
- package/generators/lib/data/db-executor.js +8 -8
- package/generators/lib/data/envelope.js +3 -3
- package/generators/lib/dbschema-kit/apply-engine.js +20 -0
- package/generators/lib/dbschema-kit/dialect/mysql.js +2 -0
- package/generators/lib/dbschema-kit/dialect/oracle.js +2 -0
- package/generators/lib/dbschema-kit/dialect/postgres.js +4 -0
- package/generators/lib/dbschema-kit/dialect/sqlite.js +5 -0
- package/generators/lib/dbschema-kit/diff-engine.js +22 -1
- package/generators/lib/dbschema-kit/diff-reporter.js +293 -272
- package/generators/lib/dbschema-kit/emitters/create-index.js +23 -1
- package/generators/lib/dbschema-kit/emitters/create-table.js +48 -0
- package/generators/lib/dbschema-kit/introspect-mapper.js +154 -2
- package/generators/lib/dbschema-kit/ir-builder.js +84 -1
- package/generators/lib/dbschema-kit/schema-printer.js +20 -0
- package/generators/lib/dbschema-kit/soft-delete-constants.js +111 -0
- package/generators/lib/dbschema-kit/validator/schema-validator.js +231 -0
- package/generators/lib/generators/processor-validation-generator.js +16 -16
- package/generators/lib/payload/payload-runner.js +711 -1
- package/generators/lib/payload/schema-diff.js +7 -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/database-introspector.js +48 -0
- package/generators/lib/utils/env-manager.js +4 -4
- package/generators/lib/utils/file-utils.js +6 -6
- package/generators/lib/utils/payload-processor.js +18 -2
- package/generators/lib/validators/argument-validator.js +2 -2
- package/generators/lib/validators/dashboard-validator.js +35 -1
- package/generators/lib/validators/payload-validator.js +460 -33
- package/integrity-manifest.json +20 -20
- package/package.json +2 -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 -0
- package/src/utils/sql-table-extractor.js +1 -0
- 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
- package/generators/lib/utils/sql-table-extractor.js +0 -83
|
@@ -32,6 +32,18 @@ const {
|
|
|
32
32
|
detectAuditAlignment
|
|
33
33
|
} = require('../utils/audit-columns');
|
|
34
34
|
const { compareSchemaStrict } = require('./schema-diff');
|
|
35
|
+
const { loadSchemaPath } = require('../dbschema-kit/loader');
|
|
36
|
+
const {
|
|
37
|
+
SOFT_DELETE_COLUMNS,
|
|
38
|
+
isSoftDeleteEnabled,
|
|
39
|
+
softDeleteCheckName
|
|
40
|
+
} = require('../dbschema-kit/soft-delete-constants');
|
|
41
|
+
const softDeleteDashboardGuard = require('../../../src/utils/soft-delete-dashboard-guard');
|
|
42
|
+
|
|
43
|
+
// Panjang identifier maksimum PostgreSQL (Fase 1 PostgreSQL-only). Dipakai untuk
|
|
44
|
+
// menderivasi nama CHECK soft-delete saat menentukan status parent-soft-delete pada
|
|
45
|
+
// FK-awareness (R20), identik dengan emitter forward (Phase 02) + reverse (Phase 03).
|
|
46
|
+
const POSTGRES_MAX_IDENTIFIER_LENGTH = 63;
|
|
35
47
|
|
|
36
48
|
// Kolom audit yang di-handle otomatis oleh RESTForge runtime (base-model).
|
|
37
49
|
// Konstanta dipakai dari shared util agar source-of-truth tunggal lintas
|
|
@@ -487,6 +499,579 @@ function applyIsActiveDefaultScope(payload, hasIsActive) {
|
|
|
487
499
|
if (Object.keys(ds).length === 0) delete payload.defaultScope;
|
|
488
500
|
}
|
|
489
501
|
|
|
502
|
+
// ============================================================================
|
|
503
|
+
// SOFT-DELETE DERIVATION (SDF -> RDF) — payload generate (R12/R13)
|
|
504
|
+
// ============================================================================
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Cari IR model untuk sebuah tabel di dalam Map hasil loadSchemaPath().
|
|
508
|
+
*
|
|
509
|
+
* Strategi match deterministik:
|
|
510
|
+
* 1. Exact qualifiedName: `models.get(tableName)` (Map dikunci qualifiedName).
|
|
511
|
+
* 2. Bare name: cocokkan `model.tableName === <bare>` (schema prefix di-strip)
|
|
512
|
+
* atau `model.qualifiedName === tableName`.
|
|
513
|
+
*
|
|
514
|
+
* Bila lebih dari satu model cocok dengan bare name (mis. tabel sama di dua schema),
|
|
515
|
+
* lempar ERROR ambiguous — pemanggil harus qualify dengan schema.table (R1, strict).
|
|
516
|
+
*
|
|
517
|
+
* @param {Map<string, Object>} models - hasil loadSchemaPath()
|
|
518
|
+
* @param {string} tableName - nama tabel (bare atau schema.table)
|
|
519
|
+
* @returns {Object|null} IR model atau null bila tidak ditemukan
|
|
520
|
+
*/
|
|
521
|
+
function findModelByTable(models, tableName) {
|
|
522
|
+
if (models.has(tableName)) return models.get(tableName);
|
|
523
|
+
|
|
524
|
+
const bare = String(tableName).split('.').pop();
|
|
525
|
+
const matches = [];
|
|
526
|
+
for (const model of models.values()) {
|
|
527
|
+
if (model.tableName === bare || model.qualifiedName === tableName) {
|
|
528
|
+
matches.push(model);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (matches.length === 1) return matches[0];
|
|
533
|
+
if (matches.length > 1) {
|
|
534
|
+
throw new Error(
|
|
535
|
+
`Table '${tableName}' is ambiguous in the SDF: ${matches.length} models match the bare ` +
|
|
536
|
+
`name '${bare}'. Qualify --table with the schema (schema.table) to disambiguate.`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Resolusi blok `softDelete` SDF untuk tabel yang terdeteksi punya kolom soft-delete
|
|
544
|
+
* di database (R12). Memuat SDF via `loadSchemaPath` (jalur identik dengan command
|
|
545
|
+
* `schema`), mencari IR tabel, lalu memastikan blok `softDelete` valid (enabled).
|
|
546
|
+
*
|
|
547
|
+
* Aturan ERROR (tabel R12) — soft-delete WAJIB punya SDF yang konsisten:
|
|
548
|
+
* - `--schema-path` kosong -> ERROR (tidak bisa menurunkan blok)
|
|
549
|
+
* - SDF gagal di-muat / SDF tidak valid -> ERROR (loadSchemaPath menjalankan
|
|
550
|
+
* validateSchema; SDF dengan softDelete invalid gagal di sini)
|
|
551
|
+
* - tabel tidak ada di SDF -> ERROR (SDF tidak ditemukan untuk tabel)
|
|
552
|
+
* - tabel ada tapi tanpa softDelete valid-> ERROR (softDelete.enabled !== true)
|
|
553
|
+
*
|
|
554
|
+
* `schemaPath` adalah nilai flag `--schema-path` (default `"schema"`, selaras
|
|
555
|
+
* `data pull`/`data push`); di-resolve relatif cwd oleh `loadSchemaPath`. Tabel
|
|
556
|
+
* soft-delete tanpa `--schema-path` eksplisit memakai default `"schema"`; bila
|
|
557
|
+
* folder itu tidak ada, ERROR "failed to load SDF" mengarahkan ke `--schema-path`.
|
|
558
|
+
*
|
|
559
|
+
* Validasi semantik blok (R3/R5/R7/R9) sudah ditegakkan `validateSoftDelete` saat
|
|
560
|
+
* load, jadi blok yang dikembalikan di sini dijamin enabled + kolom lengkap + reusable
|
|
561
|
+
* memenuhi syarat panjang.
|
|
562
|
+
*
|
|
563
|
+
* @param {string} tableName - nama tabel target (dari --table)
|
|
564
|
+
* @param {string|null} schemaPath - nilai flag --schema-path
|
|
565
|
+
* @returns {{ enabled: true, reusable?: Array<{field: string, length: number}> }} blok softDelete IR
|
|
566
|
+
* @throws {Error} bila salah satu kondisi R12 ERROR terpenuhi
|
|
567
|
+
*/
|
|
568
|
+
function resolveSoftDeleteForTable(tableName, schemaPath) {
|
|
569
|
+
if (typeof schemaPath !== 'string' || schemaPath.trim() === '') {
|
|
570
|
+
throw new Error(
|
|
571
|
+
`Table '${tableName}' has soft-delete columns (${SOFT_DELETE_COLUMNS.join('/')}) but ` +
|
|
572
|
+
`--schema-path was not provided. payload generate must read the SDF to derive the softDelete ` +
|
|
573
|
+
`block of the RDF (base length is not stored in the database, R12/R13). ` +
|
|
574
|
+
`Re-run with --schema-path=<path-to-sdf-file-or-folder>.`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
let models;
|
|
579
|
+
try {
|
|
580
|
+
models = loadSchemaPath(schemaPath);
|
|
581
|
+
} catch (err) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
`Table '${tableName}': failed to load SDF from --schema-path='${schemaPath}': ${err.message}`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const ir = findModelByTable(models, tableName);
|
|
588
|
+
if (!ir) {
|
|
589
|
+
throw new Error(
|
|
590
|
+
`Table '${tableName}' has soft-delete columns but is not declared in the SDF at ` +
|
|
591
|
+
`'${schemaPath}'. Add the table (with a valid softDelete block) to the SDF, or remove ` +
|
|
592
|
+
`the soft-delete columns from the database (R12).`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (!isSoftDeleteEnabled(ir)) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`Table '${tableName}' has soft-delete columns but its SDF declaration has no valid ` +
|
|
599
|
+
`softDelete block (softDelete.enabled !== true). Declare softDelete in the SDF to generate ` +
|
|
600
|
+
`a soft-delete RDF (R12).`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return ir.softDelete;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Turunkan blok `softDelete` RDF dari blok `softDelete` SDF, secara in-place pada
|
|
609
|
+
* `payload` (pola `applyIsActiveDefaultScope`). Dipanggil HANYA bila tabel terdeteksi
|
|
610
|
+
* soft-delete dan SDF sudah diresolusi valid (R12), jadi seluruh mutasi guarded oleh
|
|
611
|
+
* deteksi (backward-compat byte-identik baseline untuk tabel non-soft-delete, R13).
|
|
612
|
+
*
|
|
613
|
+
* Dua mutasi:
|
|
614
|
+
* 1. Set `payload.softDelete = { enabled, reusable, visibility }`. `enabled`+`reusable`
|
|
615
|
+
* dari SDF; `visibility` default "active_only" (config endpoint, BUKAN turunan SDF —
|
|
616
|
+
* developer ubah per-endpoint nanti, R13/R15). Urutan key tetap enabled→reusable→
|
|
617
|
+
* visibility sesuai R13.
|
|
618
|
+
* 2. Override `fieldValidation[field].maxLength = base length` untuk tiap field reusable
|
|
619
|
+
* (R13): batasi input user ke base sehingga `base + 38 = physical` pas tanpa overflow.
|
|
620
|
+
* Default `generateFieldValidation` memakai character_maximum_length (physical 88);
|
|
621
|
+
* di sinilah perilaku default di-override pasca-generate.
|
|
622
|
+
*
|
|
623
|
+
* Idempoten dan tidak menyentuh field lain di payload.
|
|
624
|
+
*
|
|
625
|
+
* @param {Object} payload - payloadData yang dimutasi
|
|
626
|
+
* @param {{ enabled: boolean, reusable?: Array<{field: string, length: number}> }} sdfSoftDelete
|
|
627
|
+
* @returns {void}
|
|
628
|
+
*/
|
|
629
|
+
function applySoftDeleteDerivation(payload, sdfSoftDelete) {
|
|
630
|
+
const reusable = Array.isArray(sdfSoftDelete && sdfSoftDelete.reusable)
|
|
631
|
+
? sdfSoftDelete.reusable.map((entry) => ({ field: entry.field, length: entry.length }))
|
|
632
|
+
: [];
|
|
633
|
+
|
|
634
|
+
payload.softDelete = {
|
|
635
|
+
enabled: true,
|
|
636
|
+
reusable,
|
|
637
|
+
visibility: 'active_only'
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// Override maxLength reusable -> base length (R13). Entry pasti ada: reusable field
|
|
641
|
+
// wajib unique string (R9) sehingga generateFieldValidation menghasilkan entry-nya.
|
|
642
|
+
if (Array.isArray(payload.fieldValidation)) {
|
|
643
|
+
for (const entry of reusable) {
|
|
644
|
+
const fv = payload.fieldValidation.find((f) => f && f.name === entry.field);
|
|
645
|
+
if (fv && fv.constraints && typeof fv.constraints === 'object') {
|
|
646
|
+
fv.constraints.maxLength = entry.length;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Checkpoint soft-delete dashboard (WARNING, non-blocking) untuk `payload generate`.
|
|
654
|
+
*
|
|
655
|
+
* Saat RDF tabel soft-delete baru ditulis, scan dashboard yang sudah ada di `payloadDir`
|
|
656
|
+
* terhadap registry soft-delete saat ini. `payload generate` TIDAK meregenerasi dashboard
|
|
657
|
+
* yang sudah ada, sehingga bila soft-delete baru diaktifkan pada sebuah tabel, module
|
|
658
|
+
* dashboard lama tetap memuat SQL bocor (ikut membaca baris is_deleted=true). Tiap
|
|
659
|
+
* pelanggaran di-emit sebagai peringatan menonjol yang menyebut dashboard + widget + tabel
|
|
660
|
+
* pemicu, plus penjelasan bahwa dashboard harus diperbaiki lalu diregenerasi manual.
|
|
661
|
+
*
|
|
662
|
+
* WAJIB non-blocking: tidak melempar error. Generate RDF tetap selesai sukses meski ada
|
|
663
|
+
* peringatan. Hard stop atas drift ini ada di checkpoint runtime `serve` (prompt terpisah).
|
|
664
|
+
* Logika difaktorkan ke fungsi module-level agar dapat di-unit-test dengan spy pada `warn`,
|
|
665
|
+
* tanpa menjalankan jalur generate penuh.
|
|
666
|
+
*
|
|
667
|
+
* @param {string} payloadDir - direktori payload (tempat dashboard + file SQL-nya)
|
|
668
|
+
* @param {(msg: string) => void} [warn=console.warn] - sink peringatan (diinjeksi untuk test)
|
|
669
|
+
* @returns {Array} daftar pelanggaran yang diperingatkan (untuk observabilitas/test)
|
|
670
|
+
*/
|
|
671
|
+
function warnSoftDeleteDashboardDrift(payloadDir, warn = console.warn) {
|
|
672
|
+
let violations;
|
|
673
|
+
try {
|
|
674
|
+
violations = softDeleteDashboardGuard.scanDashboardsForViolations(payloadDir);
|
|
675
|
+
} catch (_err) {
|
|
676
|
+
// Scanner best-effort: kegagalan tak terduga TIDAK boleh menggagalkan generate.
|
|
677
|
+
return [];
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
for (const v of violations) {
|
|
681
|
+
const queryContext = v.key === null ? '' : ` (query '${v.key}')`;
|
|
682
|
+
warn(
|
|
683
|
+
`WARNING [soft-delete] dashboard '${v.dashboard}'${queryContext}: ` +
|
|
684
|
+
`${softDeleteDashboardGuard.formatViolationMessage(v)} ` +
|
|
685
|
+
`This dashboard was NOT regenerated by 'payload generate' and its module still serves the ` +
|
|
686
|
+
`leaking query; fix the widget and regenerate the dashboard.`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return violations;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Tentukan apakah tabel parent (`refTable`) adalah tabel soft-delete (R20 FK-awareness).
|
|
695
|
+
* Reuse deteksi name-based Phase 03: parent diakui soft-delete bila (a) ketiga kolom
|
|
696
|
+
* soft-delete (is_deleted/deleted_at/deleted_by) ADA, dan (b) CHECK konsistensi bernama
|
|
697
|
+
* `softDeleteCheckName(refTable, 63)` ADA. CHECK adalah penanda otoritatif (di-emit hanya
|
|
698
|
+
* oleh `schema migrate` dari SDF soft-delete valid), sehingga kombinasi kolom + CHECK cukup
|
|
699
|
+
* pasti tanpa perlu mengecek tipe ulang. Konservatif: ketidakpastian apa pun (introspeksi
|
|
700
|
+
* gagal, method tak tersedia, dialect non-PG) → `false` (cek eksistensi parent saja, R20 fallback).
|
|
701
|
+
*
|
|
702
|
+
* @param {Object} db - DatabaseIntrospector instance
|
|
703
|
+
* @param {string} qualifiedRefTable - nama parent ter-qualify (schema.table bila ada schema)
|
|
704
|
+
* @param {string} refTableName - nama parent TANPA schema (untuk derivasi nama CHECK)
|
|
705
|
+
* @returns {Promise<boolean>}
|
|
706
|
+
*/
|
|
707
|
+
async function isParentSoftDelete(db, qualifiedRefTable, refTableName) {
|
|
708
|
+
try {
|
|
709
|
+
if (!db || typeof db.getColumns !== 'function' || typeof db.getCheckConstraints !== 'function') {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
const cols = await db.getColumns(qualifiedRefTable);
|
|
713
|
+
const colSet = new Set((cols || []).map((c) => String(c).toLowerCase()));
|
|
714
|
+
const hasAllColumns = SOFT_DELETE_COLUMNS.every((c) => colSet.has(c));
|
|
715
|
+
if (!hasAllColumns) return false;
|
|
716
|
+
|
|
717
|
+
const checks = await db.getCheckConstraints(qualifiedRefTable);
|
|
718
|
+
const expectedName = softDeleteCheckName(refTableName, POSTGRES_MAX_IDENTIFIER_LENGTH);
|
|
719
|
+
return Array.isArray(checks) && checks.some((c) => c && c.name === expectedName);
|
|
720
|
+
} catch (err) {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Derivasi daftar cek FK untuk restore (R20 FK-awareness) dari introspeksi DB. Untuk tiap
|
|
727
|
+
* foreign key tabel yang di-restore, simpan kolom lokal, tabel + kolom parent, dan status
|
|
728
|
+
* parent-soft-delete. Dikonsumsi template (`generateSoftDeleteRestoreOverride`) untuk meng-emit
|
|
729
|
+
* cek parent: parentSoftDelete=true → parent wajib aktif (is_deleted=FALSE, 422 bila tidak);
|
|
730
|
+
* false → cek eksistensi parent saja (422 bila hilang).
|
|
731
|
+
*
|
|
732
|
+
* Sumber FK list = `db.getForeignKeys` (tersedia di payload generate; TIDAK tersedia di jalur
|
|
733
|
+
* model generator yang murni RDF→kode). Hasil di-persist top-level di RDF (`softDeleteFkChecks`),
|
|
734
|
+
* pola yang sama dengan `uniqueConstraints` (sama-sama derived dari introspeksi DB di generate
|
|
735
|
+
* time, dikonsumsi template di codegen). Konservatif: introspeksi gagal / tidak ada FK → [].
|
|
736
|
+
*
|
|
737
|
+
* @param {Object} db - DatabaseIntrospector instance
|
|
738
|
+
* @param {string} tableName - nama tabel yang di-restore
|
|
739
|
+
* @returns {Promise<Array<{columns:string[], refTable:string, refColumns:string[], parentSoftDelete:boolean}>>}
|
|
740
|
+
*/
|
|
741
|
+
async function deriveSoftDeleteFkChecks(db, tableName) {
|
|
742
|
+
if (!db || !db.pool || typeof db.getForeignKeys !== 'function') return [];
|
|
743
|
+
|
|
744
|
+
let foreignKeys;
|
|
745
|
+
try {
|
|
746
|
+
foreignKeys = await db.getForeignKeys(tableName);
|
|
747
|
+
} catch (err) {
|
|
748
|
+
return [];
|
|
749
|
+
}
|
|
750
|
+
if (!Array.isArray(foreignKeys) || foreignKeys.length === 0) return [];
|
|
751
|
+
|
|
752
|
+
const checks = [];
|
|
753
|
+
const parentCache = new Map();
|
|
754
|
+
for (const fk of foreignKeys) {
|
|
755
|
+
const ref = fk && fk.references;
|
|
756
|
+
if (!ref || !ref.table) continue;
|
|
757
|
+
const columns = Array.isArray(fk.columns) ? fk.columns.slice() : [];
|
|
758
|
+
const refColumns = Array.isArray(ref.columns) ? ref.columns.slice() : [];
|
|
759
|
+
if (columns.length === 0 || refColumns.length === 0 || columns.length !== refColumns.length) {
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
const qualifiedRefTable = ref.schema ? `${ref.schema}.${ref.table}` : ref.table;
|
|
763
|
+
|
|
764
|
+
let parentSoftDelete;
|
|
765
|
+
if (parentCache.has(qualifiedRefTable)) {
|
|
766
|
+
parentSoftDelete = parentCache.get(qualifiedRefTable);
|
|
767
|
+
} else {
|
|
768
|
+
parentSoftDelete = await isParentSoftDelete(db, qualifiedRefTable, ref.table);
|
|
769
|
+
parentCache.set(qualifiedRefTable, parentSoftDelete);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
checks.push({ columns, refTable: qualifiedRefTable, refColumns, parentSoftDelete });
|
|
773
|
+
}
|
|
774
|
+
return checks;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// ============================================================================
|
|
778
|
+
// FK-SD-4 REGISTRY DERIVATION (SDF scan -> RDF) — Fase 1.5 FK-aware soft-delete
|
|
779
|
+
// ============================================================================
|
|
780
|
+
//
|
|
781
|
+
// Menurunkan dua field RDF top-level baru saat `payload generate`, fondasi data untuk
|
|
782
|
+
// FK-SD-1 (forward block), FK-SD-2 (restrict), FK-SD-3 (cascade). Phase ini TIDAK
|
|
783
|
+
// mengubah behavior handler; hanya derivasi + persist metadata.
|
|
784
|
+
//
|
|
785
|
+
// Sumber metadata = SDF scan (Map model `loadSchemaPath`), deterministik dan tanpa
|
|
786
|
+
// query DB (keputusan terkunci Pasca-Discovery). Berbeda dengan jalur restore R20
|
|
787
|
+
// (`deriveSoftDeleteFkChecks`, introspeksi DB) yang TIDAK diubah di sini.
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Normalisasi nilai `onDelete` IR (camelCase) ke semantik FK-SD (keputusan terkunci Q5):
|
|
791
|
+
* - 'cascade' -> 'cascade'
|
|
792
|
+
* - 'setNull' -> 'setNull' (pass-through; ditolak validator Phase 05, bukan di sini)
|
|
793
|
+
* - 'restrict' / 'noAction' / absent -> 'restrict' (default blok)
|
|
794
|
+
*
|
|
795
|
+
* Nilai di luar whitelist (`cascade`/`restrict`/`setNull`/`noAction`) sudah ditolak
|
|
796
|
+
* schema-validator (validateRelations), sehingga input ke fungsi ini dijamin salah satu
|
|
797
|
+
* dari empat itu atau absent (undefined/null). Fallback konservatif ke 'restrict' (blok).
|
|
798
|
+
*
|
|
799
|
+
* @param {string|undefined|null} onDelete - nilai onDelete dari IR relasi
|
|
800
|
+
* @returns {'cascade'|'restrict'|'setNull'}
|
|
801
|
+
*/
|
|
802
|
+
function normalizeFkOnDelete(onDelete) {
|
|
803
|
+
if (onDelete === 'cascade') return 'cascade';
|
|
804
|
+
if (onDelete === 'setNull') return 'setNull';
|
|
805
|
+
return 'restrict';
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Muat Map model SDF secara graceful untuk SDF scan FK-SD-4. Berbeda dengan
|
|
810
|
+
* `resolveSoftDeleteForTable` (yang ERROR bila schema-path absen, R12), derivasi registry
|
|
811
|
+
* FK TIDAK boleh menggagalkan generate. Tiga kondisi -> kembalikan null (derivasi kosong):
|
|
812
|
+
* - `--schema-path` absen/kosong (tabel non-soft-delete tak wajib schema-path)
|
|
813
|
+
* - `loadSchemaPath` gagal (folder/SDF invalid)
|
|
814
|
+
* - mode single-file: tetap memuat 1 model, tetapi scan lintas-tabel natural kosong
|
|
815
|
+
* (parent/anak tabel lain tidak ada di Map) -> field dihilangkan secara graceful.
|
|
816
|
+
*
|
|
817
|
+
* @param {string|null|undefined} schemaPath - nilai flag --schema-path
|
|
818
|
+
* @returns {Map<string, Object>|null} Map model SDF, atau null bila tak tersedia
|
|
819
|
+
*/
|
|
820
|
+
function loadSchemaMapGraceful(schemaPath) {
|
|
821
|
+
if (typeof schemaPath !== 'string' || schemaPath.trim() === '') return null;
|
|
822
|
+
try {
|
|
823
|
+
return loadSchemaPath(schemaPath);
|
|
824
|
+
} catch (err) {
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Resolusi IR model parent yang dirujuk sebuah relasi `belongsTo`. `rel.target` konvensinya
|
|
831
|
+
* adalah nama tabel target: `schema introspect` menamai relasi = nama tabel target, dan bila
|
|
832
|
+
* `target` eksplisit absen IR fallback ke nama relasi (`ir-builder.js:198`), sehingga relasi
|
|
833
|
+
* introspeksi tetap menunjuk tabel yang benar. `findModelByTable` menerima nama bare maupun
|
|
834
|
+
* qualified. Konservatif: target tak cocok model mana pun -> null; bare-name ambigu (>1
|
|
835
|
+
* schema, findModelByTable melempar) -> null.
|
|
836
|
+
*
|
|
837
|
+
* @param {Map<string, Object>} models - Map model SDF
|
|
838
|
+
* @param {Object} rel - relasi IR (harus belongsTo)
|
|
839
|
+
* @returns {Object|null} IR model parent, atau null
|
|
840
|
+
*/
|
|
841
|
+
function resolveBelongsToParent(models, rel) {
|
|
842
|
+
if (!rel || rel.type !== 'belongsTo') return null;
|
|
843
|
+
const target = rel.target;
|
|
844
|
+
if (typeof target !== 'string' || target === '') return null;
|
|
845
|
+
try {
|
|
846
|
+
return findModelByTable(models, target);
|
|
847
|
+
} catch (err) {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Komparator determinisme snapshot (independen urutan deklarasi/urutan file loader).
|
|
853
|
+
function compareFkParent(a, b) {
|
|
854
|
+
const ka = `${a.refTable}|${a.columns.join(',')}`;
|
|
855
|
+
const kb = `${b.refTable}|${b.columns.join(',')}`;
|
|
856
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
857
|
+
}
|
|
858
|
+
function compareFkChild(a, b) {
|
|
859
|
+
const ka = `${a.childTable}|${a.childColumns.join(',')}`;
|
|
860
|
+
const kb = `${b.childTable}|${b.childColumns.join(',')}`;
|
|
861
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* FK-SD-4 FORWARD registry (`softDeleteFkParents`). Untuk tabel `tableName`, daftar FK
|
|
866
|
+
* outbound (relasi `belongsTo`) yang parent-nya soft-delete-enabled (SDF `softDelete.enabled
|
|
867
|
+
* === true`). Dikonsumsi FK-SD-1 (forward block create/update anak ke parent terhapus).
|
|
868
|
+
*
|
|
869
|
+
* Berlaku untuk SEMUA tabel, TERMASUK tabel anak yang bukan soft-delete (mis. `visitors`):
|
|
870
|
+
* hanya FK ke parent soft-delete yang masuk. Shape per-entry sengaja subset
|
|
871
|
+
* `softDeleteFkChecks` (tanpa `parentSoftDelete`, karena di sini setiap entry pasti
|
|
872
|
+
* soft-delete) agar emit FK-SD-1 dapat memakai ulang pola SQL parent-aktif restore (R20):
|
|
873
|
+
* { columns: [localKey], refTable: <qualified>, refColumns: [references] }
|
|
874
|
+
*
|
|
875
|
+
* `localKey`/`references` dijamin string tunggal oleh validator (validateRelations
|
|
876
|
+
* mewajibkan keduanya string), sehingga selalu single-column array. Graceful empty bila
|
|
877
|
+
* Map/IR/target tidak tersedia (single-file mode, schema-path absen).
|
|
878
|
+
*
|
|
879
|
+
* @param {Map<string, Object>|null} models - Map model SDF (null -> [])
|
|
880
|
+
* @param {string} tableName - nama tabel (bare atau schema.table)
|
|
881
|
+
* @returns {Array<{columns:string[], refTable:string, refColumns:string[]}>}
|
|
882
|
+
*/
|
|
883
|
+
function deriveSoftDeleteFkParents(models, tableName) {
|
|
884
|
+
if (!models) return [];
|
|
885
|
+
let ir;
|
|
886
|
+
try {
|
|
887
|
+
ir = findModelByTable(models, tableName);
|
|
888
|
+
} catch (err) {
|
|
889
|
+
return [];
|
|
890
|
+
}
|
|
891
|
+
if (!ir || !ir.relations) return [];
|
|
892
|
+
|
|
893
|
+
const parents = [];
|
|
894
|
+
for (const rel of Object.values(ir.relations)) {
|
|
895
|
+
if (!rel || rel.type !== 'belongsTo') continue;
|
|
896
|
+
if (typeof rel.localKey !== 'string' || typeof rel.references !== 'string') continue;
|
|
897
|
+
const parentModel = resolveBelongsToParent(models, rel);
|
|
898
|
+
if (!parentModel || !isSoftDeleteEnabled(parentModel)) continue;
|
|
899
|
+
parents.push({
|
|
900
|
+
columns: [rel.localKey],
|
|
901
|
+
refTable: parentModel.qualifiedName || parentModel.tableName,
|
|
902
|
+
refColumns: [rel.references]
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
parents.sort(compareFkParent);
|
|
906
|
+
return parents;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* FK-SD-4 REVERSE registry (`softDeleteFkChildren`). Untuk tabel master `tableName`, pindai
|
|
911
|
+
* SELURUH model lain di Map: tiap relasi `belongsTo` yang target-nya = master ini berarti
|
|
912
|
+
* tabel pemilik relasi adalah anak. Rekam tabel anak, kolom FK lokal anak, `onDelete`
|
|
913
|
+
* (dinormalisasi Q5), dan status soft-delete anak. Dikonsumsi FK-SD-2 (restrict) & FK-SD-3
|
|
914
|
+
* (cascade).
|
|
915
|
+
*
|
|
916
|
+
* Hanya relasi `belongsTo` yang diperhitungkan: FK fisik selalu berada di tabel anak yang
|
|
917
|
+
* mendeklarasikannya sebagai `belongsTo` (introspeksi DB menghasilkan FK hanya pada child).
|
|
918
|
+
* `hasMany`/`hasOne` adalah view logis inverse dari FK yang sama (kolom-nya bukan FK fisik
|
|
919
|
+
* di tabel pendeklarasi), sehingga TIDAK dipakai agar tidak salah-arah/duplikat. Pencocokan
|
|
920
|
+
* master dilakukan dengan me-resolve target relasi ke model lalu membandingkan qualifiedName
|
|
921
|
+
* (robust terhadap target bare vs qualified). Self-reference (anak == master) sah dan
|
|
922
|
+
* disertakan. Pemanggil HARUS hanya memanggil ini untuk tabel soft-delete (guard
|
|
923
|
+
* hasSoftDeleteColumns); fungsi sendiri tidak menambah guard itu.
|
|
924
|
+
*
|
|
925
|
+
* `parentColumn` (= `rel.references`) membawa kolom MASTER yang dirujuk FK anak. FK-SD-2/3
|
|
926
|
+
* mem-query anak dengan `WHERE <childColumn> = <nilai parentColumn master row>`; mengandalkan
|
|
927
|
+
* PK master salah bila FK merujuk kolom non-PK. Diambil dari relasi, BUKAN diasumsikan = PK.
|
|
928
|
+
* Sejalan forward derivation, `localKey`/`references` wajib string tunggal (composite ditolak
|
|
929
|
+
* validator); entry tanpa keduanya string di-skip (defensif, konsisten determinisme snapshot).
|
|
930
|
+
*
|
|
931
|
+
* @param {Map<string, Object>|null} models - Map model SDF (null -> [])
|
|
932
|
+
* @param {string} tableName - nama tabel master (bare atau schema.table)
|
|
933
|
+
* @returns {Array<{childTable:string, childColumns:string[], parentColumn:string, onDelete:string, childSoftDelete:boolean}>}
|
|
934
|
+
*/
|
|
935
|
+
function deriveSoftDeleteFkChildren(models, tableName) {
|
|
936
|
+
if (!models) return [];
|
|
937
|
+
let masterIr;
|
|
938
|
+
try {
|
|
939
|
+
masterIr = findModelByTable(models, tableName);
|
|
940
|
+
} catch (err) {
|
|
941
|
+
return [];
|
|
942
|
+
}
|
|
943
|
+
if (!masterIr) return [];
|
|
944
|
+
const masterQualified = masterIr.qualifiedName || masterIr.tableName;
|
|
945
|
+
|
|
946
|
+
const children = [];
|
|
947
|
+
for (const childModel of models.values()) {
|
|
948
|
+
if (!childModel || !childModel.relations) continue;
|
|
949
|
+
for (const rel of Object.values(childModel.relations)) {
|
|
950
|
+
if (!rel || rel.type !== 'belongsTo') continue;
|
|
951
|
+
if (typeof rel.localKey !== 'string' || typeof rel.references !== 'string') continue;
|
|
952
|
+
const parentModel = resolveBelongsToParent(models, rel);
|
|
953
|
+
if (!parentModel) continue;
|
|
954
|
+
if ((parentModel.qualifiedName || parentModel.tableName) !== masterQualified) continue;
|
|
955
|
+
children.push({
|
|
956
|
+
childTable: childModel.qualifiedName || childModel.tableName,
|
|
957
|
+
childColumns: [rel.localKey],
|
|
958
|
+
parentColumn: rel.references,
|
|
959
|
+
onDelete: normalizeFkOnDelete(rel.onDelete),
|
|
960
|
+
childSoftDelete: isSoftDeleteEnabled(childModel)
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
children.sort(compareFkChild);
|
|
965
|
+
return children;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* FK-SD-3 (Fase 1.5) — derivasi closure cascade transitif untuk master soft-delete.
|
|
970
|
+
* Mengadopsi algoritma spike Phase 04s (`buildCascadeClosure`, terbukti deterministik +
|
|
971
|
+
* cycle-safe) menjadi fungsi produksi. Dari `masterTable`, hitung peta node (master +
|
|
972
|
+
* SELURUH keturunan yang reachable lewat edge `onDelete:cascade` ke anak soft-delete),
|
|
973
|
+
* masing-masing membawa metadata yang dibutuhkan walk runtime (Phase 04b) untuk
|
|
974
|
+
* men-soft-delete dirinya + menjalar lebih dalam. Closure dipersist PENUH di RDF master
|
|
975
|
+
* (keputusan orchestrator) agar template Phase 04b tetap Map-free: tidak ada lookup
|
|
976
|
+
* lintas-RDF/akses DB saat codegen.
|
|
977
|
+
*
|
|
978
|
+
* Tiap node menyimpan:
|
|
979
|
+
* - table : nama qualified node
|
|
980
|
+
* - pk : primary key node (string bila single-column, array bila composite)
|
|
981
|
+
* dari IR `primaryKey`
|
|
982
|
+
* - reusable : [{field, length}] dari IR `softDelete.reusable` node ([] bila tidak ada)
|
|
983
|
+
* - children : SELURUH anak langsung (cascade + restrict + setNull), tiap entry
|
|
984
|
+
* { childTable, childColumn, parentColumn, onDelete, childSoftDelete }.
|
|
985
|
+
* Anak restrict/setNull DIREKAM (agar restrict-per-node Phase 04c bisa
|
|
986
|
+
* cek di SETIAP node, bukan hanya master) tetapi TIDAK memicu rekursi.
|
|
987
|
+
*
|
|
988
|
+
* Aturan expand (rekursi): HANYA ke `childTable` dari edge `onDelete==='cascade'` yang
|
|
989
|
+
* `childSoftDelete===true`. Edge cascade ke anak NON-soft-delete direkam di `children`
|
|
990
|
+
* (`childSoftDelete:false`) tetapi TIDAK di-expand dan TIDAK error di sini (konfigurasi
|
|
991
|
+
* itu ditolak validator Phase 05). Edge restrict/setNull direkam tetapi tidak di-expand.
|
|
992
|
+
*
|
|
993
|
+
* Cycle-safe (level closure): `visited` set berisi qualified table name; tabel yang sudah
|
|
994
|
+
* masuk `nodes` tidak di-expand ulang, sehingga closure pasti terminasi pada graf
|
|
995
|
+
* ber-cycle (self-reference, A→B→A) maupun diamond (anak dengan ≥2 parent cascade diproses
|
|
996
|
+
* sekali). `parentColumn = rel.references` (BUKAN diasumsikan PK), konsisten Phase 03.
|
|
997
|
+
*
|
|
998
|
+
* `order` = urutan kunjungan node DFS pre-order dengan anak cascade dikunjungi menurut
|
|
999
|
+
* urutan sorted `deriveSoftDeleteFkChildren` (master lebih dulu, lalu descendant). Stabil
|
|
1000
|
+
* dan deterministik untuk snapshot RDF.
|
|
1001
|
+
*
|
|
1002
|
+
* Graceful: Map null/parsial (single-file, `--schema-path` absen) → `deriveSoftDeleteFkChildren`
|
|
1003
|
+
* mengembalikan [] sehingga tree berisi hanya node master tanpa children, konsisten Phase 01.
|
|
1004
|
+
* (Pemanggil men-guard persist dengan ≥1 anak cascade, sehingga kasus graceful itu tidak
|
|
1005
|
+
* mempersist field sama sekali.)
|
|
1006
|
+
*
|
|
1007
|
+
* @param {Map<string, Object>|null} models - Map model SDF
|
|
1008
|
+
* @param {string} masterTable - nama tabel master (bare atau schema.table)
|
|
1009
|
+
* @returns {{ master:string, order:string[], nodes: Object<string, {table:string, pk:(string|string[]), reusable:Array<{field:string,length:number}>, children:Array<{childTable:string,childColumn:string,parentColumn:string,onDelete:string,childSoftDelete:boolean}>}> }}
|
|
1010
|
+
*/
|
|
1011
|
+
function buildSoftDeleteCascadeTree(models, masterTable) {
|
|
1012
|
+
let masterIr = null;
|
|
1013
|
+
if (models) {
|
|
1014
|
+
try {
|
|
1015
|
+
masterIr = findModelByTable(models, masterTable);
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
masterIr = null;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
// Map tak tersedia / master tak ada di Map (single-file, schema-path absen): pakai nama
|
|
1021
|
+
// tabel apa adanya. directChildren akan kosong (deriveSoftDeleteFkChildren graceful),
|
|
1022
|
+
// menghasilkan tree minimal (hanya master, tanpa expand). Konsisten Phase 01.
|
|
1023
|
+
const masterQ = masterIr
|
|
1024
|
+
? (masterIr.qualifiedName || masterIr.tableName)
|
|
1025
|
+
: masterTable;
|
|
1026
|
+
|
|
1027
|
+
const nodes = {};
|
|
1028
|
+
const order = [];
|
|
1029
|
+
const visited = new Set();
|
|
1030
|
+
const stack = [masterQ];
|
|
1031
|
+
|
|
1032
|
+
while (stack.length) {
|
|
1033
|
+
const tableQ = stack.pop();
|
|
1034
|
+
if (visited.has(tableQ)) continue; // cycle/diamond guard: tabel sudah diproses
|
|
1035
|
+
visited.add(tableQ);
|
|
1036
|
+
|
|
1037
|
+
const ir = models ? models.get(tableQ) : null;
|
|
1038
|
+
const pkArr = (ir && Array.isArray(ir.primaryKey)) ? ir.primaryKey : [];
|
|
1039
|
+
const reusable = (ir && ir.softDelete && Array.isArray(ir.softDelete.reusable))
|
|
1040
|
+
? ir.softDelete.reusable.map((r) => ({ field: r.field, length: r.length }))
|
|
1041
|
+
: [];
|
|
1042
|
+
|
|
1043
|
+
// Anak langsung — derivasi REVERSE registry ASLI (Phase 01/03), sudah membawa
|
|
1044
|
+
// childColumns/parentColumn/onDelete/childSoftDelete dan tersortir deterministik.
|
|
1045
|
+
const directChildren = deriveSoftDeleteFkChildren(models, tableQ);
|
|
1046
|
+
|
|
1047
|
+
nodes[tableQ] = {
|
|
1048
|
+
table: tableQ,
|
|
1049
|
+
pk: pkArr.length === 1 ? pkArr[0] : pkArr.slice(),
|
|
1050
|
+
reusable,
|
|
1051
|
+
children: directChildren.map((c) => ({
|
|
1052
|
+
childTable: c.childTable,
|
|
1053
|
+
childColumn: c.childColumns[0],
|
|
1054
|
+
parentColumn: c.parentColumn,
|
|
1055
|
+
onDelete: c.onDelete,
|
|
1056
|
+
childSoftDelete: c.childSoftDelete
|
|
1057
|
+
}))
|
|
1058
|
+
};
|
|
1059
|
+
order.push(tableQ);
|
|
1060
|
+
|
|
1061
|
+
// Expand HANYA edge cascade ke anak soft-delete-enabled. Push REVERSE agar pop dalam
|
|
1062
|
+
// urutan sorted (DFS pre-order stabil). Cascade→non-SD & restrict/setNull TIDAK di-expand.
|
|
1063
|
+
const toExpand = [];
|
|
1064
|
+
for (const c of directChildren) {
|
|
1065
|
+
if (c.onDelete !== 'cascade' || !c.childSoftDelete) continue;
|
|
1066
|
+
if (visited.has(c.childTable)) continue;
|
|
1067
|
+
toExpand.push(c.childTable);
|
|
1068
|
+
}
|
|
1069
|
+
for (let i = toExpand.length - 1; i >= 0; i--) stack.push(toExpand[i]);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
return { master: masterQ, order, nodes };
|
|
1073
|
+
}
|
|
1074
|
+
|
|
490
1075
|
// ============================================================================
|
|
491
1076
|
// ADVISORIES (informational) — untuk payload validate
|
|
492
1077
|
// ============================================================================
|
|
@@ -1115,6 +1700,13 @@ class PayloadGenerator {
|
|
|
1115
1700
|
console.log(`Table: ${payloadData.tableName}`);
|
|
1116
1701
|
console.log(`Primary Key: ${payloadData.primaryKey}`);
|
|
1117
1702
|
|
|
1703
|
+
// Flag kehadiran kolom soft-delete di DB (R12). DITANGKAP dari daftar kolom DB
|
|
1704
|
+
// SEBELUM strip fieldName, supaya resolveSoftDeleteForTable/derivation (~1786)
|
|
1705
|
+
// tetap jalan dan tetap ERROR bila kolom hadir di DB tetapi SDF tidak punya blok
|
|
1706
|
+
// softDelete valid. Strip middle-ground beroperasi pada PROYEKSI fieldName, bukan
|
|
1707
|
+
// pada daftar kolom DB yang dipakai cek konsistensi.
|
|
1708
|
+
let hasSoftDeleteColumns = false;
|
|
1709
|
+
|
|
1118
1710
|
// Get fields from database
|
|
1119
1711
|
if (this.db.pool) {
|
|
1120
1712
|
const columns = await this.db.getColumns(args.table);
|
|
@@ -1130,6 +1722,22 @@ class PayloadGenerator {
|
|
|
1130
1722
|
payloadData.auditColumns = false;
|
|
1131
1723
|
console.log('No audit columns detected in table: setting auditColumns: false');
|
|
1132
1724
|
}
|
|
1725
|
+
|
|
1726
|
+
// Soft-delete middle-ground (audit-parity): keluarkan tiga kolom soft-delete
|
|
1727
|
+
// (is_deleted/deleted_at/deleted_by) dari PROYEKSI fieldName agar RDF bersih.
|
|
1728
|
+
// Seluruh turunan (datatablesQuery SQL, datatablesWhere, dateTimeFields,
|
|
1729
|
+
// fieldValidation) diturunkan dari fieldName SETELAH titik ini sehingga ikut
|
|
1730
|
+
// bersih otomatis. validFields MODEL tetap memuat tiga kolom (di-append di
|
|
1731
|
+
// template) supaya mekanisme visibility R15 tetap lolos guard BaseModel.
|
|
1732
|
+
// Flag ditangkap dari kolom DB (bukan fieldName yang sudah di-strip).
|
|
1733
|
+
hasSoftDeleteColumns = SOFT_DELETE_COLUMNS.some(col => columns.includes(col));
|
|
1734
|
+
if (hasSoftDeleteColumns) {
|
|
1735
|
+
const softDeleteStripped = payloadData.fieldName.filter(col => SOFT_DELETE_COLUMNS.includes(col));
|
|
1736
|
+
payloadData.fieldName = payloadData.fieldName.filter(col => !SOFT_DELETE_COLUMNS.includes(col));
|
|
1737
|
+
if (softDeleteStripped.length > 0) {
|
|
1738
|
+
console.log(`Soft-delete columns excluded from RDF fieldName: ${softDeleteStripped.join(', ')}`);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1133
1741
|
}
|
|
1134
1742
|
}
|
|
1135
1743
|
|
|
@@ -1222,6 +1830,78 @@ class PayloadGenerator {
|
|
|
1222
1830
|
.map((c) => ({ name: c.name, fields: c.columns }));
|
|
1223
1831
|
}
|
|
1224
1832
|
|
|
1833
|
+
// Soft-delete derivation (R12/R13): bila tabel memiliki satu atau lebih kolom
|
|
1834
|
+
// soft-delete (is_deleted/deleted_at/deleted_by), wajib resolusi SDF dengan blok
|
|
1835
|
+
// softDelete valid lalu turunkan blok ke RDF (+ override maxLength reusable = base).
|
|
1836
|
+
// Guard ketat: tabel non-soft-delete tidak menyentuh jalur ini sama sekali, sehingga
|
|
1837
|
+
// output byte-identik baseline (--schema-path diabaikan untuk tabel non-soft-delete).
|
|
1838
|
+
// CATATAN: `hasSoftDeleteColumns` di-capture dari daftar kolom DB SEBELUM strip
|
|
1839
|
+
// fieldName (lihat ~1662), karena fieldName kini sudah tidak memuat tiga kolom.
|
|
1840
|
+
|
|
1841
|
+
// FK-SD-4 (Fase 1.5): muat Map SDF SEKALI secara graceful untuk derivasi registry FK
|
|
1842
|
+
// (forward `softDeleteFkParents` + reverse `softDeleteFkChildren`). Berbeda dengan
|
|
1843
|
+
// resolveSoftDeleteForTable yang ERROR bila schema-path absen (R12), registry FK tidak
|
|
1844
|
+
// boleh menggagalkan generate: Map null -> derivasi kosong (field dihilangkan secara
|
|
1845
|
+
// graceful). Tidak menambah query DB saat runtime (derivasi hanya saat generate).
|
|
1846
|
+
const fkRegistryModels = loadSchemaMapGraceful(args['schema-path']);
|
|
1847
|
+
|
|
1848
|
+
if (hasSoftDeleteColumns) {
|
|
1849
|
+
const sdfSoftDelete = resolveSoftDeleteForTable(payloadData.tableName, args['schema-path']);
|
|
1850
|
+
applySoftDeleteDerivation(payloadData, sdfSoftDelete);
|
|
1851
|
+
const reusableCount = Array.isArray(sdfSoftDelete.reusable)
|
|
1852
|
+
? sdfSoftDelete.reusable.length
|
|
1853
|
+
: 0;
|
|
1854
|
+
|
|
1855
|
+
// FK-awareness (R20): derive daftar cek FK + status parent-soft-delete dari introspeksi
|
|
1856
|
+
// DB. Top-level (pola uniqueConstraints), guarded oleh hasSoftDeleteColumns → tabel
|
|
1857
|
+
// non-soft-delete tidak menyentuh field ini (diff-zero baseline terjaga). Dikonsumsi
|
|
1858
|
+
// template restore (R20) saat action.restore aktif. TIDAK diubah Fase 1.5.
|
|
1859
|
+
payloadData.softDeleteFkChecks = await deriveSoftDeleteFkChecks(this.db, payloadData.tableName);
|
|
1860
|
+
const softDeleteParentCount = payloadData.softDeleteFkChecks.filter((c) => c.parentSoftDelete).length;
|
|
1861
|
+
|
|
1862
|
+
// FK-SD-4 REVERSE registry (Fase 1.5): anak yang menunjuk master ini, untuk FK-SD-2
|
|
1863
|
+
// (restrict) & FK-SD-3 (cascade). Persist SELALU untuk tabel soft-delete (boleh []),
|
|
1864
|
+
// mengikuti preseden softDeleteFkChecks. Diturunkan via SDF scan (Map), bukan DB.
|
|
1865
|
+
payloadData.softDeleteFkChildren = deriveSoftDeleteFkChildren(fkRegistryModels, payloadData.tableName);
|
|
1866
|
+
|
|
1867
|
+
// FK-SD-3 (Fase 1.5, Phase 04a/04b): closure cascade transitif (master + seluruh keturunan
|
|
1868
|
+
// reachable lewat edge cascade ke anak soft-delete). Persist PENUH di RDF master agar
|
|
1869
|
+
// walk Phase 04b Map-free. Guard: HANYA bila master punya ≥1 anak langsung
|
|
1870
|
+
// onDelete:cascade YANG anaknya soft-delete-enabled (childSoftDelete === true) — sebab
|
|
1871
|
+
// softDeleteCascadeTree hanya bermakna bila ada cascade NYATA yang bisa di-walk. Edge
|
|
1872
|
+
// cascade→non-soft-delete adalah config invalid (ditolak validator Phase 05) → tidak
|
|
1873
|
+
// mempersist tree degenerate. Master tanpa cascade expandable → field DIHILANGKAN
|
|
1874
|
+
// (diff-zero baseline + komposisi Phase 03 restrict-only tetap utuh). Penempatan tepat
|
|
1875
|
+
// setelah softDeleteFkChildren → urutan key RDF deterministik.
|
|
1876
|
+
const hasCascadeChild = payloadData.softDeleteFkChildren.some(
|
|
1877
|
+
(c) => c.onDelete === 'cascade' && c.childSoftDelete === true
|
|
1878
|
+
);
|
|
1879
|
+
let cascadeNodeCount = 0;
|
|
1880
|
+
if (hasCascadeChild) {
|
|
1881
|
+
payloadData.softDeleteCascadeTree =
|
|
1882
|
+
buildSoftDeleteCascadeTree(fkRegistryModels, payloadData.tableName);
|
|
1883
|
+
cascadeNodeCount = payloadData.softDeleteCascadeTree.order.length;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
console.log(
|
|
1887
|
+
`Soft-delete detected: derived softDelete block from SDF ` +
|
|
1888
|
+
`('${args['schema-path']}', ${reusableCount} reusable field(s); ` +
|
|
1889
|
+
`${payloadData.softDeleteFkChecks.length} FK check(s), ${softDeleteParentCount} soft-delete parent(s); ` +
|
|
1890
|
+
`${payloadData.softDeleteFkChildren.length} reverse child(ren)` +
|
|
1891
|
+
(hasCascadeChild ? `; cascade closure ${cascadeNodeCount} node(s)` : '') + `).`
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// FK-SD-4 FORWARD registry (Fase 1.5): FK outbound tabel ini ke parent soft-delete, untuk
|
|
1896
|
+
// FK-SD-1 (forward block create/update). Berlaku untuk SEMUA tabel TERMASUK anak
|
|
1897
|
+
// non-soft-delete (mis. visitors), sehingga jalur ini di LUAR guard hasSoftDeleteColumns.
|
|
1898
|
+
// Persist HANYA bila array non-kosong → tabel netral (tanpa softDelete & tanpa FK ke
|
|
1899
|
+
// tabel soft-delete) tetap byte-identik baseline (diff-zero terjaga).
|
|
1900
|
+
const softDeleteFkParents = deriveSoftDeleteFkParents(fkRegistryModels, payloadData.tableName);
|
|
1901
|
+
if (softDeleteFkParents.length > 0) {
|
|
1902
|
+
payloadData.softDeleteFkParents = softDeleteFkParents;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1225
1905
|
// Save payload
|
|
1226
1906
|
this.ensureOutputDir();
|
|
1227
1907
|
const filename = baseFilename + '.json';
|
|
@@ -1239,6 +1919,15 @@ class PayloadGenerator {
|
|
|
1239
1919
|
console.log();
|
|
1240
1920
|
console.log(`Payload saved: ${outputPath}`);
|
|
1241
1921
|
console.log(`Query saved: ${sqlOutputPath}`);
|
|
1922
|
+
|
|
1923
|
+
// Checkpoint 2 soft-delete dashboard guard (WARNING, non-blocking): bila RDF yang baru
|
|
1924
|
+
// ditulis adalah tabel soft-delete, scan dashboard yang sudah ada di outputDir terhadap
|
|
1925
|
+
// registry soft-delete saat ini lalu peringatkan dashboard yang bocor. Gating
|
|
1926
|
+
// isSoftDeleteEnabled memastikan tabel netral tidak memicu scan (no noise, no regresi).
|
|
1927
|
+
// Tidak boleh menggagalkan generate (fungsi warn bersifat best-effort, tidak throw).
|
|
1928
|
+
if (isSoftDeleteEnabled(payloadData)) {
|
|
1929
|
+
warnSoftDeleteDashboardDrift(this.outputDir);
|
|
1930
|
+
}
|
|
1242
1931
|
}
|
|
1243
1932
|
}
|
|
1244
1933
|
|
|
@@ -1863,6 +2552,14 @@ class SchemaValidator {
|
|
|
1863
2552
|
enrichedColumns = tempGenerator.enrichDetailedColumnsWithBoolean(enrichedColumns, booleanColumns);
|
|
1864
2553
|
const stringColumns = collectStringColumns(enrichedColumns);
|
|
1865
2554
|
|
|
2555
|
+
// Soft-delete middle-ground (audit-parity): bila blok softDelete dipertahankan di
|
|
2556
|
+
// RDF (lewat spread ...oldPayload), keluarkan tiga kolom soft-delete dari fieldName
|
|
2557
|
+
// hasil sync sama seperti jalur generate. Gate dari oldPayload (blok softDelete tidak
|
|
2558
|
+
// berubah saat sync). Tabel non-soft-delete: gate false → newFieldName byte-identik
|
|
2559
|
+
// baseline. Strip di KEDUA loop agar RDF lama (yang masih memuat tiga kolom di
|
|
2560
|
+
// fieldName) ikut dibersihkan secara idempoten saat sync.
|
|
2561
|
+
const stripSoftDelete = isSoftDeleteEnabled(oldPayload);
|
|
2562
|
+
|
|
1866
2563
|
// Bangun fieldName baru: pertahankan urutan field lama, tambahkan field baru di akhir.
|
|
1867
2564
|
// Kolom yang sebelumnya dicantumkan user di payload (termasuk kolom audit default)
|
|
1868
2565
|
// dipertahankan. Kolom audit default yang tidak pernah ada di payload tidak
|
|
@@ -1870,6 +2567,7 @@ class SchemaValidator {
|
|
|
1870
2567
|
const newFieldName = [];
|
|
1871
2568
|
// Pertahankan field lama yang masih ada di database
|
|
1872
2569
|
for (const field of oldPayload.fieldName) {
|
|
2570
|
+
if (stripSoftDelete && SOFT_DELETE_COLUMNS.includes(field)) continue;
|
|
1873
2571
|
if (dbColumns.includes(field)) {
|
|
1874
2572
|
newFieldName.push(field);
|
|
1875
2573
|
}
|
|
@@ -1878,6 +2576,7 @@ class SchemaValidator {
|
|
|
1878
2576
|
for (const col of dbColumns) {
|
|
1879
2577
|
if (newFieldName.includes(col)) continue;
|
|
1880
2578
|
if (DEFAULT_AUDIT_COLUMNS.includes(col)) continue;
|
|
2579
|
+
if (stripSoftDelete && SOFT_DELETE_COLUMNS.includes(col)) continue;
|
|
1881
2580
|
newFieldName.push(col);
|
|
1882
2581
|
}
|
|
1883
2582
|
|
|
@@ -2056,5 +2755,16 @@ module.exports = {
|
|
|
2056
2755
|
collectStringColumns,
|
|
2057
2756
|
isDefaultSearchableColumn,
|
|
2058
2757
|
applyIsActiveDefaultScope,
|
|
2059
|
-
|
|
2758
|
+
applySoftDeleteDerivation,
|
|
2759
|
+
resolveSoftDeleteForTable,
|
|
2760
|
+
deriveSoftDeleteFkChecks,
|
|
2761
|
+
isParentSoftDelete,
|
|
2762
|
+
findModelByTable,
|
|
2763
|
+
loadSchemaMapGraceful,
|
|
2764
|
+
normalizeFkOnDelete,
|
|
2765
|
+
deriveSoftDeleteFkParents,
|
|
2766
|
+
deriveSoftDeleteFkChildren,
|
|
2767
|
+
buildSoftDeleteCascadeTree,
|
|
2768
|
+
buildPayloadAdvisories,
|
|
2769
|
+
warnSoftDeleteDashboardDrift
|
|
2060
2770
|
};
|