@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.
Files changed (202) 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/fast-track.js +63 -43
  7. package/generators/cli/payload/generate.js +10 -2
  8. package/generators/cli/schema/apply.js +6 -1
  9. package/generators/cli/schema/diff.js +6 -1
  10. package/generators/cli/schema/introspect.js +32 -11
  11. package/generators/lib/data/db-executor.js +8 -8
  12. package/generators/lib/data/envelope.js +3 -3
  13. package/generators/lib/dbschema-kit/apply-engine.js +20 -0
  14. package/generators/lib/dbschema-kit/dialect/mysql.js +2 -0
  15. package/generators/lib/dbschema-kit/dialect/oracle.js +2 -0
  16. package/generators/lib/dbschema-kit/dialect/postgres.js +4 -0
  17. package/generators/lib/dbschema-kit/dialect/sqlite.js +5 -0
  18. package/generators/lib/dbschema-kit/diff-engine.js +22 -1
  19. package/generators/lib/dbschema-kit/diff-reporter.js +293 -272
  20. package/generators/lib/dbschema-kit/emitters/create-index.js +23 -1
  21. package/generators/lib/dbschema-kit/emitters/create-table.js +48 -0
  22. package/generators/lib/dbschema-kit/introspect-mapper.js +154 -2
  23. package/generators/lib/dbschema-kit/ir-builder.js +84 -1
  24. package/generators/lib/dbschema-kit/schema-printer.js +20 -0
  25. package/generators/lib/dbschema-kit/soft-delete-constants.js +111 -0
  26. package/generators/lib/dbschema-kit/validator/schema-validator.js +231 -0
  27. package/generators/lib/generators/processor-validation-generator.js +16 -16
  28. package/generators/lib/payload/payload-runner.js +711 -1
  29. package/generators/lib/payload/schema-diff.js +7 -0
  30. package/generators/lib/templates/dashboard-catalog.js +1 -1
  31. package/generators/lib/templates/db-connection-env.js +1 -1
  32. package/generators/lib/templates/dbschema-catalog.js +1 -1
  33. package/generators/lib/templates/field-validation-catalog.js +1 -1
  34. package/generators/lib/templates/mysql-template.js +1 -1
  35. package/generators/lib/templates/oracle-template.js +1 -1
  36. package/generators/lib/templates/postgres-template.js +1 -1
  37. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  38. package/generators/lib/templates/sqlite-template.js +1 -1
  39. package/generators/lib/utils/database-introspector.js +48 -0
  40. package/generators/lib/utils/env-manager.js +4 -4
  41. package/generators/lib/utils/file-utils.js +6 -6
  42. package/generators/lib/utils/payload-processor.js +18 -2
  43. package/generators/lib/validators/argument-validator.js +2 -2
  44. package/generators/lib/validators/dashboard-validator.js +35 -1
  45. package/generators/lib/validators/payload-validator.js +460 -33
  46. package/integrity-manifest.json +20 -20
  47. package/package.json +2 -1
  48. package/scripts/verify-integrity.js +1 -1
  49. package/server.js +1 -1
  50. package/src/components/handlers/adjust_handler.js +1 -1
  51. package/src/components/handlers/audit_handler.js +1 -1
  52. package/src/components/handlers/delete_handler.js +1 -1
  53. package/src/components/handlers/export_handler.js +1 -1
  54. package/src/components/handlers/import_handler.js +1 -1
  55. package/src/components/handlers/insert_handler.js +1 -1
  56. package/src/components/handlers/update_handler.js +1 -1
  57. package/src/components/handlers/upload_handler.js +1 -1
  58. package/src/components/handlers/workflow_handler.js +1 -1
  59. package/src/components/integrations/webhook.js +1 -1
  60. package/src/consumers/baseConsumer.js +1 -1
  61. package/src/consumers/declarativeMapper.js +1 -1
  62. package/src/consumers/handlers/apiHandler.js +1 -1
  63. package/src/consumers/handlers/consoleHandler.js +1 -1
  64. package/src/consumers/handlers/databaseHandler.js +1 -1
  65. package/src/consumers/handlers/index.js +1 -1
  66. package/src/consumers/handlers/kafkaHandler.js +1 -1
  67. package/src/consumers/index.js +1 -1
  68. package/src/consumers/messageTransformer.js +1 -1
  69. package/src/consumers/validator.js +1 -1
  70. package/src/core/db/dialect/base-dialect.js +1 -1
  71. package/src/core/db/dialect/index.js +1 -1
  72. package/src/core/db/dialect/mysql-dialect.js +1 -1
  73. package/src/core/db/dialect/oracle-dialect.js +1 -1
  74. package/src/core/db/dialect/postgres-dialect.js +1 -1
  75. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  76. package/src/core/db/flatten-helper.js +1 -1
  77. package/src/core/db/query-builder-error.js +1 -1
  78. package/src/core/db/query-builder.js +1 -1
  79. package/src/core/db/relation-helper.js +1 -1
  80. package/src/core/handlers/delete_handler.js +1 -1
  81. package/src/core/handlers/insert_handler.js +1 -1
  82. package/src/core/handlers/update_handler.js +1 -1
  83. package/src/core/models/base-model.js +1 -1
  84. package/src/core/utils/cache-manager.js +1 -1
  85. package/src/core/utils/component-engine.js +1 -1
  86. package/src/core/utils/context-builder.js +1 -1
  87. package/src/core/utils/datetime-formatter.js +1 -1
  88. package/src/core/utils/datetime-parser.js +1 -1
  89. package/src/core/utils/db.js +1 -1
  90. package/src/core/utils/logger.js +1 -1
  91. package/src/core/utils/payload-loader.js +1 -1
  92. package/src/core/utils/security-checks.js +1 -1
  93. package/src/middleware/body-options.js +1 -1
  94. package/src/middleware/cors.js +1 -1
  95. package/src/middleware/idempotency.js +1 -1
  96. package/src/middleware/rate-limiter.js +1 -1
  97. package/src/middleware/request-logger.js +1 -1
  98. package/src/middleware/security-headers.js +1 -1
  99. package/src/models/base-model-mysql.js +1 -1
  100. package/src/models/base-model-oracle.js +1 -1
  101. package/src/models/base-model-sqlite.js +1 -1
  102. package/src/models/base-model.js +1 -1
  103. package/src/pro/caching/redis-client.js +1 -1
  104. package/src/pro/caching/redis-helper.js +1 -1
  105. package/src/pro/consumers/baseConsumer.js +1 -1
  106. package/src/pro/consumers/declarativeMapper.js +1 -1
  107. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  108. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  109. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  110. package/src/pro/consumers/handlers/index.js +1 -1
  111. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  112. package/src/pro/consumers/index.js +1 -1
  113. package/src/pro/consumers/messageTransformer.js +1 -1
  114. package/src/pro/consumers/validator.js +1 -1
  115. package/src/pro/database/base-model-mysql.js +1 -1
  116. package/src/pro/database/base-model-oracle.js +1 -1
  117. package/src/pro/database/base-model-sqlite.js +1 -1
  118. package/src/pro/database/db-mysql.js +1 -1
  119. package/src/pro/database/db-oracle.js +1 -1
  120. package/src/pro/database/db-sqlite.js +1 -1
  121. package/src/pro/excel/excel-generator.js +1 -1
  122. package/src/pro/excel/excel-parser.js +1 -1
  123. package/src/pro/excel/export-service.js +1 -1
  124. package/src/pro/excel/export_handler.js +1 -1
  125. package/src/pro/excel/import-service.js +1 -1
  126. package/src/pro/excel/import-validator.js +1 -1
  127. package/src/pro/excel/import_handler.js +1 -1
  128. package/src/pro/excel/upsert-builder.js +1 -1
  129. package/src/pro/idgen/idgen-routes.js +1 -1
  130. package/src/pro/integrations/lookup-resolver.js +1 -1
  131. package/src/pro/integrations/upload-handler-v2.js +1 -1
  132. package/src/pro/integrations/upload-handler.js +1 -1
  133. package/src/pro/integrations/webhook.js +1 -1
  134. package/src/pro/locking/lock-routes.js +1 -1
  135. package/src/pro/locking/resource-lock-manager.js +1 -1
  136. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  137. package/src/pro/messaging/kafkaService.js +1 -1
  138. package/src/pro/messaging/messagehubService.js +1 -1
  139. package/src/pro/messaging/rabbitmqService.js +1 -1
  140. package/src/pro/scheduler/job-manager.js +1 -1
  141. package/src/pro/scheduler/job-routes.js +1 -1
  142. package/src/pro/scheduler/job-validator.js +1 -1
  143. package/src/pro/storage/base-storage-provider.js +1 -1
  144. package/src/pro/storage/file-metadata-helper.js +1 -1
  145. package/src/pro/storage/index.js +1 -1
  146. package/src/pro/storage/local-storage-provider.js +1 -1
  147. package/src/pro/storage/s3-storage-provider.js +1 -1
  148. package/src/pro/storage/upload-cleanup-job.js +1 -1
  149. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  150. package/src/pro/storage/upload-pending-tracker.js +1 -1
  151. package/src/pro/websocket/broadcast-helper.js +1 -1
  152. package/src/pro/websocket/index.js +1 -1
  153. package/src/pro/websocket/livesync-server.js +1 -1
  154. package/src/pro/websocket/ws-broadcaster.js +1 -1
  155. package/src/services/export-service.js +1 -1
  156. package/src/services/import-service.js +1 -1
  157. package/src/services/kafkaConsumerService.js +1 -1
  158. package/src/services/kafkaService.js +1 -1
  159. package/src/services/messagehubService.js +1 -1
  160. package/src/services/rabbitmqService.js +1 -1
  161. package/src/utils/cache-invalidation-registry.js +1 -1
  162. package/src/utils/cache-manager.js +1 -1
  163. package/src/utils/component-engine.js +1 -1
  164. package/src/utils/config-extractor.js +1 -1
  165. package/src/utils/consumerLogger.js +1 -1
  166. package/src/utils/context-builder.js +1 -1
  167. package/src/utils/dashboard-helpers.js +1 -1
  168. package/src/utils/dateHelper.js +1 -1
  169. package/src/utils/datetime-formatter.js +1 -1
  170. package/src/utils/datetime-parser.js +1 -1
  171. package/src/utils/db-bootstrap.js +1 -1
  172. package/src/utils/db-mysql.js +1 -1
  173. package/src/utils/db-oracle.js +1 -1
  174. package/src/utils/db-sqlite.js +1 -1
  175. package/src/utils/db.js +1 -1
  176. package/src/utils/demo-generator.js +1 -1
  177. package/src/utils/excel-generator.js +1 -1
  178. package/src/utils/excel-parser.js +1 -1
  179. package/src/utils/file-watcher.js +1 -1
  180. package/src/utils/id-generator.js +1 -1
  181. package/src/utils/idempotency-manager.js +1 -1
  182. package/src/utils/import-validator.js +1 -1
  183. package/src/utils/license-client.js +1 -1
  184. package/src/utils/lock-manager.js +1 -1
  185. package/src/utils/logger.js +1 -1
  186. package/src/utils/lookup-resolver.js +1 -1
  187. package/src/utils/payload-loader.js +1 -1
  188. package/src/utils/processor-response.js +1 -1
  189. package/src/utils/rabbitmq.js +1 -1
  190. package/src/utils/redis-client.js +1 -1
  191. package/src/utils/redis-helper.js +1 -1
  192. package/src/utils/request-scope.js +1 -1
  193. package/src/utils/security-checks.js +1 -1
  194. package/src/utils/service-resolver.js +1 -1
  195. package/src/utils/shutdown-coordinator.js +1 -1
  196. package/src/utils/soft-delete-dashboard-guard.js +1 -0
  197. package/src/utils/sql-table-extractor.js +1 -0
  198. package/src/utils/trusted-keys.js +1 -1
  199. package/src/utils/upload-handler.js +1 -1
  200. package/src/utils/upsert-builder.js +1 -1
  201. package/src/utils/workflow-hook-executor.js +1 -1
  202. package/generators/lib/utils/sql-table-extractor.js +0 -83
@@ -28,6 +28,11 @@ const http = require('node:http');
28
28
  const readline = require('node:readline');
29
29
  const { spawnSync } = require('node:child_process');
30
30
 
31
+ // Loader SDF yang SAMA dengan `schema migrate`. Bersifat rekursif sehingga
32
+ // struktur per-schema (mis. `schema/public/visitors.js`) ikut terbaca, dan
33
+ // mengembalikan IR model dengan relasi ter-normalisasi (sumber FK yang andal).
34
+ const { loadSchemaPath } = require('../lib/dbschema-kit/loader');
35
+
31
36
  // ---------------------------------------------------------------------------
32
37
  // Default konfigurasi (selaras dengan restforge-playbook/fast-track.mjs)
33
38
  // ---------------------------------------------------------------------------
@@ -207,9 +212,11 @@ const FALLBACK_TABLES = [
207
212
  ];
208
213
 
209
214
  /**
210
- * Mengumpulkan nama tabel dari folder schema bila ada; bila tidak, memakai
215
+ * Mengumpulkan nama tabel dari folder schema bila ada (rekursif, termasuk
216
+ * subfolder per schema seperti `schema/public/*.js`); bila tidak, memakai
211
217
  * daftar fallback yang dipadkan sampai 30 entri agar contoh kanonik (30 tabel)
212
- * tetap dapat dirender.
218
+ * tetap dapat dirender. Nama yang dikumpulkan adalah kebab tabel agar count
219
+ * preview konsisten dengan file payload/endpoint yang akan dibuat.
213
220
  */
214
221
  function collectTables(schemaDir) {
215
222
  let names = [];
@@ -217,10 +224,7 @@ function collectTables(schemaDir) {
217
224
 
218
225
  try {
219
226
  if (schemaDir && fs.existsSync(schemaDir) && fs.statSync(schemaDir).isDirectory()) {
220
- names = fs.readdirSync(schemaDir)
221
- .filter((f) => f.endsWith('.js') && !f.startsWith('_') && !f.endsWith('.test.js'))
222
- .map((f) => f.slice(0, -3))
223
- .sort();
227
+ names = loadModels(schemaDir).map((m) => m.kebab).sort();
224
228
  if (names.length > 0) realSchema = true;
225
229
  }
226
230
  } catch (_err) {
@@ -497,48 +501,64 @@ function ensureEnv(ctx) {
497
501
  const AUDIT_COLS = new Set(['created_at', 'created_by', 'updated_at', 'updated_by']);
498
502
 
499
503
  /**
500
- * Parse satu file SDF menjadi descriptor model. Deterministik dari SDF (tanpa DB):
501
- * { kebab, table, pk, fields[{name,isPk,isAudit,fk}], fks[{childCol,parentTable,parentCol}], displayCols }
502
- * displayCols = kolom yang mengandung `code`/`name`, di luar PK & kolom audit
503
- * (dipakai sebagai kolom display saat tabel ini menjadi parent FK).
504
+ * Turunkan kebab dari NAMA TABEL (bukan nama file) dengan normalisasi
505
+ * `[._]` -> `-`, identik dengan penamaan file output `payload generate`
506
+ * (payload-runner: baseFilename = table.replace(/[._]/g, '-')). Dengan ini
507
+ * `endpoint create --payload=<kebab>.json` dan `payload migrate --name=<kebab>.json`
508
+ * menemukan file payload yang benar, baik untuk SDF bernama underscore (hasil
509
+ * `schema introspect`, mis. `visitor_categories.js`) maupun kebab.
510
+ */
511
+ function tableToKebab(table) {
512
+ return table.replace(/[._]/g, '-');
513
+ }
514
+
515
+ /**
516
+ * Muat seluruh SDF di folder memakai loader dbschema-kit yang SAMA dengan
517
+ * `schema migrate`. Loader bersifat REKURSIF sehingga struktur per-schema
518
+ * (mis. `schema/public/visitors.js`) ikut terbaca; flat readdir lama hanya
519
+ * membaca top-level sehingga subfolder schema tidak dikenali.
520
+ *
521
+ * Mengembalikan descriptor per tabel (deterministik dari SDF, tanpa DB):
522
+ * { kebab, table, fks[{childCol,parentTable,parentCol}], displayCols }
523
+ *
524
+ * FK dibaca dari `relations` (belongsTo) pada IR model. IR menormalkan KEDUA
525
+ * gaya format: blok `relations:` eksplisit (gaya `schema introspect`) DAN inline
526
+ * `fk:<parent>.<col>` pada field (di-auto-promote ke relations oleh ir-builder).
527
+ * displayCols = kolom code/name di luar PK & kolom audit, dipakai sebagai kolom
528
+ * display saat tabel ini menjadi parent FK (untuk `payload sync --expand-fk`).
504
529
  */
505
- function parseModel(absFile, kebab) {
506
- let content = '';
507
- try { content = fs.readFileSync(absFile, 'utf8'); } catch (_err) { /* kosong */ }
508
- const table = (content.match(/defineModel\(\s*['"]([^'"]+)['"]/) || [, kebab.replace(/-/g, '_')])[1];
509
- const block = (content.match(/fields\s*:\s*\{([\s\S]*?)\}/) || [, ''])[1];
510
-
511
- const fields = [];
512
- const re = /(\w+)\s*:\s*'([^']*)'/g;
513
- let m;
514
- while ((m = re.exec(block)) !== null) {
515
- const name = m[1];
516
- const spec = m[2];
517
- const fkMatch = spec.match(/fk:(\w+)\.(\w+)/);
518
- fields.push({
519
- name,
520
- isPk: /\bpk\b/.test(spec),
521
- isAudit: AUDIT_COLS.has(name),
522
- fk: fkMatch ? { parentTable: fkMatch[1], parentCol: fkMatch[2] } : null
530
+ function loadModels(schemaDir) {
531
+ const models = loadSchemaPath(schemaDir);
532
+ const entries = [];
533
+ for (const model of models.values()) {
534
+ const table = model.tableName;
535
+ const primaryKey = Array.isArray(model.primaryKey) ? model.primaryKey : [];
536
+
537
+ const fks = [];
538
+ for (const rel of Object.values(model.relations || {})) {
539
+ if (rel.type !== 'belongsTo' || !rel.localKey || !rel.target) continue;
540
+ fks.push({
541
+ childCol: rel.localKey,
542
+ parentTable: rel.target,
543
+ parentCol: rel.references || null
544
+ });
545
+ }
546
+
547
+ const displayCols = Object.keys(model.fields || {}).filter((name) => {
548
+ const field = model.fields[name];
549
+ const isPk = (field && field.pk === true) || primaryKey.includes(name);
550
+ return !isPk && !AUDIT_COLS.has(name) && /(code|name)/i.test(name);
523
551
  });
524
- }
525
- const pk = (fields.find((f) => f.isPk) || {}).name || null;
526
- const displayCols = fields
527
- .filter((f) => !f.isPk && !f.isAudit && /(code|name)/i.test(f.name))
528
- .map((f) => f.name);
529
- const fks = fields
530
- .filter((f) => f.fk)
531
- .map((f) => ({ childCol: f.name, parentTable: f.fk.parentTable, parentCol: f.fk.parentCol }));
532
552
 
533
- return { kebab, table, pk, fields, fks, displayCols };
553
+ entries.push({ kebab: tableToKebab(table), table, fks, displayCols });
554
+ }
555
+ entries.sort((a, b) => a.table.localeCompare(b.table));
556
+ return entries;
534
557
  }
535
558
 
536
- /** Bangun daftar model dari folder schema. */
559
+ /** Bangun daftar model dari folder schema (rekursif, via loader bersama). */
537
560
  function buildTableEntries(schemaDir) {
538
- const files = fs.readdirSync(schemaDir)
539
- .filter((f) => f.endsWith('.js') && !f.startsWith('_') && !f.endsWith('.test.js'))
540
- .sort();
541
- return files.map((f) => parseModel(path.join(schemaDir, f), f.slice(0, -3)));
561
+ return loadModels(schemaDir);
542
562
  }
543
563
 
544
564
  /**
@@ -946,5 +966,5 @@ module.exports = {
946
966
  // terpisah tanpa menjalankan pipeline penuh. Pada penggunaan CLI normal env ini
947
967
  // tidak di-set sehingga export tetap berupa contract murni.
948
968
  if (process.env.FASTTRACK_TEST === '1') {
949
- module.exports.__test = { parseModel, buildTableEntries, fkColumnsForEntry };
969
+ module.exports.__test = { loadModels, buildTableEntries, fkColumnsForEntry, tableToKebab };
950
970
  }
@@ -36,18 +36,26 @@ module.exports = {
36
36
  required: false,
37
37
  default: null,
38
38
  description: 'Output directory untuk file payload yang di-generate (default: payload/)'
39
+ },
40
+ 'schema-path': {
41
+ type: 'string',
42
+ required: false,
43
+ default: 'schema',
44
+ description: 'Lokasi SDF (file atau folder) sumber metadata tabel (default: folder `schema`). WAJIB memuat tabel bila tabel memiliki kolom soft-delete (is_deleted/deleted_at/deleted_by): blok softDelete RDF diturunkan dari SDF (R12/R13). Diabaikan untuk tabel non-soft-delete.'
39
45
  }
40
46
  },
41
47
  examples: [
42
48
  'npx restforge payload generate --config=db.env --table=users',
43
- 'npx restforge payload generate --config=db.env --table=users --output=./my-payloads'
49
+ 'npx restforge payload generate --config=db.env --table=users --output=./my-payloads',
50
+ 'npx restforge payload generate --config=db.env --table=visitor_categories --schema-path=./schema/visitor-categories.js'
44
51
  ],
45
52
  async handler(args) {
46
53
  const generator = new PayloadGenerator();
47
54
  await generator.run({
48
55
  config: args.config,
49
56
  table: args.table,
50
- output: args.output || null
57
+ output: args.output || null,
58
+ 'schema-path': args['schema-path']
51
59
  });
52
60
  }
53
61
  };
@@ -80,6 +80,7 @@ async function collectTableMeta(introspector, target) {
80
80
  const constraints = await introspector.getConstraints(ref);
81
81
  const foreignKeys = await introspector.getForeignKeys(ref);
82
82
  const indexes = await introspector.getIndexes(ref);
83
+ const checks = await introspector.getCheckConstraints(ref);
83
84
 
84
85
  const pkConstraint = constraints.find((c) => c.type === 'PRIMARY KEY');
85
86
  const uniqueConstraints = constraints
@@ -94,7 +95,11 @@ async function collectTableMeta(introspector, target) {
94
95
  uniques: uniqueConstraints,
95
96
  foreignKeys,
96
97
  indexes,
97
- checks: []
98
+ // introspect-mapper expects { name, expression }; getCheckConstraints
99
+ // returns { name, clause } (D5 wiring, Phase 03).
100
+ checks: Array.isArray(checks)
101
+ ? checks.map((c) => ({ name: c.name, expression: c.clause }))
102
+ : []
98
103
  };
99
104
  }
100
105
 
@@ -69,6 +69,7 @@ async function collectTableMeta(introspector, target) {
69
69
  const constraints = await introspector.getConstraints(ref);
70
70
  const foreignKeys = await introspector.getForeignKeys(ref);
71
71
  const indexes = await introspector.getIndexes(ref);
72
+ const checks = await introspector.getCheckConstraints(ref);
72
73
 
73
74
  const pkConstraint = constraints.find((c) => c.type === 'PRIMARY KEY');
74
75
  const uniqueConstraints = constraints
@@ -83,7 +84,11 @@ async function collectTableMeta(introspector, target) {
83
84
  uniques: uniqueConstraints,
84
85
  foreignKeys,
85
86
  indexes,
86
- checks: []
87
+ // introspect-mapper expects { name, expression }; getCheckConstraints
88
+ // returns { name, clause } (D5 wiring, Phase 03).
89
+ checks: Array.isArray(checks)
90
+ ? checks.map((c) => ({ name: c.name, expression: c.clause }))
91
+ : []
87
92
  };
88
93
  }
89
94
 
@@ -146,6 +146,7 @@ async function collectTableMeta(introspector, target) {
146
146
  const constraints = await introspector.getConstraints(ref);
147
147
  const foreignKeys = await introspector.getForeignKeys(ref);
148
148
  const indexes = await introspector.getIndexes(ref);
149
+ const checks = await introspector.getCheckConstraints(ref);
149
150
 
150
151
  const pkConstraint = constraints.find((c) => c.type === 'PRIMARY KEY');
151
152
  const uniqueConstraints = constraints
@@ -160,7 +161,11 @@ async function collectTableMeta(introspector, target) {
160
161
  uniques: uniqueConstraints,
161
162
  foreignKeys,
162
163
  indexes,
163
- checks: []
164
+ // introspect-mapper expects { name, expression }; getCheckConstraints
165
+ // returns { name, clause } (D5 wiring, Phase 03).
166
+ checks: Array.isArray(checks)
167
+ ? checks.map((c) => ({ name: c.name, expression: c.clause }))
168
+ : []
164
169
  };
165
170
  }
166
171
 
@@ -406,16 +411,32 @@ module.exports = {
406
411
  }
407
412
 
408
413
  const dialect = config.dialect;
409
- const emitted = result.tables.map((tableMeta) => {
410
- const ir = mapTableMetaToIR(tableMeta, dialect);
411
- const source = serialize(ir);
412
- return {
413
- tableName: ir.tableName,
414
- schemaName: ir.schemaName,
415
- qualifiedName: ir.qualifiedName,
416
- source
417
- };
418
- });
414
+ let emitted;
415
+ try {
416
+ emitted = result.tables.map((tableMeta) => {
417
+ // strictSoftDelete: introspect ERROR (R6) bila kolom soft-delete tidak
418
+ // lengkap / tipe salah / CHECK konsistensi hilang, dan tidak menghasilkan SDF.
419
+ const ir = mapTableMetaToIR(tableMeta, dialect, { strictSoftDelete: true });
420
+ const source = serialize(ir);
421
+ return {
422
+ tableName: ir.tableName,
423
+ schemaName: ir.schemaName,
424
+ qualifiedName: ir.qualifiedName,
425
+ source
426
+ };
427
+ });
428
+ } catch (err) {
429
+ if (err && err.isSoftDeleteIntrospectError) {
430
+ // Pesan multi-baris (R6/R8) sudah lengkap; print apa adanya lalu rethrow
431
+ // silent agar cli-entry tidak menambah prefix "Error: " yang merusak format.
432
+ console.error(err.message);
433
+ const e = new Error(err.message);
434
+ e.silent = true;
435
+ e.exitCode = 1;
436
+ throw e;
437
+ }
438
+ throw err;
439
+ }
419
440
 
420
441
  const schemaSet = new Set();
421
442
  for (const item of emitted) {
@@ -38,33 +38,33 @@ function loadDriver(dialect) {
38
38
  case 'postgresql':
39
39
  if (!pgDriver) {
40
40
  try { pgDriver = require('pg'); } catch (_e) {
41
- throw new Error('PostgreSQL driver (pg) tidak terpasang. Jalankan: npm install pg');
41
+ throw new Error('PostgreSQL driver (pg) is not installed. Run: npm install pg');
42
42
  }
43
43
  }
44
44
  return pgDriver;
45
45
  case 'mysql':
46
46
  if (!mysqlDriver) {
47
47
  try { mysqlDriver = require('mysql2/promise'); } catch (_e) {
48
- throw new Error('MySQL driver (mysql2) tidak terpasang. Jalankan: npm install mysql2');
48
+ throw new Error('MySQL driver (mysql2) is not installed. Run: npm install mysql2');
49
49
  }
50
50
  }
51
51
  return mysqlDriver;
52
52
  case 'oracle':
53
53
  if (!oracleDriver) {
54
54
  try { oracleDriver = require('oracledb'); } catch (_e) {
55
- throw new Error('Oracle driver (oracledb) tidak terpasang. Jalankan: npm install oracledb');
55
+ throw new Error('Oracle driver (oracledb) is not installed. Run: npm install oracledb');
56
56
  }
57
57
  }
58
58
  return oracleDriver;
59
59
  case 'sqlite':
60
60
  if (!sqliteDriver) {
61
61
  try { sqliteDriver = require('better-sqlite3'); } catch (_e) {
62
- throw new Error('SQLite driver (better-sqlite3) tidak terpasang. Jalankan: npm rebuild better-sqlite3');
62
+ throw new Error('SQLite driver (better-sqlite3) is not installed. Run: npm rebuild better-sqlite3');
63
63
  }
64
64
  }
65
65
  return sqliteDriver;
66
66
  default:
67
- throw new Error(`Dialect tidak didukung: ${dialect}. Didukung: postgresql, mysql, oracle, sqlite`);
67
+ throw new Error(`Unsupported dialect: ${dialect}. Supported: postgresql, mysql, oracle, sqlite`);
68
68
  }
69
69
  }
70
70
 
@@ -362,7 +362,7 @@ function createSqliteExecutor(config) {
362
362
  const Database = loadDriver('sqlite');
363
363
  const filePath = config.file || config.database;
364
364
  if (!filePath) {
365
- throw new Error('SQLite: path file database wajib ada di config (field "file" atau "database")');
365
+ throw new Error('SQLite: the database file path is required in config (field "file" or "database")');
366
366
  }
367
367
  let db = null;
368
368
 
@@ -419,7 +419,7 @@ function createSqliteExecutor(config) {
419
419
  */
420
420
  function createExecutor(config) {
421
421
  if (!config || typeof config !== 'object') {
422
- throw new Error('createExecutor: config wajib berupa object hasil loadConfig');
422
+ throw new Error('createExecutor: config must be an object produced by loadConfig');
423
423
  }
424
424
  const dialect = normalizeDialect(config.dialect);
425
425
  switch (dialect) {
@@ -428,7 +428,7 @@ function createExecutor(config) {
428
428
  case 'oracle': return createOracleExecutor(config);
429
429
  case 'sqlite': return createSqliteExecutor(config);
430
430
  default:
431
- throw new Error(`createExecutor: dialect tidak didukung '${config.dialect}'`);
431
+ throw new Error(`createExecutor: unsupported dialect '${config.dialect}'`);
432
432
  }
433
433
  }
434
434
 
@@ -55,7 +55,7 @@ function resolveSink(sink) {
55
55
  if (typeof sink.write === 'function') {
56
56
  return { write: (chunk) => sink.write(chunk), buffer: null };
57
57
  }
58
- throw new Error('EnvelopeWriter: sink harus berupa fungsi, object dengan method write(), atau undefined');
58
+ throw new Error('EnvelopeWriter: sink must be a function, an object with a write() method, or undefined');
59
59
  }
60
60
 
61
61
  /**
@@ -116,7 +116,7 @@ class EnvelopeWriter {
116
116
  */
117
117
  appendRows(rows) {
118
118
  if (this._state !== 'header') {
119
- throw new Error('EnvelopeWriter: appendRows() harus dipanggil setelah writeHeader() dan sebelum end()');
119
+ throw new Error('EnvelopeWriter: appendRows() must be called after writeHeader() and before end()');
120
120
  }
121
121
  if (!Array.isArray(rows)) {
122
122
  throw new Error('EnvelopeWriter: appendRows() membutuhkan array rows');
@@ -138,7 +138,7 @@ class EnvelopeWriter {
138
138
  throw new Error('EnvelopeWriter: end() dipanggil sebelum writeHeader()');
139
139
  }
140
140
  if (this._state === 'ended') {
141
- throw new Error('EnvelopeWriter: end() sudah dipanggil');
141
+ throw new Error('EnvelopeWriter: end() has already been called');
142
142
  }
143
143
  this._write(']}');
144
144
  this._state = 'ended';
@@ -645,6 +645,26 @@ function noteDeferredSections(delta, skipped) {
645
645
  });
646
646
  }
647
647
  }
648
+
649
+ // Soft-delete CHECK drift — Detection-only di Fase 1 (TIDAK ada emitter ALTER ADD
650
+ // CHECK). apply tidak meng-auto-retrofit; cukup laporkan + rekomendasi recreate/manual.
651
+ if (delta.softDelete && delta.softDelete.match === false) {
652
+ const sd = delta.softDelete;
653
+ const description = (sd.sdf && !sd.db)
654
+ ? `Soft-delete consistency CHECK missing in DB for '${delta.tableName}'. ` +
655
+ `Detection-only in Phase 1 (no ALTER ADD CHECK): retrofit via ` +
656
+ `'schema migrate --drop' (recreate) or add the CHECK manually.`
657
+ : `Soft-delete state mismatch for '${delta.tableName}' ` +
658
+ `(DB has the consistency CHECK, SDF does not declare softDelete.enabled). ` +
659
+ `Detection-only: reconcile SDF/DB manually.`;
660
+ skipped.push({
661
+ table: delta.tableName,
662
+ kind: 'soft-delete-check',
663
+ target: 'soft_delete_consistency',
664
+ reason: 'detection-only',
665
+ description
666
+ });
667
+ }
648
668
  }
649
669
 
650
670
  // ─────────────────────────────────────────────────────────────
@@ -84,6 +84,8 @@ module.exports = {
84
84
  maxIdentifierLength: 64,
85
85
  // Boolean disimpan sebagai VARCHAR(5) 'true'/'false'; bukan native.
86
86
  nativeBoolean: false,
87
+ // MySQL tidak punya partial index efisien; index soft-delete jadi plain (R11).
88
+ supportsPartialIndex: false,
87
89
  mapType,
88
90
  formatDefault,
89
91
  quoteIdentifier,
@@ -89,6 +89,8 @@ module.exports = {
89
89
  maxIdentifierLength: 128,
90
90
  // Boolean disimpan sebagai VARCHAR2(5) 'true'/'false'; bukan native.
91
91
  nativeBoolean: false,
92
+ // Oracle tidak punya partial index efisien; index soft-delete jadi plain (R11).
93
+ supportsPartialIndex: false,
92
94
  mapType,
93
95
  formatDefault,
94
96
  quoteIdentifier,
@@ -84,6 +84,10 @@ module.exports = {
84
84
  // PostgreSQL punya tipe BOOLEAN native; dialect lain menyimpan boolean
85
85
  // sebagai VARCHAR 'true'/'false' dan butuh CHECK sebagai penanda boolean.
86
86
  nativeBoolean: true,
87
+ // PostgreSQL mendukung partial index (`CREATE INDEX ... WHERE <predikat>`).
88
+ // Dipakai R10/R11: index non-unique pada tabel soft-delete jadi partial
89
+ // (`WHERE is_deleted = FALSE`).
90
+ supportsPartialIndex: true,
87
91
  mapType,
88
92
  formatDefault,
89
93
  quoteIdentifier,
@@ -87,6 +87,11 @@ module.exports = {
87
87
  maxIdentifierLength: 63,
88
88
  // Boolean disimpan sebagai VARCHAR(5) 'true'/'false'; bukan native.
89
89
  nativeBoolean: false,
90
+ // Flag ini menandai apakah index non-unique soft-delete dimaterialisasi sebagai
91
+ // partial index (R10/R11). Per plan master R11, SQLite masuk grup plain index
92
+ // (bersama MySQL/Oracle), jadi false. (Soft-delete Fase 1 juga PostgreSQL-only via
93
+ // guard di emitter, sehingga jalur ini belum aktif untuk SQLite.)
94
+ supportsPartialIndex: false,
90
95
  mapType,
91
96
  formatDefault,
92
97
  quoteIdentifier,
@@ -588,6 +588,24 @@ function diffChecks(sdfChecks, dbChecks) {
588
588
  return { onlyInSdf, onlyInDb, mismatched };
589
589
  }
590
590
 
591
+ // ─────────────────────────────────────────────────────────────
592
+ // Soft-delete comparison (R8 poin 2 / R6 reverse)
593
+ // ─────────────────────────────────────────────────────────────
594
+
595
+ // Bandingkan status soft-delete kedua IR. CHECK konsistensi soft-delete bersifat
596
+ // IMPLISIT (diturunkan dari `softDelete.enabled`, tidak pernah di-list di `checks`).
597
+ // introspect-mapper men-set `dbIR.softDelete = { enabled: true }` HANYA bila tabel DB
598
+ // valid (3 kolom + tipe benar + CHECK ada by-nama), dan mengecualikan CHECK soft-delete
599
+ // dari `checks` di kedua sisi. Maka:
600
+ // - enabled di kedua sisi → CHECK implisit expected di keduanya → tidak ada drift.
601
+ // - SDF enabled tetapi DB tidak (CHECK absen / kolom-tipe tak valid) → drift NYATA
602
+ // (Detection-only; dilaporkan apa adanya, bukan disembunyikan, bukan di-auto-fix).
603
+ function diffSoftDelete(sdfIR, dbIR) {
604
+ const sdf = !!(sdfIR && sdfIR.softDelete && sdfIR.softDelete.enabled === true);
605
+ const db = !!(dbIR && dbIR.softDelete && dbIR.softDelete.enabled === true);
606
+ return { sdf, db, match: sdf === db };
607
+ }
608
+
591
609
  // ─────────────────────────────────────────────────────────────
592
610
  // Top-level API
593
611
  // ─────────────────────────────────────────────────────────────
@@ -610,6 +628,7 @@ function hasAnyDrift(delta) {
610
628
  if (delta.checks && delta.checks.onlyInSdf.length > 0) return true;
611
629
  if (delta.checks && delta.checks.onlyInDb.length > 0) return true;
612
630
  if (delta.checks && delta.checks.mismatched.length > 0) return true;
631
+ if (delta.softDelete && delta.softDelete.match === false) return true;
613
632
  return false;
614
633
  }
615
634
 
@@ -631,7 +650,8 @@ function diffModel(sdfIR, dbIR) {
631
650
  indexes: diffIndexes(sdfIR.indexes, dbIR.indexes),
632
651
  uniques: diffUniques(sdfIR.uniques, dbIR.uniques),
633
652
  foreignKeys: diffForeignKeys(sdfIR, dbIR),
634
- checks: diffChecks(sdfIR.checks, dbIR.checks)
653
+ checks: diffChecks(sdfIR.checks, dbIR.checks),
654
+ softDelete: diffSoftDelete(sdfIR, dbIR)
635
655
  };
636
656
  delta.hasDrift = hasAnyDrift(delta);
637
657
 
@@ -706,6 +726,7 @@ module.exports = {
706
726
  diffForeignKeys,
707
727
  extractForeignKeys,
708
728
  diffChecks,
729
+ diffSoftDelete,
709
730
  compareDefaultValue,
710
731
  canonicalDefaultPayload,
711
732
  normalizeWeakDefault,