@restforgejs/platform 5.1.6 → 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/fast-track.js +63 -43
- 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
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const ConfigReader = require('../config/config-reader');
|
|
4
|
+
const {
|
|
5
|
+
SOFT_DELETE_COLUMNS,
|
|
6
|
+
isSoftDeleteEnabled
|
|
7
|
+
} = require('../dbschema-kit/soft-delete-constants');
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Validator untuk payload files - fokus HANYA pada validasi
|
|
@@ -270,6 +274,14 @@ class PayloadValidator {
|
|
|
270
274
|
if (payload.fieldPolicy) {
|
|
271
275
|
this.validateFieldPolicy(payload.fieldPolicy, payload.fieldName, fileName);
|
|
272
276
|
}
|
|
277
|
+
|
|
278
|
+
// Soft-delete (R17): validasi blok `softDelete` (shape, scope guard Fase 1,
|
|
279
|
+
// konsistensi reusable, action.restore gate) PLUS guard kolom-tanpa-blok (Q3).
|
|
280
|
+
// Dipanggil TANPA syarat (bukan `if (payload.softDelete)`): guard kolom-tanpa-blok
|
|
281
|
+
// dan restore gate justru menangani kasus blok ABSEN (payload hasil `sync`/edit manual
|
|
282
|
+
// yang menjatuhkan blok), sehingga menggantungnya pada keberadaan blok akan membuat
|
|
283
|
+
// guard itu dead code. validateSoftDelete early-return untuk payload non-soft-delete.
|
|
284
|
+
this.validateSoftDelete(payload, fileName);
|
|
273
285
|
}
|
|
274
286
|
|
|
275
287
|
/**
|
|
@@ -486,6 +498,421 @@ class PayloadValidator {
|
|
|
486
498
|
}
|
|
487
499
|
}
|
|
488
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Validasi blok `softDelete` di RDF (R17) — gate kedua (defense in depth) setelah
|
|
503
|
+
* `payload generate` menurunkan blok dari SDF (R12). Berguna terutama bila payload
|
|
504
|
+
* di-edit manual atau di-`sync` (sync TIDAK menurunkan blok; lihat guard kolom-tanpa-blok).
|
|
505
|
+
*
|
|
506
|
+
* Dipanggil TANPA syarat dari validatePayloadStructure (bukan `if (payload.softDelete)`):
|
|
507
|
+
* dua guard di bawah (kolom-tanpa-blok & action.restore) justru menangani kasus blok ABSEN,
|
|
508
|
+
* sehingga menggantungnya pada keberadaan blok akan membuat guard itu tidak pernah jalan.
|
|
509
|
+
* Untuk payload non-soft-delete (tanpa kolom soft-delete, tanpa blok, tanpa action.restore)
|
|
510
|
+
* method langsung return — tidak ada efek samping (backward-compat byte-identik).
|
|
511
|
+
*
|
|
512
|
+
* Cakupan (R17 + Q3 + R20):
|
|
513
|
+
* - Shape: `enabled` boolean; `reusable` array of { field:string, length:int>0 };
|
|
514
|
+
* `visibility` opsional ∈ {active_only, deleted_only, include_deleted}.
|
|
515
|
+
* - Konsistensi reusable: field ada di `fieldName`; unique
|
|
516
|
+
* (`fieldValidation[field].constraints.unique === true`); `length` === maxLength field (R13).
|
|
517
|
+
* - Scope guard Fase 1 (ERROR "not supported in Phase 1"): `purge` di blok; `restoreEndpoint`
|
|
518
|
+
* di blok (sisa arsitektur lama, restore via `action.restore` R20); `masterDetail`
|
|
519
|
+
* enabled bersama `softDelete.enabled`.
|
|
520
|
+
* - Guard kolom-tanpa-blok (Q3): kolom soft-delete ada di `fieldName` tetapi blok absen /
|
|
521
|
+
* `enabled !== true` → ERROR.
|
|
522
|
+
* - Restore gate (R20): `action.restore === true` mewajibkan `softDelete.enabled === true`.
|
|
523
|
+
* - Gate FK-aware Fase 1.5 (G1-G5, via `validateSoftDeleteFk`): tolak cascade→anak non-soft-delete
|
|
524
|
+
* (G1), `onDelete:setNull` (G2, deferral), node cascade composite-PK (G3), plus shape defensif
|
|
525
|
+
* `softDeleteFkChildren`/`softDeleteCascadeTree` (G4) dan `softDeleteFkParents` (G5).
|
|
526
|
+
*
|
|
527
|
+
* Validasi SDF (R3/R5/R7/R9 — lapis schema) TIDAK diduplikasi di sini; itu ditegakkan
|
|
528
|
+
* `validateSchema`/`validateSoftDelete` SDF saat `schema validate`/`migrate`/`diff`/`apply`.
|
|
529
|
+
*
|
|
530
|
+
* @param {Object} payload - Payload utuh (butuh softDelete, fieldName, fieldValidation, action, masterDetail)
|
|
531
|
+
* @param {string} fileName - Nama file untuk error message
|
|
532
|
+
* @throws {Error} Jika blok softDelete / guard scope / gate dilanggar
|
|
533
|
+
*/
|
|
534
|
+
static validateSoftDelete(payload, fileName) {
|
|
535
|
+
const fieldNames = Array.isArray(payload.fieldName) ? payload.fieldName : [];
|
|
536
|
+
const softDelete = payload.softDelete;
|
|
537
|
+
const hasBlock = softDelete !== undefined && softDelete !== null;
|
|
538
|
+
const presentColumns = SOFT_DELETE_COLUMNS.filter((col) => fieldNames.includes(col));
|
|
539
|
+
const hasSoftDeleteColumns = presentColumns.length > 0;
|
|
540
|
+
const restoreRequested = !!(payload.action && payload.action.restore === true);
|
|
541
|
+
|
|
542
|
+
// FK-aware metadata (Fase 1.5). `softDeleteFkChildren`/`softDeleteCascadeTree` dipersist hanya
|
|
543
|
+
// untuk tabel soft-delete (di-guard hasSoftDeleteColumns di payload-runner); `softDeleteFkParents`
|
|
544
|
+
// (forward registry FK-SD-1) dipersist di LUAR guard itu, sehingga muncul juga pada tabel anak
|
|
545
|
+
// NON-soft-delete (mis. `visitors`: tanpa blok/kolom soft-delete). Ketiganya disertakan di
|
|
546
|
+
// early-return agar payload yang membawa metadata FK TANPA blok/kolom tetap mencapai gate FK di
|
|
547
|
+
// bawah (validateSoftDeleteFk) — termasuk shape guard `softDeleteFkParents` (G5) — alih-alih
|
|
548
|
+
// lolos diam-diam.
|
|
549
|
+
const hasFkMetadata =
|
|
550
|
+
payload.softDeleteFkParents !== undefined ||
|
|
551
|
+
payload.softDeleteFkChildren !== undefined ||
|
|
552
|
+
payload.softDeleteCascadeTree !== undefined;
|
|
553
|
+
|
|
554
|
+
// Backward-compat: payload tanpa blok, tanpa kolom soft-delete, tanpa action.restore,
|
|
555
|
+
// tanpa metadata FK tidak terpengaruh sama sekali (lolos seperti biasa).
|
|
556
|
+
if (!hasBlock && !hasSoftDeleteColumns && !restoreRequested && !hasFkMetadata) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// --- Shape + konsistensi blok (bila blok ada) ---
|
|
561
|
+
// Divalidasi LEBIH DULU sehingga blok yang malformed (mis. `enabled` bukan boolean)
|
|
562
|
+
// memberi error shape yang presisi, bukan tertangkap guard kolom-tanpa-blok di bawah
|
|
563
|
+
// (yang pesannya menyesatkan untuk blok yang sebenarnya ADA tetapi salah bentuk).
|
|
564
|
+
if (hasBlock) {
|
|
565
|
+
if (typeof softDelete !== 'object' || Array.isArray(softDelete)) {
|
|
566
|
+
throw new Error(`softDelete in ${fileName} must be a non-null object`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Scope guard Fase 1: purge ditolak (operasi destruktif permanen, fase lanjut).
|
|
570
|
+
// Dicek lebih dulu, lepas dari nilai `enabled` (purge di luar scope total).
|
|
571
|
+
if (softDelete.purge !== undefined) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
`softDelete.purge in ${fileName} is not supported in Phase 1: purge is a destructive, ` +
|
|
574
|
+
`permanent operation deferred to a later phase. Remove the purge key from the softDelete block.`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Scope guard Fase 1: restoreEndpoint ditolak (sisa arsitektur lama). Restore di Fase 1
|
|
579
|
+
// diaktifkan via `action.restore` (R20), bukan key `restoreEndpoint` di blok softDelete.
|
|
580
|
+
// Dicek berdampingan dengan purge, lepas dari nilai `enabled`.
|
|
581
|
+
if (softDelete.restoreEndpoint !== undefined) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
`softDelete.restoreEndpoint in ${fileName} is not supported in Phase 1: it is a remnant of the ` +
|
|
584
|
+
`old architecture. Enable the restore endpoint via action.restore = true instead (R20), then ` +
|
|
585
|
+
`remove the restoreEndpoint key from the softDelete block.`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// enabled: harus boolean (R17).
|
|
590
|
+
if (typeof softDelete.enabled !== 'boolean') {
|
|
591
|
+
throw new Error(`softDelete.enabled in ${fileName} is required and must be a boolean`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// visibility: opsional (default active_only); harus salah satu enum (R13/R15).
|
|
595
|
+
if (softDelete.visibility !== undefined) {
|
|
596
|
+
const VALID_VISIBILITY = ['active_only', 'deleted_only', 'include_deleted'];
|
|
597
|
+
if (!VALID_VISIBILITY.includes(softDelete.visibility)) {
|
|
598
|
+
throw new Error(
|
|
599
|
+
`softDelete.visibility '${softDelete.visibility}' in ${fileName} is invalid. ` +
|
|
600
|
+
`Valid values: ${VALID_VISIBILITY.join(', ')} (default: active_only).`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// reusable: opsional. Bila ada, harus array of { field:string, length:int>0 } dengan
|
|
606
|
+
// konsistensi unique + base length (R13). Blok bisa { enabled: true } tanpa reusable.
|
|
607
|
+
if (softDelete.reusable !== undefined) {
|
|
608
|
+
if (!Array.isArray(softDelete.reusable)) {
|
|
609
|
+
throw new Error(`softDelete.reusable in ${fileName} must be an array of { field, length }`);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
for (let i = 0; i < softDelete.reusable.length; i++) {
|
|
613
|
+
const entry = softDelete.reusable[i];
|
|
614
|
+
|
|
615
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
616
|
+
throw new Error(`softDelete.reusable[${i}] in ${fileName} must be an object { field, length }`);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (typeof entry.field !== 'string' || entry.field.trim() === '') {
|
|
620
|
+
throw new Error(
|
|
621
|
+
`softDelete.reusable[${i}].field in ${fileName} is required and must be a non-empty string`
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (!Number.isInteger(entry.length) || entry.length <= 0) {
|
|
626
|
+
throw new Error(
|
|
627
|
+
`softDelete.reusable[${i}].length (field '${entry.field}') in ${fileName} must be an integer greater than 0`
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// field harus ada di fieldName.
|
|
632
|
+
if (!fieldNames.includes(entry.field)) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
`softDelete.reusable references field '${entry.field}' in ${fileName} which is not in fieldName`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// field harus unique: fieldValidation[field].constraints.unique === true.
|
|
639
|
+
const fv = Array.isArray(payload.fieldValidation)
|
|
640
|
+
? payload.fieldValidation.find((f) => f && f.name === entry.field)
|
|
641
|
+
: undefined;
|
|
642
|
+
const constraints = (fv && fv.constraints) || {};
|
|
643
|
+
|
|
644
|
+
if (constraints.unique !== true) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
`softDelete.reusable field '${entry.field}' in ${fileName} must be unique ` +
|
|
647
|
+
`(fieldValidation['${entry.field}'].constraints.unique must be true)`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Konsistensi base length (R13): reusable.length === fieldValidation[field].constraints.maxLength.
|
|
652
|
+
// Menutup kasus edit manual yang membuat keduanya tidak lagi cocok (mis. maxLength diubah).
|
|
653
|
+
if (constraints.maxLength !== entry.length) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
`softDelete.reusable field '${entry.field}' length (${entry.length}) in ${fileName} must equal ` +
|
|
656
|
+
`fieldValidation['${entry.field}'].constraints.maxLength (${constraints.maxLength}); ` +
|
|
657
|
+
`the base length and the field maxLength must be identical (R13).`
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// `enabled === true` dipakai konsisten untuk guard cross-cutting di bawah. isSoftDeleteEnabled
|
|
665
|
+
// memeriksa payload.softDelete.enabled === true (false bila blok absen / enabled bukan true).
|
|
666
|
+
// Bila blok ada, shape di atas sudah memastikan `enabled` benar-benar boolean.
|
|
667
|
+
const blockEnabled = isSoftDeleteEnabled(payload);
|
|
668
|
+
|
|
669
|
+
// --- Guard kolom-tanpa-blok (Q3, defense in depth) ---
|
|
670
|
+
// Kolom soft-delete ada di fieldName tetapi blok absen atau enabled !== true. Menangkap
|
|
671
|
+
// payload hasil `sync`/edit manual yang menjatuhkan blok sehingga tabel soft-delete
|
|
672
|
+
// diperlakukan sebagai tabel normal (/delete jadi hard delete, kolom soft-delete writable).
|
|
673
|
+
if (hasSoftDeleteColumns && !blockEnabled) {
|
|
674
|
+
throw new Error(
|
|
675
|
+
`Payload ${fileName} has soft-delete column(s) [${presentColumns.join(', ')}] in fieldName ` +
|
|
676
|
+
`but no enabled 'softDelete' block (softDelete.enabled must be true). This usually means the ` +
|
|
677
|
+
`block was dropped by 'payload sync' or a manual edit, which would treat a soft-delete table ` +
|
|
678
|
+
`as a normal table (/delete becomes a hard delete and the soft-delete columns become ` +
|
|
679
|
+
`user-writable). Re-derive the payload with 'payload generate --schema-path=<sdf>' or restore ` +
|
|
680
|
+
`the softDelete block.`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// --- Restore gate (R20) ---
|
|
685
|
+
// action.restore = true mewajibkan softDelete.enabled = true (restore membalik soft delete).
|
|
686
|
+
if (restoreRequested && !blockEnabled) {
|
|
687
|
+
throw new Error(
|
|
688
|
+
`Payload ${fileName} sets action.restore = true but softDelete.enabled is not true. ` +
|
|
689
|
+
`The restore endpoint reverses a soft delete and requires an enabled softDelete block (R20).`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Scope guard Fase 1: master-detail + soft-delete ditolak. Cascade soft-delete butuh
|
|
694
|
+
// SQL filter injection (detailQuery) yang ditunda ke fase lanjut (Q4 Phase 05).
|
|
695
|
+
const masterDetailEnabled = !!(payload.masterDetail && payload.masterDetail.enabled);
|
|
696
|
+
if (masterDetailEnabled && blockEnabled) {
|
|
697
|
+
throw new Error(
|
|
698
|
+
`Payload ${fileName} combines masterDetail with an enabled softDelete block, which is not ` +
|
|
699
|
+
`supported in Phase 1. Cascade soft-delete needs custom SQL filter injection (detailQuery) ` +
|
|
700
|
+
`deferred to a later phase; Phase 1 covers single-table soft-delete only.`
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// --- Gate FK-aware Fase 1.5 (G1-G4) ---
|
|
705
|
+
// Penolakan konfigurasi FK-aware yang TIDAK didukung Fase 1.5, plus shape defensif untuk
|
|
706
|
+
// metadata yang di-edit manual. Dipisah ke sub-fungsi sendiri; internal no-op bila payload
|
|
707
|
+
// tidak membawa softDeleteFkChildren/softDeleteCascadeTree (backward-compat byte-identik).
|
|
708
|
+
PayloadValidator.validateSoftDeleteFk(payload, fileName);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Gate FK-aware soft-delete (Fase 1.5) — perluasan R17 (defense in depth) yang menolak
|
|
713
|
+
* konfigurasi FK-aware INVALID yang belum didukung Fase 1.5. Dipanggil dari validateSoftDelete
|
|
714
|
+
* setelah seluruh guard R17/Q3/R20. Beroperasi atas metadata FK yang diturunkan `payload generate`:
|
|
715
|
+
* `softDeleteFkChildren` (reverse registry) + `softDeleteCascadeTree` (closure cascade) hanya
|
|
716
|
+
* dipersist untuk tabel soft-delete; `softDeleteFkParents` (forward registry FK-SD-1) dipersist juga
|
|
717
|
+
* untuk tabel anak NON-soft-delete (mis. `visitors`), sehingga shape guard-nya (G5) di sini adalah
|
|
718
|
+
* satu-satunya gate defensif untuknya. Payload tanpa ketiga field ini tidak terpengaruh (no-op).
|
|
719
|
+
*
|
|
720
|
+
* Lima penolakan:
|
|
721
|
+
* - G1: entry `softDeleteFkChildren` `onDelete:'cascade'` ke anak `childSoftDelete:false` →
|
|
722
|
+
* ERROR (cascade men-soft-delete anak, tetapi anak tanpa kolom soft-delete tak bisa).
|
|
723
|
+
* - G2: entry `onDelete:'setNull'` → ERROR (deferral Fase 1.5; SET NULL menuntut mutasi kolom
|
|
724
|
+
* FK anak yang ditunda).
|
|
725
|
+
* - G3: node `softDeleteCascadeTree` dengan `pk` array (composite PK) → ERROR (walk cascade
|
|
726
|
+
* memakai single-column `row[node.pk]` sebagai identitas visited-set/lock; di luar scope).
|
|
727
|
+
* - G4: shape defensif — tipe dasar `softDeleteFkChildren`/`softDeleteCascadeTree` salah (mis.
|
|
728
|
+
* bukan array, `onDelete` di luar enum) → ERROR jelas, bukan meledak saat generate handler.
|
|
729
|
+
* - G5: shape defensif `softDeleteFkParents` — tiap entry `{ columns:[string], refTable:string,
|
|
730
|
+
* refColumns:[string] }`; tipe dasar salah → ERROR jelas (simetri G4).
|
|
731
|
+
*
|
|
732
|
+
* Cakupan G1/G2 sengaja per-tabel pada `softDeleteFkChildren` (anak LANGSUNG master ini). Edge
|
|
733
|
+
* cascade→non-SD/setNull yang lebih dalam (cucu) muncul sebagai anak langsung pada RDF tabel
|
|
734
|
+
* perantara (yang juga soft-delete dan divalidasi sendiri), sehingga tertutup saat tiap tabel
|
|
735
|
+
* soft-delete melewati validator ini. `softDeleteCascadeTree` master hanya cermin agregat dan
|
|
736
|
+
* tidak perlu validasi semantik ulang anaknya.
|
|
737
|
+
*
|
|
738
|
+
* @param {Object} payload - Payload utuh (membaca softDeleteFkChildren, softDeleteCascadeTree)
|
|
739
|
+
* @param {string} fileName - Nama file untuk error message
|
|
740
|
+
* @throws {Error} Jika konfigurasi FK-aware invalid (G1-G4) ditemukan
|
|
741
|
+
*/
|
|
742
|
+
static validateSoftDeleteFk(payload, fileName) {
|
|
743
|
+
const VALID_ON_DELETE = ['cascade', 'restrict', 'setNull'];
|
|
744
|
+
const fkChildren = payload.softDeleteFkChildren;
|
|
745
|
+
const cascadeTree = payload.softDeleteCascadeTree;
|
|
746
|
+
|
|
747
|
+
// --- softDeleteFkChildren: shape (G4) + G1 (cascade→non-SD) + G2 (setNull) ---
|
|
748
|
+
if (fkChildren !== undefined) {
|
|
749
|
+
if (!Array.isArray(fkChildren)) {
|
|
750
|
+
throw new Error(
|
|
751
|
+
`softDeleteFkChildren in ${fileName} must be an array of FK child entries ` +
|
|
752
|
+
`{ childTable, childColumns, parentColumn, onDelete, childSoftDelete }.`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
for (let i = 0; i < fkChildren.length; i++) {
|
|
757
|
+
const entry = fkChildren[i];
|
|
758
|
+
|
|
759
|
+
// G4 shape per-entry.
|
|
760
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
761
|
+
throw new Error(`softDeleteFkChildren[${i}] in ${fileName} must be an object`);
|
|
762
|
+
}
|
|
763
|
+
if (typeof entry.childTable !== 'string' || entry.childTable.trim() === '') {
|
|
764
|
+
throw new Error(
|
|
765
|
+
`softDeleteFkChildren[${i}].childTable in ${fileName} is required and must be a non-empty string`
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
if (
|
|
769
|
+
!Array.isArray(entry.childColumns) ||
|
|
770
|
+
entry.childColumns.length === 0 ||
|
|
771
|
+
!entry.childColumns.every((c) => typeof c === 'string' && c.trim() !== '')
|
|
772
|
+
) {
|
|
773
|
+
throw new Error(
|
|
774
|
+
`softDeleteFkChildren[${i}].childColumns (child '${entry.childTable}') in ${fileName} ` +
|
|
775
|
+
`must be a non-empty array of non-empty strings`
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
if (typeof entry.parentColumn !== 'string' || entry.parentColumn.trim() === '') {
|
|
779
|
+
throw new Error(
|
|
780
|
+
`softDeleteFkChildren[${i}].parentColumn (child '${entry.childTable}') in ${fileName} ` +
|
|
781
|
+
`is required and must be a non-empty string`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
if (!VALID_ON_DELETE.includes(entry.onDelete)) {
|
|
785
|
+
throw new Error(
|
|
786
|
+
`softDeleteFkChildren[${i}].onDelete '${entry.onDelete}' (child '${entry.childTable}') in ` +
|
|
787
|
+
`${fileName} is invalid. Valid values: ${VALID_ON_DELETE.join(', ')}.`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (typeof entry.childSoftDelete !== 'boolean') {
|
|
791
|
+
throw new Error(
|
|
792
|
+
`softDeleteFkChildren[${i}].childSoftDelete (child '${entry.childTable}') in ${fileName} ` +
|
|
793
|
+
`must be a boolean`
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// G2 — setNull ditunda di Fase 1.5. Dicek sebelum G1 (keduanya eksklusif per entry):
|
|
798
|
+
// setNull menuntut mutasi kolom FK anak (interaksi notnull) yang belum dikerjakan.
|
|
799
|
+
if (entry.onDelete === 'setNull') {
|
|
800
|
+
throw new Error(
|
|
801
|
+
`softDeleteFkChildren references child '${entry.childTable}' with onDelete: 'setNull' in ` +
|
|
802
|
+
`${fileName}, which is not supported in Phase 1.5. SET NULL requires mutating the child FK ` +
|
|
803
|
+
`column (interaction with notnull) and is deferred to a later phase. Change onDelete to ` +
|
|
804
|
+
`'restrict' or 'cascade'.`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// G1 — cascade ke anak non-soft-delete. Cascade men-soft-delete anak, tetapi tabel anak
|
|
809
|
+
// tanpa kolom soft-delete tidak dapat di-soft-delete. restrict ke anak non-SD tetap VALID.
|
|
810
|
+
if (entry.onDelete === 'cascade' && entry.childSoftDelete === false) {
|
|
811
|
+
throw new Error(
|
|
812
|
+
`softDeleteFkChildren references child '${entry.childTable}' with onDelete: 'cascade' but ` +
|
|
813
|
+
`the child table is not soft-delete-enabled (childSoftDelete: false) in ${fileName}. Cascade ` +
|
|
814
|
+
`soft-deletes the child, but a table without soft-delete columns cannot be soft-deleted. ` +
|
|
815
|
+
`Either make '${entry.childTable}' soft-delete-enabled, or change onDelete to 'restrict'.`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// --- softDeleteCascadeTree: shape (G4) + G3 (composite-PK node) ---
|
|
822
|
+
if (cascadeTree !== undefined) {
|
|
823
|
+
if (typeof cascadeTree !== 'object' || cascadeTree === null || Array.isArray(cascadeTree)) {
|
|
824
|
+
throw new Error(
|
|
825
|
+
`softDeleteCascadeTree in ${fileName} must be a non-null object { master, order, nodes }`
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
const nodes = cascadeTree.nodes;
|
|
829
|
+
if (typeof nodes !== 'object' || nodes === null || Array.isArray(nodes)) {
|
|
830
|
+
throw new Error(
|
|
831
|
+
`softDeleteCascadeTree.nodes in ${fileName} must be an object keyed by qualified table name`
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
for (const key of Object.keys(nodes)) {
|
|
836
|
+
const node = nodes[key];
|
|
837
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) {
|
|
838
|
+
throw new Error(`softDeleteCascadeTree.nodes['${key}'] in ${fileName} must be an object`);
|
|
839
|
+
}
|
|
840
|
+
const label = (typeof node.table === 'string' && node.table) || key;
|
|
841
|
+
const pk = node.pk;
|
|
842
|
+
|
|
843
|
+
// G3 — composite (multi-column) PK di luar scope FK-SD-3 Fase 1.5. Walk cascade memakai
|
|
844
|
+
// `row[node.pk]` single-column sebagai identitas visited-set/lock; pk array (composite,
|
|
845
|
+
// atau kosong dari IR tanpa PK) tidak dapat dipakai.
|
|
846
|
+
if (Array.isArray(pk)) {
|
|
847
|
+
throw new Error(
|
|
848
|
+
`softDeleteCascadeTree node '${label}' in ${fileName} has a composite (multi-column) ` +
|
|
849
|
+
`primary key (pk: [${pk.join(', ')}]), which is out of scope for Phase 1.5 cascade ` +
|
|
850
|
+
`(FK-SD-3). The cascade walk uses a single-column pk (row[node.pk]) as the visited-set/lock ` +
|
|
851
|
+
`identity. Composite-PK cascade is deferred to a later phase.`
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// G4 shape — pk single harus string non-kosong (identitas walk).
|
|
856
|
+
if (typeof pk !== 'string' || pk.trim() === '') {
|
|
857
|
+
throw new Error(
|
|
858
|
+
`softDeleteCascadeTree node '${label}' in ${fileName} must have a non-empty string 'pk' ` +
|
|
859
|
+
`(single-column primary key used as the cascade walk identity)`
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// --- softDeleteFkParents: shape (G5) ---
|
|
866
|
+
// Forward registry FK-SD-1: tiap entry { columns:[string], refTable:string, refColumns:[string] }.
|
|
867
|
+
// Berbeda dari softDeleteFkChildren/softDeleteCascadeTree (hanya pada tabel soft-delete), field
|
|
868
|
+
// ini dipersist juga pada tabel anak NON-soft-delete (mis. visitors), sehingga shape guard di sini
|
|
869
|
+
// adalah satu-satunya gate defensif untuknya — simetri dengan G4 softDeleteFkChildren. Hanya
|
|
870
|
+
// memeriksa bentuk dasar (array/object/string non-kosong); semantik FK (kolom benar-benar FK ke
|
|
871
|
+
// parent soft-delete) ditegakkan SDF saat schema validate, bukan diduplikasi di sini.
|
|
872
|
+
const fkParents = payload.softDeleteFkParents;
|
|
873
|
+
if (fkParents !== undefined) {
|
|
874
|
+
if (!Array.isArray(fkParents)) {
|
|
875
|
+
throw new Error(
|
|
876
|
+
`softDeleteFkParents in ${fileName} must be an array of FK parent entries ` +
|
|
877
|
+
`{ columns, refTable, refColumns }.`
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
for (let i = 0; i < fkParents.length; i++) {
|
|
882
|
+
const entry = fkParents[i];
|
|
883
|
+
|
|
884
|
+
// G5 shape per-entry.
|
|
885
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
886
|
+
throw new Error(`softDeleteFkParents[${i}] in ${fileName} must be an object`);
|
|
887
|
+
}
|
|
888
|
+
if (
|
|
889
|
+
!Array.isArray(entry.columns) ||
|
|
890
|
+
entry.columns.length === 0 ||
|
|
891
|
+
!entry.columns.every((c) => typeof c === 'string' && c.trim() !== '')
|
|
892
|
+
) {
|
|
893
|
+
throw new Error(
|
|
894
|
+
`softDeleteFkParents[${i}].columns in ${fileName} must be a non-empty array of non-empty strings`
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
if (typeof entry.refTable !== 'string' || entry.refTable.trim() === '') {
|
|
898
|
+
throw new Error(
|
|
899
|
+
`softDeleteFkParents[${i}].refTable in ${fileName} is required and must be a non-empty string`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
if (
|
|
903
|
+
!Array.isArray(entry.refColumns) ||
|
|
904
|
+
entry.refColumns.length === 0 ||
|
|
905
|
+
!entry.refColumns.every((c) => typeof c === 'string' && c.trim() !== '')
|
|
906
|
+
) {
|
|
907
|
+
throw new Error(
|
|
908
|
+
`softDeleteFkParents[${i}].refColumns (refTable '${entry.refTable}') in ${fileName} ` +
|
|
909
|
+
`must be a non-empty array of non-empty strings`
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
489
916
|
/**
|
|
490
917
|
* Validasi khusus untuk fieldName array
|
|
491
918
|
* @param {Array} fieldNames - Array nama field
|
|
@@ -705,22 +1132,22 @@ class PayloadValidator {
|
|
|
705
1132
|
*/
|
|
706
1133
|
static validateSearchColumns(searchColumns, fileName) {
|
|
707
1134
|
if (!Array.isArray(searchColumns)) {
|
|
708
|
-
throw new Error(`Property 'searchColumns'
|
|
1135
|
+
throw new Error(`Property 'searchColumns' in ${fileName} must be an array`);
|
|
709
1136
|
}
|
|
710
1137
|
|
|
711
1138
|
for (let i = 0; i < searchColumns.length; i++) {
|
|
712
1139
|
const column = searchColumns[i];
|
|
713
1140
|
|
|
714
1141
|
if (typeof column !== 'object' || column === null) {
|
|
715
|
-
throw new Error(`Search column index ${i}
|
|
1142
|
+
throw new Error(`Search column index ${i} in ${fileName} must be an object`);
|
|
716
1143
|
}
|
|
717
1144
|
|
|
718
1145
|
if (!column.name || typeof column.name !== 'string') {
|
|
719
|
-
throw new Error(`Search column index ${i}
|
|
1146
|
+
throw new Error(`Search column index ${i} in ${fileName} must have a 'name' property of type string`);
|
|
720
1147
|
}
|
|
721
1148
|
|
|
722
1149
|
if (column.searchable !== undefined && typeof column.searchable !== 'boolean') {
|
|
723
|
-
throw new Error(`Search column '${column.name}'
|
|
1150
|
+
throw new Error(`Search column '${column.name}' in ${fileName}: property 'searchable' must be a boolean`);
|
|
724
1151
|
}
|
|
725
1152
|
}
|
|
726
1153
|
}
|
|
@@ -733,7 +1160,7 @@ class PayloadValidator {
|
|
|
733
1160
|
*/
|
|
734
1161
|
static validateFilters(filters, fileName) {
|
|
735
1162
|
if (!Array.isArray(filters)) {
|
|
736
|
-
throw new Error(`Property 'filters'
|
|
1163
|
+
throw new Error(`Property 'filters' in ${fileName} must be an array`);
|
|
737
1164
|
}
|
|
738
1165
|
|
|
739
1166
|
const validFilterTypes = ['combobox', 'select', 'daterange', 'text', 'number'];
|
|
@@ -742,35 +1169,35 @@ class PayloadValidator {
|
|
|
742
1169
|
const filter = filters[i];
|
|
743
1170
|
|
|
744
1171
|
if (typeof filter !== 'object' || filter === null) {
|
|
745
|
-
throw new Error(`Filter index ${i}
|
|
1172
|
+
throw new Error(`Filter index ${i} in ${fileName} must be an object`);
|
|
746
1173
|
}
|
|
747
1174
|
|
|
748
1175
|
// Required properties
|
|
749
1176
|
if (!filter.name || typeof filter.name !== 'string') {
|
|
750
|
-
throw new Error(`Filter index ${i}
|
|
1177
|
+
throw new Error(`Filter index ${i} in ${fileName} must have a 'name' property of type string`);
|
|
751
1178
|
}
|
|
752
1179
|
|
|
753
1180
|
if (!filter.type || typeof filter.type !== 'string') {
|
|
754
|
-
throw new Error(`Filter '${filter.name}'
|
|
1181
|
+
throw new Error(`Filter '${filter.name}' in ${fileName} must have a 'type' property of type string`);
|
|
755
1182
|
}
|
|
756
1183
|
|
|
757
1184
|
if (!validFilterTypes.includes(filter.type)) {
|
|
758
|
-
throw new Error(`Filter '${filter.name}'
|
|
1185
|
+
throw new Error(`Filter '${filter.name}' in ${fileName} has an invalid type: ${filter.type}. Valid types: ${validFilterTypes.join(', ')}`);
|
|
759
1186
|
}
|
|
760
1187
|
|
|
761
1188
|
// Type-specific validation
|
|
762
1189
|
if (['combobox', 'select'].includes(filter.type)) {
|
|
763
1190
|
if (!filter.targetField || typeof filter.targetField !== 'string') {
|
|
764
|
-
throw new Error(`Filter '${filter.name}' type '${filter.type}'
|
|
1191
|
+
throw new Error(`Filter '${filter.name}' of type '${filter.type}' in ${fileName} must have a 'targetField' property`);
|
|
765
1192
|
}
|
|
766
1193
|
}
|
|
767
1194
|
|
|
768
1195
|
if (filter.type === 'daterange') {
|
|
769
1196
|
if (!filter.startField || typeof filter.startField !== 'string') {
|
|
770
|
-
throw new Error(`Filter '${filter.name}' type 'daterange'
|
|
1197
|
+
throw new Error(`Filter '${filter.name}' of type 'daterange' in ${fileName} must have a 'startField' property`);
|
|
771
1198
|
}
|
|
772
1199
|
if (!filter.endField || typeof filter.endField !== 'string') {
|
|
773
|
-
throw new Error(`Filter '${filter.name}' type 'daterange'
|
|
1200
|
+
throw new Error(`Filter '${filter.name}' of type 'daterange' in ${fileName} must have an 'endField' property`);
|
|
774
1201
|
}
|
|
775
1202
|
}
|
|
776
1203
|
}
|
|
@@ -784,17 +1211,17 @@ class PayloadValidator {
|
|
|
784
1211
|
*/
|
|
785
1212
|
static validateAdvancedQueries(advancedQueries, fileName) {
|
|
786
1213
|
if (typeof advancedQueries !== 'object' || advancedQueries === null) {
|
|
787
|
-
throw new Error(`Property 'advancedQueries'
|
|
1214
|
+
throw new Error(`Property 'advancedQueries' in ${fileName} must be an object`);
|
|
788
1215
|
}
|
|
789
1216
|
|
|
790
1217
|
for (const [queryName, querySource] of Object.entries(advancedQueries)) {
|
|
791
1218
|
if (typeof querySource !== 'string' || querySource.trim().length === 0) {
|
|
792
|
-
throw new Error(`Advanced query '${queryName}'
|
|
1219
|
+
throw new Error(`Advanced query '${queryName}' in ${fileName} must be a non-empty string`);
|
|
793
1220
|
}
|
|
794
1221
|
|
|
795
1222
|
// Validate query name
|
|
796
1223
|
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(queryName)) {
|
|
797
|
-
throw new Error(`Advanced query name '${queryName}'
|
|
1224
|
+
throw new Error(`Advanced query name '${queryName}' in ${fileName} is invalid. It must start with a letter and contain only letters, numbers, and underscores`);
|
|
798
1225
|
}
|
|
799
1226
|
|
|
800
1227
|
// Validate file reference if applicable
|
|
@@ -815,22 +1242,22 @@ class PayloadValidator {
|
|
|
815
1242
|
*/
|
|
816
1243
|
static validateNestedQueries(nestedQueries, fileName) {
|
|
817
1244
|
if (!Array.isArray(nestedQueries)) {
|
|
818
|
-
throw new Error(`Property 'nestedQueries'
|
|
1245
|
+
throw new Error(`Property 'nestedQueries' in ${fileName} must be an array`);
|
|
819
1246
|
}
|
|
820
1247
|
|
|
821
1248
|
for (let i = 0; i < nestedQueries.length; i++) {
|
|
822
1249
|
const nestedQuery = nestedQueries[i];
|
|
823
1250
|
|
|
824
1251
|
if (typeof nestedQuery !== 'object' || nestedQuery === null) {
|
|
825
|
-
throw new Error(`Nested query index ${i}
|
|
1252
|
+
throw new Error(`Nested query index ${i} in ${fileName} must be an object`);
|
|
826
1253
|
}
|
|
827
1254
|
|
|
828
1255
|
if (!nestedQuery.name || typeof nestedQuery.name !== 'string') {
|
|
829
|
-
throw new Error(`Nested query index ${i}
|
|
1256
|
+
throw new Error(`Nested query index ${i} in ${fileName} must have a 'name' property of type string`);
|
|
830
1257
|
}
|
|
831
1258
|
|
|
832
1259
|
if (!nestedQuery.query || typeof nestedQuery.query !== 'string') {
|
|
833
|
-
throw new Error(`Nested query '${nestedQuery.name}'
|
|
1260
|
+
throw new Error(`Nested query '${nestedQuery.name}' in ${fileName} must have a 'query' property of type string`);
|
|
834
1261
|
}
|
|
835
1262
|
|
|
836
1263
|
// Validate file reference if applicable
|
|
@@ -852,13 +1279,13 @@ class PayloadValidator {
|
|
|
852
1279
|
*/
|
|
853
1280
|
static validateFileReference(fileReference, fileName, context) {
|
|
854
1281
|
if (!fileReference.startsWith('file:')) {
|
|
855
|
-
throw new Error(`File reference
|
|
1282
|
+
throw new Error(`File reference for ${context} in ${fileName} must start with 'file:'`);
|
|
856
1283
|
}
|
|
857
1284
|
|
|
858
1285
|
const relativePath = fileReference.substring(5);
|
|
859
1286
|
|
|
860
1287
|
if (!relativePath || relativePath.trim().length === 0) {
|
|
861
|
-
throw new Error(`File reference
|
|
1288
|
+
throw new Error(`File reference for ${context} in ${fileName} does not have a valid path`);
|
|
862
1289
|
}
|
|
863
1290
|
|
|
864
1291
|
// Validate file extension - allow both .sql and .json for queries
|
|
@@ -904,7 +1331,7 @@ class PayloadValidator {
|
|
|
904
1331
|
|
|
905
1332
|
for (const pattern of dangerousPatterns) {
|
|
906
1333
|
if (pattern.test(trimmedSql)) {
|
|
907
|
-
throw new Error(`Inline SQL
|
|
1334
|
+
throw new Error(`Inline SQL for ${context} in ${fileName} contains a dangerous keyword: ${pattern.source}`);
|
|
908
1335
|
}
|
|
909
1336
|
}
|
|
910
1337
|
}
|
|
@@ -949,8 +1376,8 @@ class PayloadValidator {
|
|
|
949
1376
|
);
|
|
950
1377
|
|
|
951
1378
|
if (conflictingFields.length > 0) {
|
|
952
|
-
result.warnings.push(`Field names
|
|
953
|
-
result.recommendations.push('
|
|
1379
|
+
result.warnings.push(`Field names may conflict with Oracle reserved words: ${conflictingFields.join(', ')}`);
|
|
1380
|
+
result.recommendations.push('Consider renaming the fields or using quoted identifiers');
|
|
954
1381
|
}
|
|
955
1382
|
}
|
|
956
1383
|
|
|
@@ -1141,7 +1568,7 @@ class PayloadValidator {
|
|
|
1141
1568
|
);
|
|
1142
1569
|
|
|
1143
1570
|
if (missingActions.length > 0) {
|
|
1144
|
-
throw new Error(`Payload
|
|
1571
|
+
throw new Error(`Payload must include the following actions: ${missingActions.join(', ')}`);
|
|
1145
1572
|
}
|
|
1146
1573
|
}
|
|
1147
1574
|
|
|
@@ -1153,7 +1580,7 @@ class PayloadValidator {
|
|
|
1153
1580
|
);
|
|
1154
1581
|
|
|
1155
1582
|
if (missingFields.length > 0) {
|
|
1156
|
-
throw new Error(`Payload
|
|
1583
|
+
throw new Error(`Payload must include the following fields: ${missingFields.join(', ')}`);
|
|
1157
1584
|
}
|
|
1158
1585
|
}
|
|
1159
1586
|
|
|
@@ -1161,7 +1588,7 @@ class PayloadValidator {
|
|
|
1161
1588
|
if (requirements.minFieldCount && typeof requirements.minFieldCount === 'number') {
|
|
1162
1589
|
const fieldCount = payload.fieldName ? payload.fieldName.length : 0;
|
|
1163
1590
|
if (fieldCount < requirements.minFieldCount) {
|
|
1164
|
-
throw new Error(`Payload
|
|
1591
|
+
throw new Error(`Payload must have at least ${requirements.minFieldCount} fields, found ${fieldCount}`);
|
|
1165
1592
|
}
|
|
1166
1593
|
}
|
|
1167
1594
|
|
|
@@ -1169,7 +1596,7 @@ class PayloadValidator {
|
|
|
1169
1596
|
if (requirements.maxFieldCount && typeof requirements.maxFieldCount === 'number') {
|
|
1170
1597
|
const fieldCount = payload.fieldName ? payload.fieldName.length : 0;
|
|
1171
1598
|
if (fieldCount > requirements.maxFieldCount) {
|
|
1172
|
-
throw new Error(`Payload
|
|
1599
|
+
throw new Error(`Payload must not have more than ${requirements.maxFieldCount} fields, found ${fieldCount}`);
|
|
1173
1600
|
}
|
|
1174
1601
|
}
|
|
1175
1602
|
|
|
@@ -1177,7 +1604,7 @@ class PayloadValidator {
|
|
|
1177
1604
|
if (requirements.tableNamePattern && typeof requirements.tableNamePattern === 'string') {
|
|
1178
1605
|
const pattern = new RegExp(requirements.tableNamePattern);
|
|
1179
1606
|
if (!pattern.test(payload.tableName)) {
|
|
1180
|
-
throw new Error(`Table name '${payload.tableName}'
|
|
1607
|
+
throw new Error(`Table name '${payload.tableName}' does not match the required pattern: ${requirements.tableNamePattern}`);
|
|
1181
1608
|
}
|
|
1182
1609
|
}
|
|
1183
1610
|
|
|
@@ -1285,7 +1712,7 @@ class PayloadValidator {
|
|
|
1285
1712
|
// Check file existence
|
|
1286
1713
|
const payloadPath = this.findPayloadFile(payloadFileName);
|
|
1287
1714
|
if (!payloadPath) {
|
|
1288
|
-
result.errors.push('
|
|
1715
|
+
result.errors.push('Payload file not found');
|
|
1289
1716
|
return result;
|
|
1290
1717
|
}
|
|
1291
1718
|
|
|
@@ -1297,12 +1724,12 @@ class PayloadValidator {
|
|
|
1297
1724
|
result.size = stats.size;
|
|
1298
1725
|
|
|
1299
1726
|
if (result.size === 0) {
|
|
1300
|
-
result.errors.push('
|
|
1727
|
+
result.errors.push('Payload file is empty');
|
|
1301
1728
|
return result;
|
|
1302
1729
|
}
|
|
1303
1730
|
|
|
1304
1731
|
if (result.size > 10 * 1024 * 1024) { // 10MB limit
|
|
1305
|
-
result.errors.push('
|
|
1732
|
+
result.errors.push('Payload file is too large (>10MB)');
|
|
1306
1733
|
return result;
|
|
1307
1734
|
}
|
|
1308
1735
|
|
|
@@ -1312,7 +1739,7 @@ class PayloadValidator {
|
|
|
1312
1739
|
JSON.parse(content);
|
|
1313
1740
|
result.isValidJson = true;
|
|
1314
1741
|
} catch (jsonError) {
|
|
1315
|
-
result.errors.push(`
|
|
1742
|
+
result.errors.push(`Invalid JSON format: ${jsonError.message}`);
|
|
1316
1743
|
}
|
|
1317
1744
|
|
|
1318
1745
|
} catch (error) {
|