@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.
Files changed (201) hide show
  1. package/bin/restforge-hwinfo-linux +0 -0
  2. package/bin/restforge-hwinfo.exe +0 -0
  3. package/build-info.json +2 -2
  4. package/cli/consumer-deploy.js +1 -1
  5. package/cli/consumer.js +1 -1
  6. package/generators/cli/payload/generate.js +10 -2
  7. package/generators/cli/schema/apply.js +6 -1
  8. package/generators/cli/schema/diff.js +6 -1
  9. package/generators/cli/schema/introspect.js +32 -11
  10. package/generators/lib/data/db-executor.js +8 -8
  11. package/generators/lib/data/envelope.js +3 -3
  12. package/generators/lib/dbschema-kit/apply-engine.js +20 -0
  13. package/generators/lib/dbschema-kit/dialect/mysql.js +2 -0
  14. package/generators/lib/dbschema-kit/dialect/oracle.js +2 -0
  15. package/generators/lib/dbschema-kit/dialect/postgres.js +4 -0
  16. package/generators/lib/dbschema-kit/dialect/sqlite.js +5 -0
  17. package/generators/lib/dbschema-kit/diff-engine.js +22 -1
  18. package/generators/lib/dbschema-kit/diff-reporter.js +293 -272
  19. package/generators/lib/dbschema-kit/emitters/create-index.js +23 -1
  20. package/generators/lib/dbschema-kit/emitters/create-table.js +48 -0
  21. package/generators/lib/dbschema-kit/introspect-mapper.js +154 -2
  22. package/generators/lib/dbschema-kit/ir-builder.js +84 -1
  23. package/generators/lib/dbschema-kit/schema-printer.js +20 -0
  24. package/generators/lib/dbschema-kit/soft-delete-constants.js +111 -0
  25. package/generators/lib/dbschema-kit/validator/schema-validator.js +231 -0
  26. package/generators/lib/generators/processor-validation-generator.js +16 -16
  27. package/generators/lib/payload/payload-runner.js +711 -1
  28. package/generators/lib/payload/schema-diff.js +7 -0
  29. package/generators/lib/templates/dashboard-catalog.js +1 -1
  30. package/generators/lib/templates/db-connection-env.js +1 -1
  31. package/generators/lib/templates/dbschema-catalog.js +1 -1
  32. package/generators/lib/templates/field-validation-catalog.js +1 -1
  33. package/generators/lib/templates/mysql-template.js +1 -1
  34. package/generators/lib/templates/oracle-template.js +1 -1
  35. package/generators/lib/templates/postgres-template.js +1 -1
  36. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  37. package/generators/lib/templates/sqlite-template.js +1 -1
  38. package/generators/lib/utils/database-introspector.js +48 -0
  39. package/generators/lib/utils/env-manager.js +4 -4
  40. package/generators/lib/utils/file-utils.js +6 -6
  41. package/generators/lib/utils/payload-processor.js +18 -2
  42. package/generators/lib/validators/argument-validator.js +2 -2
  43. package/generators/lib/validators/dashboard-validator.js +35 -1
  44. package/generators/lib/validators/payload-validator.js +460 -33
  45. package/integrity-manifest.json +20 -20
  46. package/package.json +2 -1
  47. package/scripts/verify-integrity.js +1 -1
  48. package/server.js +1 -1
  49. package/src/components/handlers/adjust_handler.js +1 -1
  50. package/src/components/handlers/audit_handler.js +1 -1
  51. package/src/components/handlers/delete_handler.js +1 -1
  52. package/src/components/handlers/export_handler.js +1 -1
  53. package/src/components/handlers/import_handler.js +1 -1
  54. package/src/components/handlers/insert_handler.js +1 -1
  55. package/src/components/handlers/update_handler.js +1 -1
  56. package/src/components/handlers/upload_handler.js +1 -1
  57. package/src/components/handlers/workflow_handler.js +1 -1
  58. package/src/components/integrations/webhook.js +1 -1
  59. package/src/consumers/baseConsumer.js +1 -1
  60. package/src/consumers/declarativeMapper.js +1 -1
  61. package/src/consumers/handlers/apiHandler.js +1 -1
  62. package/src/consumers/handlers/consoleHandler.js +1 -1
  63. package/src/consumers/handlers/databaseHandler.js +1 -1
  64. package/src/consumers/handlers/index.js +1 -1
  65. package/src/consumers/handlers/kafkaHandler.js +1 -1
  66. package/src/consumers/index.js +1 -1
  67. package/src/consumers/messageTransformer.js +1 -1
  68. package/src/consumers/validator.js +1 -1
  69. package/src/core/db/dialect/base-dialect.js +1 -1
  70. package/src/core/db/dialect/index.js +1 -1
  71. package/src/core/db/dialect/mysql-dialect.js +1 -1
  72. package/src/core/db/dialect/oracle-dialect.js +1 -1
  73. package/src/core/db/dialect/postgres-dialect.js +1 -1
  74. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  75. package/src/core/db/flatten-helper.js +1 -1
  76. package/src/core/db/query-builder-error.js +1 -1
  77. package/src/core/db/query-builder.js +1 -1
  78. package/src/core/db/relation-helper.js +1 -1
  79. package/src/core/handlers/delete_handler.js +1 -1
  80. package/src/core/handlers/insert_handler.js +1 -1
  81. package/src/core/handlers/update_handler.js +1 -1
  82. package/src/core/models/base-model.js +1 -1
  83. package/src/core/utils/cache-manager.js +1 -1
  84. package/src/core/utils/component-engine.js +1 -1
  85. package/src/core/utils/context-builder.js +1 -1
  86. package/src/core/utils/datetime-formatter.js +1 -1
  87. package/src/core/utils/datetime-parser.js +1 -1
  88. package/src/core/utils/db.js +1 -1
  89. package/src/core/utils/logger.js +1 -1
  90. package/src/core/utils/payload-loader.js +1 -1
  91. package/src/core/utils/security-checks.js +1 -1
  92. package/src/middleware/body-options.js +1 -1
  93. package/src/middleware/cors.js +1 -1
  94. package/src/middleware/idempotency.js +1 -1
  95. package/src/middleware/rate-limiter.js +1 -1
  96. package/src/middleware/request-logger.js +1 -1
  97. package/src/middleware/security-headers.js +1 -1
  98. package/src/models/base-model-mysql.js +1 -1
  99. package/src/models/base-model-oracle.js +1 -1
  100. package/src/models/base-model-sqlite.js +1 -1
  101. package/src/models/base-model.js +1 -1
  102. package/src/pro/caching/redis-client.js +1 -1
  103. package/src/pro/caching/redis-helper.js +1 -1
  104. package/src/pro/consumers/baseConsumer.js +1 -1
  105. package/src/pro/consumers/declarativeMapper.js +1 -1
  106. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  107. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  108. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  109. package/src/pro/consumers/handlers/index.js +1 -1
  110. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  111. package/src/pro/consumers/index.js +1 -1
  112. package/src/pro/consumers/messageTransformer.js +1 -1
  113. package/src/pro/consumers/validator.js +1 -1
  114. package/src/pro/database/base-model-mysql.js +1 -1
  115. package/src/pro/database/base-model-oracle.js +1 -1
  116. package/src/pro/database/base-model-sqlite.js +1 -1
  117. package/src/pro/database/db-mysql.js +1 -1
  118. package/src/pro/database/db-oracle.js +1 -1
  119. package/src/pro/database/db-sqlite.js +1 -1
  120. package/src/pro/excel/excel-generator.js +1 -1
  121. package/src/pro/excel/excel-parser.js +1 -1
  122. package/src/pro/excel/export-service.js +1 -1
  123. package/src/pro/excel/export_handler.js +1 -1
  124. package/src/pro/excel/import-service.js +1 -1
  125. package/src/pro/excel/import-validator.js +1 -1
  126. package/src/pro/excel/import_handler.js +1 -1
  127. package/src/pro/excel/upsert-builder.js +1 -1
  128. package/src/pro/idgen/idgen-routes.js +1 -1
  129. package/src/pro/integrations/lookup-resolver.js +1 -1
  130. package/src/pro/integrations/upload-handler-v2.js +1 -1
  131. package/src/pro/integrations/upload-handler.js +1 -1
  132. package/src/pro/integrations/webhook.js +1 -1
  133. package/src/pro/locking/lock-routes.js +1 -1
  134. package/src/pro/locking/resource-lock-manager.js +1 -1
  135. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  136. package/src/pro/messaging/kafkaService.js +1 -1
  137. package/src/pro/messaging/messagehubService.js +1 -1
  138. package/src/pro/messaging/rabbitmqService.js +1 -1
  139. package/src/pro/scheduler/job-manager.js +1 -1
  140. package/src/pro/scheduler/job-routes.js +1 -1
  141. package/src/pro/scheduler/job-validator.js +1 -1
  142. package/src/pro/storage/base-storage-provider.js +1 -1
  143. package/src/pro/storage/file-metadata-helper.js +1 -1
  144. package/src/pro/storage/index.js +1 -1
  145. package/src/pro/storage/local-storage-provider.js +1 -1
  146. package/src/pro/storage/s3-storage-provider.js +1 -1
  147. package/src/pro/storage/upload-cleanup-job.js +1 -1
  148. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  149. package/src/pro/storage/upload-pending-tracker.js +1 -1
  150. package/src/pro/websocket/broadcast-helper.js +1 -1
  151. package/src/pro/websocket/index.js +1 -1
  152. package/src/pro/websocket/livesync-server.js +1 -1
  153. package/src/pro/websocket/ws-broadcaster.js +1 -1
  154. package/src/services/export-service.js +1 -1
  155. package/src/services/import-service.js +1 -1
  156. package/src/services/kafkaConsumerService.js +1 -1
  157. package/src/services/kafkaService.js +1 -1
  158. package/src/services/messagehubService.js +1 -1
  159. package/src/services/rabbitmqService.js +1 -1
  160. package/src/utils/cache-invalidation-registry.js +1 -1
  161. package/src/utils/cache-manager.js +1 -1
  162. package/src/utils/component-engine.js +1 -1
  163. package/src/utils/config-extractor.js +1 -1
  164. package/src/utils/consumerLogger.js +1 -1
  165. package/src/utils/context-builder.js +1 -1
  166. package/src/utils/dashboard-helpers.js +1 -1
  167. package/src/utils/dateHelper.js +1 -1
  168. package/src/utils/datetime-formatter.js +1 -1
  169. package/src/utils/datetime-parser.js +1 -1
  170. package/src/utils/db-bootstrap.js +1 -1
  171. package/src/utils/db-mysql.js +1 -1
  172. package/src/utils/db-oracle.js +1 -1
  173. package/src/utils/db-sqlite.js +1 -1
  174. package/src/utils/db.js +1 -1
  175. package/src/utils/demo-generator.js +1 -1
  176. package/src/utils/excel-generator.js +1 -1
  177. package/src/utils/excel-parser.js +1 -1
  178. package/src/utils/file-watcher.js +1 -1
  179. package/src/utils/id-generator.js +1 -1
  180. package/src/utils/idempotency-manager.js +1 -1
  181. package/src/utils/import-validator.js +1 -1
  182. package/src/utils/license-client.js +1 -1
  183. package/src/utils/lock-manager.js +1 -1
  184. package/src/utils/logger.js +1 -1
  185. package/src/utils/lookup-resolver.js +1 -1
  186. package/src/utils/payload-loader.js +1 -1
  187. package/src/utils/processor-response.js +1 -1
  188. package/src/utils/rabbitmq.js +1 -1
  189. package/src/utils/redis-client.js +1 -1
  190. package/src/utils/redis-helper.js +1 -1
  191. package/src/utils/request-scope.js +1 -1
  192. package/src/utils/security-checks.js +1 -1
  193. package/src/utils/service-resolver.js +1 -1
  194. package/src/utils/shutdown-coordinator.js +1 -1
  195. package/src/utils/soft-delete-dashboard-guard.js +1 -0
  196. package/src/utils/sql-table-extractor.js +1 -0
  197. package/src/utils/trusted-keys.js +1 -1
  198. package/src/utils/upload-handler.js +1 -1
  199. package/src/utils/upsert-builder.js +1 -1
  200. package/src/utils/workflow-hook-executor.js +1 -1
  201. 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' di ${fileName} harus berupa array`);
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} di ${fileName} harus berupa object`);
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} di ${fileName} harus memiliki property 'name' berupa string`);
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}' di ${fileName} property 'searchable' harus boolean`);
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' di ${fileName} harus berupa array`);
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} di ${fileName} harus berupa object`);
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} di ${fileName} harus memiliki property 'name' berupa string`);
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}' di ${fileName} harus memiliki property 'type' berupa string`);
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}' di ${fileName} memiliki type tidak valid: ${filter.type}. Valid types: ${validFilterTypes.join(', ')}`);
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}' di ${fileName} harus memiliki property 'targetField'`);
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' di ${fileName} harus memiliki property 'startField'`);
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' di ${fileName} harus memiliki property 'endField'`);
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' di ${fileName} harus berupa object`);
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}' di ${fileName} harus berupa string non-empty`);
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}' di ${fileName} tidak valid. Harus dimulai dengan huruf dan hanya mengandung huruf, angka, underscore`);
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' di ${fileName} harus berupa array`);
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} di ${fileName} harus berupa object`);
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} di ${fileName} harus memiliki property 'name' berupa string`);
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}' di ${fileName} harus memiliki property 'query' berupa string`);
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 untuk ${context} di ${fileName} harus dimulai dengan 'file:'`);
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 untuk ${context} di ${fileName} tidak memiliki path yang valid`);
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 untuk ${context} di ${fileName} mengandung keyword berbahaya: ${pattern.source}`);
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 berpotensi konflik dengan Oracle reserved words: ${conflictingFields.join(', ')}`);
953
- result.recommendations.push('Pertimbangkan untuk mengganti nama field atau gunakan quoted identifiers');
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 harus memiliki actions berikut: ${missingActions.join(', ')}`);
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 harus memiliki fields berikut: ${missingFields.join(', ')}`);
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 harus memiliki minimal ${requirements.minFieldCount} fields, ditemukan ${fieldCount}`);
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 tidak boleh memiliki lebih dari ${requirements.maxFieldCount} fields, ditemukan ${fieldCount}`);
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}' tidak sesuai dengan pattern yang diperlukan: ${requirements.tableNamePattern}`);
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('File payload tidak ditemukan');
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('File payload kosong');
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('File payload terlalu besar (>10MB)');
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(`Format JSON tidak valid: ${jsonError.message}`);
1742
+ result.errors.push(`Invalid JSON format: ${jsonError.message}`);
1316
1743
  }
1317
1744
 
1318
1745
  } catch (error) {