@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,5 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const {
4
+ SOFT_DELETE_COLUMNS,
5
+ SOFT_DELETE_COLUMN_TYPES,
6
+ SOFT_DELETE_SUFFIX_LENGTH
7
+ } = require('../soft-delete-constants');
8
+
3
9
  const VALID_RELATION_TYPES = new Set(['belongsTo', 'hasOne', 'hasMany']);
4
10
  const VALID_REFERENTIAL_ACTIONS = new Set(['cascade', 'restrict', 'setNull', 'noAction']);
5
11
  const COMPARISON_OPS = new Set(['gt', 'gte', 'lt', 'lte']);
@@ -7,6 +13,14 @@ const AUTO_UPDATE_VALID_TYPES = new Set(['timestamp', 'date']);
7
13
  const TABLE_NAME_RE = /^[a-z][a-z0-9_]*$/;
8
14
  const AUTO_SUFFIX = '_auto';
9
15
 
16
+ // --- Soft-delete (Fase 1) -------------------------------------------------
17
+ // Nama kolom (R3), kontrak tipe (R5), dan suffix length (R7) berasal dari modul shared
18
+ // `soft-delete-constants` (sumber kebenaran tunggal, dipakai juga oleh emitter DDL Phase 02
19
+ // dan jalur reverse introspect Phase 03). Kontrak tipe forward (R5) di validator ini dan
20
+ // kontrak tipe reverse di introspect-mapper kini membaca satu konstanta yang sama.
21
+ // Reusable hanya didukung untuk tipe string (varchar/text) karena value-mutation (R9).
22
+ const REUSABLE_STRING_TYPES = new Set(['string', 'text']);
23
+
10
24
  function validateSchema(ir) {
11
25
  validateTableLevel(ir);
12
26
  validateSchemaName(ir);
@@ -15,6 +29,7 @@ function validateSchema(ir) {
15
29
  validateIndexAndUniqueReferences(ir);
16
30
  validateChecks(ir);
17
31
  validateAutoUpdate(ir);
32
+ validateSoftDelete(ir);
18
33
  }
19
34
 
20
35
  function prefix(ir, msg) {
@@ -207,4 +222,220 @@ function validateAutoUpdate(ir) {
207
222
  }
208
223
  }
209
224
 
225
+ // --- Soft-delete invariants (R3, R5, R7, R9) ------------------------------
226
+ //
227
+ // Predikat tunggal ini dipanggil dari validateSchema, sehingga seluruh command
228
+ // schema yang memuat SDF lewat defineModel (validate/migrate/diff/apply) menegakkan
229
+ // invariant yang sama tanpa ada jalur yang bisa melewatinya (R4).
230
+ //
231
+ // Catatan bahasa pesan: mengikuti gaya validator existing yang seluruhnya berbahasa
232
+ // Inggris dengan prefix `Table '<name>': ...`. Struktur multi-baris (mis. format
233
+ // declared/required pada R7) mengikuti contoh plan master.
234
+ function validateSoftDelete(ir) {
235
+ const { fields, softDelete } = ir;
236
+
237
+ const enabled = softDelete !== undefined && softDelete !== null && softDelete.enabled === true;
238
+ const presentColumns = SOFT_DELETE_COLUMNS.filter((col) => fields[col] !== undefined);
239
+
240
+ // R3 (arah balik): kolom soft-delete ada tetapi enabled bukan true → ERROR.
241
+ if (!enabled) {
242
+ if (presentColumns.length > 0) {
243
+ throw new Error(prefix(
244
+ ir,
245
+ `soft-delete columns ${presentColumns.join(', ')} are declared in 'fields', ` +
246
+ `but 'softDelete.enabled' is not true. ` +
247
+ `Either declare 'softDelete.enabled = true', or remove these columns.`
248
+ ));
249
+ }
250
+ // Tabel normal: tidak ada blok softDelete dan tidak ada kolom soft-delete. OK.
251
+ return;
252
+ }
253
+
254
+ // R3 (arah maju): enabled = true tetapi ada kolom yang hilang → ERROR.
255
+ const missing = SOFT_DELETE_COLUMNS.filter((col) => fields[col] === undefined);
256
+ if (missing.length > 0) {
257
+ throw new Error(prefix(
258
+ ir,
259
+ `softDelete.enabled = true requires all soft-delete columns in 'fields', missing: ${missing.join(', ')}. ` +
260
+ `Declare is_deleted (boolean), deleted_at (timestamp), deleted_by (string), or set softDelete.enabled = false.`
261
+ ));
262
+ }
263
+
264
+ // R5: validasi tipe kolom (hanya saat soft-delete aktif dan ketiga kolom ada).
265
+ for (const col of SOFT_DELETE_COLUMNS) {
266
+ const expected = SOFT_DELETE_COLUMN_TYPES[col];
267
+ const actual = fields[col].type;
268
+ if (actual !== expected) {
269
+ throw new Error(prefix(
270
+ ir,
271
+ `soft-delete column '${col}' must be type '${expected}', got '${actual}'.`
272
+ ));
273
+ }
274
+ }
275
+
276
+ // R7 + R9: validasi tiap field reusable.
277
+ if (softDelete.reusable !== undefined) {
278
+ for (const entry of softDelete.reusable) {
279
+ validateReusableEntry(ir, entry);
280
+ }
281
+ }
282
+
283
+ // R21: tolak generate bila ada UNIQUE yang tak punya jalur reusable (composite,
284
+ // atau single-column non-string). Hanya tercapai saat enabled === true.
285
+ validateSoftDeleteUniqueReusability(ir);
286
+ }
287
+
288
+ function validateReusableEntry(ir, entry) {
289
+ const { fields, uniques } = ir;
290
+ const fieldName = entry.field;
291
+
292
+ // Entry shape: harus mereferensi field lewat nama.
293
+ if (typeof fieldName !== 'string' || fieldName === '') {
294
+ throw new Error(prefix(
295
+ ir,
296
+ `softDelete.reusable entry must have a 'field' (non-empty string), got '${fieldName}'.`
297
+ ));
298
+ }
299
+
300
+ // R9.1: field reusable harus ada di `fields`.
301
+ const field = fields[fieldName];
302
+ if (field === undefined) {
303
+ throw new Error(prefix(
304
+ ir,
305
+ `softDelete.reusable references unknown field '${fieldName}' (not declared in 'fields').`
306
+ ));
307
+ }
308
+
309
+ // R7: base length wajib dan harus integer positif.
310
+ const baseLength = entry.length;
311
+ if (baseLength === undefined || baseLength === null) {
312
+ throw new Error(prefix(
313
+ ir,
314
+ `softDelete.reusable field '${fieldName}' is missing required 'length' (the base business-code length).`
315
+ ));
316
+ }
317
+ if (typeof baseLength !== 'number' || !Number.isInteger(baseLength) || baseLength <= 0) {
318
+ throw new Error(prefix(
319
+ ir,
320
+ `softDelete.reusable field '${fieldName}' has invalid 'length' (must be a positive integer), got '${baseLength}'.`
321
+ ));
322
+ }
323
+
324
+ // R9.3: reusable harus bertipe string (varchar/text).
325
+ if (!REUSABLE_STRING_TYPES.has(field.type)) {
326
+ throw new Error(prefix(
327
+ ir,
328
+ `softDelete.reusable declares '${fieldName}' as reusable, but its column type is '${field.type}'. ` +
329
+ `Reusable only supports string types (varchar/text) because it uses value-mutation '{code}##{uuidv7}'. ` +
330
+ `Change the column type to string, or remove '${fieldName}' from reusable.`
331
+ ));
332
+ }
333
+
334
+ // R9.2: reusable harus punya UNIQUE single-column (composite ditolak).
335
+ const uniqueState = resolveUniqueState(fieldName, field, uniques);
336
+ if (uniqueState === 'none') {
337
+ throw new Error(prefix(
338
+ ir,
339
+ `softDelete.reusable field '${fieldName}' must have a single-column UNIQUE constraint, but none was found.`
340
+ ));
341
+ }
342
+ if (uniqueState === 'composite') {
343
+ throw new Error(prefix(
344
+ ir,
345
+ `softDelete.reusable field '${fieldName}' only participates in a composite UNIQUE constraint. ` +
346
+ `Reusable requires a single-column UNIQUE (a composite UNIQUE is a technical integrity rule, not a reusable business code).`
347
+ ));
348
+ }
349
+
350
+ // R7: lebar fisik kolom harus muat base length + suffix.
351
+ // Hanya berlaku untuk `string` (punya length tetap); `text` tidak terbatas panjangnya.
352
+ if (field.type === 'string') {
353
+ const required = baseLength + SOFT_DELETE_SUFFIX_LENGTH;
354
+ if (field.length < required) {
355
+ throw new Error(prefix(
356
+ ir,
357
+ `Reusable unique column '${fieldName}' is too narrow for the soft-delete suffix:\n` +
358
+ ` declared : string:${field.length}\n` +
359
+ ` required : string:${required} (base ${baseLength} + suffix ${SOFT_DELETE_SUFFIX_LENGTH})`
360
+ ));
361
+ }
362
+ }
363
+ }
364
+
365
+ // Tentukan status UNIQUE sebuah field: 'single' (single-column unique, valid untuk
366
+ // reusable), 'composite' (hanya bagian composite unique → ditolak R9.2), atau 'none'.
367
+ // Sumber unique: shorthand `unique` (field.unique === true) maupun ir.uniques.
368
+ function resolveUniqueState(fieldName, field, uniques) {
369
+ if (field.unique === true) return 'single';
370
+ let hasComposite = false;
371
+ for (const uq of uniques) {
372
+ if (!Array.isArray(uq.columns) || !uq.columns.includes(fieldName)) continue;
373
+ if (uq.columns.length === 1) return 'single';
374
+ hasComposite = true;
375
+ }
376
+ return hasComposite ? 'composite' : 'none';
377
+ }
378
+
379
+ // R21: gate pra-generate untuk soft-delete dengan UNIQUE tak ter-reusable.
380
+ //
381
+ // Mutasi nilai '{code}##{uuidv7}' yang membebaskan UNIQUE dari bentrok saat re-create
382
+ // baris HANYA berlaku untuk UNIQUE single-column bertipe string (R9.2 + R9.3). UNIQUE
383
+ // jenis lain tetap PENUH/non-partial (emitter create-index.js R10/R11 sengaja tidak
384
+ // mem-partial-kan UNIQUE: uniqueness tetap penuh), sehingga membuat ulang baris dengan
385
+ // kunci sama setelah soft-delete akan melanggar UNIQUE tanpa jalur pembebasan. Dua bentuk
386
+ // tanpa jalur reusable:
387
+ // (1) composite UNIQUE (lebih dari satu kolom);
388
+ // (2) single-column UNIQUE bertipe non-string (date/number/uuid/...).
389
+ // Keduanya ditolak di generate-time. Gate murni JS pada IR → identik untuk semua dialect
390
+ // (agnostic, deterministik); tidak menyentuh DDL/index/tipe kolom.
391
+ //
392
+ // Hanya dipanggil dari validateSoftDelete setelah blok enabled (jadi hanya saat
393
+ // softDelete.enabled === true). Throw pada pelanggaran pertama dengan urutan stabil:
394
+ // sumber A (shorthand `unique`, urut Object.keys(fields)) lalu sumber B (ir.uniques).
395
+ function validateSoftDeleteUniqueReusability(ir) {
396
+ const { fields, uniques } = ir;
397
+
398
+ // Sumber A: UNIQUE single-column via shorthand `unique` (field.unique === true).
399
+ for (const fieldName of Object.keys(fields)) {
400
+ const field = fields[fieldName];
401
+ if (field.unique !== true) continue;
402
+ if (!REUSABLE_STRING_TYPES.has(field.type)) {
403
+ throw new Error(softDeleteNonStringUniqueMessage(ir, fieldName, field.type));
404
+ }
405
+ }
406
+
407
+ // Sumber B: UNIQUE dari ir.uniques (composite maupun single-column).
408
+ for (const uq of uniques) {
409
+ const columns = Array.isArray(uq.columns) ? uq.columns : [];
410
+ if (columns.length > 1) {
411
+ throw new Error(prefix(
412
+ ir,
413
+ `softDelete.enabled = true, but the table has a composite UNIQUE (${columns.join(', ')}) ` +
414
+ `that cannot be freed by reusable value-mutation on soft-delete. ` +
415
+ `Re-creating a row with the same key after soft-delete would violate this UNIQUE. ` +
416
+ `Use hard-delete for process-driven tables (balances/ledgers/snapshots/period-close), ` +
417
+ `or make the only UNIQUE a single-column string business code.`
418
+ ));
419
+ }
420
+ if (columns.length === 1) {
421
+ const col = columns[0];
422
+ const field = fields[col];
423
+ // Eksistensi kolom sudah dijamin validateIndexAndUniqueReferences; cek defensif.
424
+ if (field !== undefined && !REUSABLE_STRING_TYPES.has(field.type)) {
425
+ throw new Error(softDeleteNonStringUniqueMessage(ir, col, field.type));
426
+ }
427
+ }
428
+ }
429
+ }
430
+
431
+ function softDeleteNonStringUniqueMessage(ir, column, type) {
432
+ return prefix(
433
+ ir,
434
+ `softDelete.enabled = true, but single-column UNIQUE '${column}' is type '${type}', ` +
435
+ `which cannot be freed by reusable value-mutation on soft-delete (reusable requires string/text). ` +
436
+ `Re-creating a row with the same value after soft-delete would violate this UNIQUE. ` +
437
+ `Use hard-delete, or change the unique key to a single-column string business code.`
438
+ );
439
+ }
440
+
210
441
  module.exports = { validateSchema };
@@ -34,10 +34,10 @@ const FORMAT_REGEX = {
34
34
  };
35
35
 
36
36
  const FORMAT_MESSAGES = {
37
- 'email': 'harus berupa alamat email yang valid',
38
- 'url': 'harus berupa URL yang valid',
39
- 'phone-id': 'harus berupa nomor telepon Indonesia yang valid',
40
- 'uuid': 'harus berupa UUID yang valid'
37
+ 'email': 'must be a valid email address',
38
+ 'url': 'must be a valid URL',
39
+ 'phone-id': 'must be a valid Indonesian phone number',
40
+ 'uuid': 'must be a valid UUID'
41
41
  };
42
42
 
43
43
  /**
@@ -89,7 +89,7 @@ function buildFieldCheckBlock(inputField, messagePrefix, required, followUpCheck
89
89
  if (required) {
90
90
  checks.push({
91
91
  condition: `input['${safeField}'] === undefined || input['${safeField}'] === null || input['${safeField}'] === ''`,
92
- message: `${messagePrefix} wajib diisi`
92
+ message: `${messagePrefix} is required`
93
93
  });
94
94
  }
95
95
  for (const check of followUpChecks) {
@@ -128,47 +128,47 @@ function buildFollowUpChecks(inputField, messagePrefix, fieldDef) {
128
128
  case 'string':
129
129
  checks.push({
130
130
  condition: `typeof input['${safeField}'] !== 'string'`,
131
- message: `${messagePrefix} harus berupa string`
131
+ message: `${messagePrefix} must be a string`
132
132
  });
133
133
  break;
134
134
  case 'number':
135
135
  checks.push({
136
136
  condition: `typeof input['${safeField}'] !== 'number' && isNaN(Number(input['${safeField}']))`,
137
- message: `${messagePrefix} harus berupa angka`
137
+ message: `${messagePrefix} must be a number`
138
138
  });
139
139
  break;
140
140
  case 'integer':
141
141
  checks.push({
142
142
  condition: `!Number.isInteger(Number(input['${safeField}']))`,
143
- message: `${messagePrefix} harus berupa bilangan bulat`
143
+ message: `${messagePrefix} must be an integer`
144
144
  });
145
145
  break;
146
146
  case 'boolean':
147
147
  checks.push({
148
148
  condition: `typeof input['${safeField}'] !== 'boolean' && input['${safeField}'] !== 'true' && input['${safeField}'] !== 'false'`,
149
- message: `${messagePrefix} harus berupa boolean`
149
+ message: `${messagePrefix} must be a boolean`
150
150
  });
151
151
  break;
152
152
  case 'uuid':
153
153
  checks.push({
154
154
  condition: `typeof input['${safeField}'] !== 'string'`,
155
- message: `${messagePrefix} harus berupa string`
155
+ message: `${messagePrefix} must be a string`
156
156
  });
157
157
  checks.push({
158
158
  condition: `!${FORMAT_REGEX.uuid}.test(input['${safeField}'])`,
159
- message: `${messagePrefix} harus berupa UUID yang valid`
159
+ message: `${messagePrefix} must be a valid UUID`
160
160
  });
161
161
  break;
162
162
  case 'array':
163
163
  checks.push({
164
164
  condition: `!Array.isArray(input['${safeField}'])`,
165
- message: `${messagePrefix} harus berupa array`
165
+ message: `${messagePrefix} must be an array`
166
166
  });
167
167
  break;
168
168
  case 'object':
169
169
  checks.push({
170
170
  condition: `typeof input['${safeField}'] !== 'object' || Array.isArray(input['${safeField}'])`,
171
- message: `${messagePrefix} harus berupa object`
171
+ message: `${messagePrefix} must be an object`
172
172
  });
173
173
  break;
174
174
  default:
@@ -193,7 +193,7 @@ function buildFollowUpChecks(inputField, messagePrefix, fieldDef) {
193
193
  const listLiteral = `[${safeValues.join(', ')}]`;
194
194
  checks.push({
195
195
  condition: `!${listLiteral}.includes(input['${safeField}'])`,
196
- message: `${messagePrefix} tidak termasuk nilai yang diizinkan`
196
+ message: `${messagePrefix} is not one of the allowed values`
197
197
  });
198
198
  }
199
199
  }
@@ -202,13 +202,13 @@ function buildFollowUpChecks(inputField, messagePrefix, fieldDef) {
202
202
  if (fieldDef && Number.isInteger(fieldDef.minLength) && fieldDef.minLength >= 0) {
203
203
  checks.push({
204
204
  condition: `typeof input['${safeField}'] === 'string' && input['${safeField}'].length < ${fieldDef.minLength}`,
205
- message: `${messagePrefix} minimal ${fieldDef.minLength} karakter`
205
+ message: `${messagePrefix} must be at least ${fieldDef.minLength} characters`
206
206
  });
207
207
  }
208
208
  if (fieldDef && Number.isInteger(fieldDef.maxLength) && fieldDef.maxLength >= 0) {
209
209
  checks.push({
210
210
  condition: `typeof input['${safeField}'] === 'string' && input['${safeField}'].length > ${fieldDef.maxLength}`,
211
- message: `${messagePrefix} maksimal ${fieldDef.maxLength} karakter`
211
+ message: `${messagePrefix} must be at most ${fieldDef.maxLength} characters`
212
212
  });
213
213
  }
214
214