@restforgejs/platform 4.1.1 → 4.2.8

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 (178) hide show
  1. package/bin/sdf-tools.exe +0 -0
  2. package/build-info.json +2 -2
  3. package/cli/consumer-deploy.js +1 -1
  4. package/cli/consumer.js +1 -1
  5. package/generators/cli/endpoint/create.js +42 -3
  6. package/generators/cli/schema/apply.js +525 -0
  7. package/generators/cli/schema/diff.js +321 -0
  8. package/generators/cli/schema/generate-ddl.js +7 -10
  9. package/generators/cli/schema/init.js +95 -172
  10. package/generators/cli/schema/migrate.js +10 -16
  11. package/generators/cli/schema/models.js +8 -12
  12. package/generators/cli/schema/template.js +222 -0
  13. package/generators/cli/schema/validate.js +8 -12
  14. package/generators/cli-entry.js +17 -2
  15. package/generators/lib/dbschema-kit/apply-engine.js +582 -0
  16. package/generators/lib/dbschema-kit/diff-engine.js +703 -0
  17. package/generators/lib/dbschema-kit/diff-reporter.js +272 -0
  18. package/generators/lib/dbschema-kit/emitters/alter-table.js +275 -0
  19. package/generators/lib/payload/endpoint-schema-validator.js +171 -0
  20. package/generators/lib/payload/payload-runner.js +137 -220
  21. package/generators/lib/payload/schema-diff.js +277 -0
  22. package/generators/lib/utils/audit-columns.js +181 -0
  23. package/generators/lib/utils/cli-output.js +17 -0
  24. package/generators/lib/utils/database-introspector.js +16 -13
  25. package/integrity-manifest.json +8 -8
  26. package/package.json +1 -1
  27. package/scripts/verify-integrity.js +1 -1
  28. package/server.js +1 -1
  29. package/src/components/handlers/adjust_handler.js +1 -1
  30. package/src/components/handlers/audit_handler.js +1 -1
  31. package/src/components/handlers/delete_handler.js +1 -1
  32. package/src/components/handlers/export_handler.js +1 -1
  33. package/src/components/handlers/import_handler.js +1 -1
  34. package/src/components/handlers/insert_handler.js +1 -1
  35. package/src/components/handlers/update_handler.js +1 -1
  36. package/src/components/handlers/upload_handler.js +1 -1
  37. package/src/components/handlers/workflow_handler.js +1 -1
  38. package/src/components/integrations/webhook.js +1 -1
  39. package/src/consumers/baseConsumer.js +1 -1
  40. package/src/consumers/declarativeMapper.js +1 -1
  41. package/src/consumers/handlers/apiHandler.js +1 -1
  42. package/src/consumers/handlers/consoleHandler.js +1 -1
  43. package/src/consumers/handlers/databaseHandler.js +1 -1
  44. package/src/consumers/handlers/index.js +1 -1
  45. package/src/consumers/handlers/kafkaHandler.js +1 -1
  46. package/src/consumers/index.js +1 -1
  47. package/src/consumers/messageTransformer.js +1 -1
  48. package/src/consumers/validator.js +1 -1
  49. package/src/core/db/dialect/base-dialect.js +1 -1
  50. package/src/core/db/dialect/index.js +1 -1
  51. package/src/core/db/dialect/mysql-dialect.js +1 -1
  52. package/src/core/db/dialect/oracle-dialect.js +1 -1
  53. package/src/core/db/dialect/postgres-dialect.js +1 -1
  54. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  55. package/src/core/db/flatten-helper.js +1 -1
  56. package/src/core/db/query-builder-error.js +1 -1
  57. package/src/core/db/query-builder.js +1 -1
  58. package/src/core/db/relation-helper.js +1 -1
  59. package/src/core/handlers/delete_handler.js +1 -1
  60. package/src/core/handlers/insert_handler.js +1 -1
  61. package/src/core/handlers/update_handler.js +1 -1
  62. package/src/core/models/base-model.js +1 -1
  63. package/src/core/utils/cache-manager.js +1 -1
  64. package/src/core/utils/component-engine.js +1 -1
  65. package/src/core/utils/context-builder.js +1 -1
  66. package/src/core/utils/datetime-formatter.js +1 -1
  67. package/src/core/utils/datetime-parser.js +1 -1
  68. package/src/core/utils/db.js +1 -1
  69. package/src/core/utils/logger.js +1 -1
  70. package/src/core/utils/payload-loader.js +1 -1
  71. package/src/core/utils/security-checks.js +1 -1
  72. package/src/middleware/body-options.js +1 -1
  73. package/src/middleware/cors.js +1 -1
  74. package/src/middleware/idempotency.js +1 -1
  75. package/src/middleware/rate-limiter.js +1 -1
  76. package/src/middleware/request-logger.js +1 -1
  77. package/src/middleware/security-headers.js +1 -1
  78. package/src/models/base-model-mysql.js +1 -1
  79. package/src/models/base-model-oracle.js +1 -1
  80. package/src/models/base-model-sqlite.js +1 -1
  81. package/src/models/base-model.js +1 -1
  82. package/src/pro/caching/redis-client.js +1 -1
  83. package/src/pro/caching/redis-helper.js +1 -1
  84. package/src/pro/consumers/baseConsumer.js +1 -1
  85. package/src/pro/consumers/declarativeMapper.js +1 -1
  86. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  87. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  88. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  89. package/src/pro/consumers/handlers/index.js +1 -1
  90. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  91. package/src/pro/consumers/index.js +1 -1
  92. package/src/pro/consumers/messageTransformer.js +1 -1
  93. package/src/pro/consumers/validator.js +1 -1
  94. package/src/pro/database/base-model-mysql.js +1 -1
  95. package/src/pro/database/base-model-oracle.js +1 -1
  96. package/src/pro/database/base-model-sqlite.js +1 -1
  97. package/src/pro/database/db-mysql.js +1 -1
  98. package/src/pro/database/db-oracle.js +1 -1
  99. package/src/pro/database/db-sqlite.js +1 -1
  100. package/src/pro/excel/excel-generator.js +1 -1
  101. package/src/pro/excel/excel-parser.js +1 -1
  102. package/src/pro/excel/export-service.js +1 -1
  103. package/src/pro/excel/export_handler.js +1 -1
  104. package/src/pro/excel/import-service.js +1 -1
  105. package/src/pro/excel/import-validator.js +1 -1
  106. package/src/pro/excel/import_handler.js +1 -1
  107. package/src/pro/excel/upsert-builder.js +1 -1
  108. package/src/pro/idgen/idgen-routes.js +1 -1
  109. package/src/pro/integrations/lookup-resolver.js +1 -1
  110. package/src/pro/integrations/upload-handler-v2.js +1 -1
  111. package/src/pro/integrations/upload-handler.js +1 -1
  112. package/src/pro/integrations/webhook.js +1 -1
  113. package/src/pro/locking/lock-routes.js +1 -1
  114. package/src/pro/locking/resource-lock-manager.js +1 -1
  115. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  116. package/src/pro/messaging/kafkaService.js +1 -1
  117. package/src/pro/messaging/messagehubService.js +1 -1
  118. package/src/pro/messaging/rabbitmqService.js +1 -1
  119. package/src/pro/scheduler/job-manager.js +1 -1
  120. package/src/pro/scheduler/job-routes.js +1 -1
  121. package/src/pro/scheduler/job-validator.js +1 -1
  122. package/src/pro/storage/base-storage-provider.js +1 -1
  123. package/src/pro/storage/file-metadata-helper.js +1 -1
  124. package/src/pro/storage/index.js +1 -1
  125. package/src/pro/storage/local-storage-provider.js +1 -1
  126. package/src/pro/storage/s3-storage-provider.js +1 -1
  127. package/src/pro/storage/upload-cleanup-job.js +1 -1
  128. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  129. package/src/pro/storage/upload-pending-tracker.js +1 -1
  130. package/src/pro/websocket/broadcast-helper.js +1 -1
  131. package/src/pro/websocket/index.js +1 -1
  132. package/src/pro/websocket/livesync-server.js +1 -1
  133. package/src/pro/websocket/ws-broadcaster.js +1 -1
  134. package/src/services/export-service.js +1 -1
  135. package/src/services/import-service.js +1 -1
  136. package/src/services/kafkaConsumerService.js +1 -1
  137. package/src/services/kafkaService.js +1 -1
  138. package/src/services/messagehubService.js +1 -1
  139. package/src/services/rabbitmqService.js +1 -1
  140. package/src/utils/cache-invalidation-registry.js +1 -1
  141. package/src/utils/cache-manager.js +1 -1
  142. package/src/utils/component-engine.js +1 -1
  143. package/src/utils/config-extractor.js +1 -1
  144. package/src/utils/consumerLogger.js +1 -1
  145. package/src/utils/context-builder.js +1 -1
  146. package/src/utils/dashboard-helpers.js +1 -1
  147. package/src/utils/dateHelper.js +1 -1
  148. package/src/utils/datetime-formatter.js +1 -1
  149. package/src/utils/datetime-parser.js +1 -1
  150. package/src/utils/db-bootstrap.js +1 -1
  151. package/src/utils/db-mysql.js +1 -1
  152. package/src/utils/db-oracle.js +1 -1
  153. package/src/utils/db-sqlite.js +1 -1
  154. package/src/utils/db.js +1 -1
  155. package/src/utils/demo-generator.js +1 -1
  156. package/src/utils/excel-generator.js +1 -1
  157. package/src/utils/excel-parser.js +1 -1
  158. package/src/utils/file-watcher.js +1 -1
  159. package/src/utils/id-generator.js +1 -1
  160. package/src/utils/idempotency-manager.js +1 -1
  161. package/src/utils/import-validator.js +1 -1
  162. package/src/utils/license-client.js +1 -1
  163. package/src/utils/lock-manager.js +1 -1
  164. package/src/utils/logger.js +1 -1
  165. package/src/utils/lookup-resolver.js +1 -1
  166. package/src/utils/payload-loader.js +1 -1
  167. package/src/utils/processor-response.js +1 -1
  168. package/src/utils/rabbitmq.js +1 -1
  169. package/src/utils/redis-client.js +1 -1
  170. package/src/utils/redis-helper.js +1 -1
  171. package/src/utils/request-scope.js +1 -1
  172. package/src/utils/security-checks.js +1 -1
  173. package/src/utils/service-resolver.js +1 -1
  174. package/src/utils/shutdown-coordinator.js +1 -1
  175. package/src/utils/trusted-keys.js +1 -1
  176. package/src/utils/upload-handler.js +1 -1
  177. package/src/utils/upsert-builder.js +1 -1
  178. package/src/utils/workflow-hook-executor.js +1 -1
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Endpoint Schema Validator
5
+ *
6
+ * Orchestrator untuk validasi schema database saat `endpoint create` dijalankan.
7
+ * Module ini:
8
+ * 1. Resolve --config file (via config-resolver)
9
+ * 2. Load config + connect ke database (via DatabaseIntrospector)
10
+ * 3. Compare payload-vs-database menggunakan compareSchemaStrict
11
+ * 4. Map hasil ke exit-code semantics:
12
+ * - clean schema -> return { status: 'ok' }
13
+ * - drift detected -> throw error dengan exitCode=1 + formatted report
14
+ * - connection fail -> throw error dengan exitCode=3
15
+ * - usage error -> throw error dengan exitCode=2 (di-handle caller via validateContract)
16
+ *
17
+ * Module ini sengaja decoupled dari PayloadGenerator agar tidak ikut menjalankan
18
+ * payload regenerate/sync flow.
19
+ *
20
+ * @module lib/payload/endpoint-schema-validator
21
+ */
22
+
23
+ const { DatabaseIntrospector } = require('../utils/database-introspector');
24
+ const { resolveConfig, printDefaultConfigWarning } = require('../utils/config-resolver');
25
+ const { compareSchemaStrict, formatDriftReport } = require('./schema-diff');
26
+
27
+ /**
28
+ * Buat Error object dengan property `exitCode` agar cli-entry dapat
29
+ * forward exit code spesifik.
30
+ *
31
+ * @param {string} message
32
+ * @param {number} exitCode
33
+ * @returns {Error}
34
+ */
35
+ function createExitError(message, exitCode) {
36
+ const err = new Error(message);
37
+ err.exitCode = exitCode;
38
+ err.isSchemaValidationError = true;
39
+ return err;
40
+ }
41
+
42
+ /**
43
+ * Jalankan validasi schema untuk satu payload + table sebelum codegen.
44
+ *
45
+ * @param {Object} options
46
+ * @param {Object} options.payload - Processed payload object (harus punya tableName)
47
+ * @param {string} options.payloadFileName - Nama file payload untuk pesan error
48
+ * @param {string} options.configArg - Nilai flag --config dari CLI
49
+ * @param {boolean} options.skipSchemaCheck - true bila --skip-schema-check di-set
50
+ * @param {string} [options.workingDir] - cwd, default process.cwd()
51
+ * @param {Function} [options.IntrospectorClass] - Override untuk testing
52
+ * @returns {Promise<{
53
+ * status: 'ok' | 'skipped',
54
+ * reason?: string,
55
+ * columnsChecked?: number
56
+ * }>}
57
+ * @throws {Error} Dengan property exitCode (1=drift, 2=usage, 3=connection)
58
+ */
59
+ async function validateEndpointSchema(options) {
60
+ const {
61
+ payload,
62
+ payloadFileName,
63
+ configArg,
64
+ skipSchemaCheck,
65
+ workingDir,
66
+ IntrospectorClass
67
+ } = options;
68
+
69
+ if (!payload || !payload.tableName) {
70
+ throw createExitError('Payload missing tableName — cannot run schema validation', 2);
71
+ }
72
+
73
+ if (skipSchemaCheck) {
74
+ return { status: 'skipped', reason: '--skip-schema-check active' };
75
+ }
76
+
77
+ const resolved = resolveConfig(configArg, workingDir || process.cwd());
78
+ if (!resolved) {
79
+ const lines = [
80
+ 'Error: --config is required for schema validation',
81
+ 'Usage: npx restforge endpoint create --project=<P> --name=<N> --payload=<F> --config=<ENV> [--force=true] [--skip-schema-check]'
82
+ ];
83
+ throw createExitError(lines.join('\n'), 2);
84
+ }
85
+
86
+ if (resolved.source === 'default') {
87
+ printDefaultConfigWarning(resolved.defaultName);
88
+ }
89
+
90
+ const IntroCtor = IntrospectorClass || DatabaseIntrospector;
91
+ const db = new IntroCtor({ quiet: true });
92
+
93
+ const loaded = db.loadConfig(resolved.path);
94
+ if (!loaded) {
95
+ throw createExitError(
96
+ buildConnectionErrorMessage(`Cannot load config file: ${resolved.path}`),
97
+ 3
98
+ );
99
+ }
100
+
101
+ let connected = false;
102
+ try {
103
+ connected = await db.connect();
104
+ } catch (e) {
105
+ try { await db.close(); } catch (_e) { /* ignore */ }
106
+ throw createExitError(
107
+ buildConnectionErrorMessage(e.message || String(e)),
108
+ 3
109
+ );
110
+ }
111
+
112
+ if (!connected) {
113
+ try { await db.close(); } catch (_e) { /* ignore */ }
114
+ throw createExitError(
115
+ buildConnectionErrorMessage('database not reachable'),
116
+ 3
117
+ );
118
+ }
119
+
120
+ let comparison;
121
+ try {
122
+ comparison = await compareSchemaStrict(payload, db);
123
+ } finally {
124
+ try { await db.close(); } catch (_e) { /* ignore */ }
125
+ }
126
+
127
+ if (comparison.status === 'error') {
128
+ const lines = [
129
+ 'Schema Validation:',
130
+ ` [ERROR] ${comparison.summary}`,
131
+ ' Endpoint generation blocked.',
132
+ '',
133
+ ' Workarounds:',
134
+ ' 1. Verify the table exists in the database',
135
+ ' 2. Use --skip-schema-check to bypass validation (NOT recommended for production)'
136
+ ];
137
+ throw createExitError(lines.join('\n'), 3);
138
+ }
139
+
140
+ if (comparison.status === 'drift') {
141
+ const driftLines = formatDriftReport(comparison, {
142
+ payloadFileName,
143
+ tableName: payload.tableName
144
+ });
145
+ const lines = ['Schema Validation:', ...driftLines, '', 'Endpoint generation blocked due to schema drift'];
146
+ throw createExitError(lines.join('\n'), 1);
147
+ }
148
+
149
+ return {
150
+ status: 'ok',
151
+ columnsChecked: comparison.totalColumnsChecked || 0
152
+ };
153
+ }
154
+
155
+ function buildConnectionErrorMessage(reason) {
156
+ const lines = [
157
+ 'Schema Validation:',
158
+ ` [ERROR] Cannot connect to database: ${reason}`,
159
+ ' Endpoint generation blocked.',
160
+ '',
161
+ ' Workarounds:',
162
+ ' 1. Verify db-connection.env is correct and database is reachable',
163
+ ' 2. Use --skip-schema-check to bypass validation (NOT recommended for production)'
164
+ ];
165
+ return lines.join('\n');
166
+ }
167
+
168
+ module.exports = {
169
+ validateEndpointSchema,
170
+ createExitError
171
+ };
@@ -27,17 +27,21 @@ const path = require('path');
27
27
 
28
28
  const { DatabaseIntrospector, loadDriver } = require('../utils/database-introspector');
29
29
  const { resolveConfig, printDefaultConfigWarning } = require('../utils/config-resolver');
30
+ const {
31
+ DEFAULT_AUDIT_COLUMNS,
32
+ detectAuditAlignment
33
+ } = require('../utils/audit-columns');
34
+ const { compareSchemaStrict } = require('./schema-diff');
30
35
 
31
36
  // Kolom audit yang di-handle otomatis oleh RESTForge runtime (base-model).
32
- // Kolom ini di-exclude dari payload hasil generate karena nilainya selalu
33
- // di-set runtime saat create/update. User tetap bisa menambahkan secara manual
34
- // di file payload jika memang diperlukan untuk scenario khusus.
37
+ // Konstanta dipakai dari shared util agar source-of-truth tunggal lintas
38
+ // modul (sync, validate, endpoint create). Kolom ini di-exclude dari payload
39
+ // hasil generate karena nilainya selalu di-set runtime saat create/update.
35
40
  //
36
41
  // Bila tabel tidak memiliki satupun kolom dari list ini, generator otomatis
37
42
  // menyertakan "auditColumns": false di payload supaya template generator
38
43
  // men-set this.auditColumns = null di model. Tanpa flag ini, runtime akan
39
44
  // mencoba inject kolom audit ke INSERT dan menyebabkan error 500.
40
- const DEFAULT_EXCLUDED_COLUMNS = ['created_at', 'created_by', 'updated_at', 'updated_by'];
41
45
 
42
46
  // Deteksi environment - Bun compiled binary atau Node.js/Bun script
43
47
  const isBun = typeof Bun !== 'undefined';
@@ -461,6 +465,9 @@ class PayloadGenerator {
461
465
 
462
466
  // Drift, error, atau target tidak ditemukan -> throw (exit 1 via cli-entry.js).
463
467
  // Semantik "action required" untuk MCP/LSP integration.
468
+ // Marked silent karena verdict + summary sudah di-print oleh runValidate/
469
+ // runDiff/runSync; pesan "Error: Payload schema action required: ..."
470
+ // hanya menambah noise di stderr untuk human user.
464
471
  const hasDrift = (result?.drift ?? 0) > 0;
465
472
  const hasError = (result?.error ?? 0) > 0;
466
473
  const targetMissing = targetTable && (result?.total ?? 0) === 0;
@@ -470,7 +477,10 @@ class PayloadGenerator {
470
477
  if (hasDrift) reasons.push(`drift=${result.drift}`);
471
478
  if (hasError) reasons.push(`error=${result.error}`);
472
479
  if (targetMissing) reasons.push(`target '${targetTable}' not found`);
473
- throw new Error(`Payload schema action required: ${reasons.join(', ')}`);
480
+ const err = new Error(`Payload schema action required: ${reasons.join(', ')}`);
481
+ err.silent = true;
482
+ err.exitCode = 1;
483
+ throw err;
474
484
  }
475
485
 
476
486
  return;
@@ -534,8 +544,8 @@ class PayloadGenerator {
534
544
  const columns = await this.db.getColumns(args.table);
535
545
  if (columns.length > 0) {
536
546
  // Filter kolom audit yang di-handle otomatis oleh runtime.
537
- const excludedPresent = columns.filter(col => DEFAULT_EXCLUDED_COLUMNS.includes(col));
538
- payloadData.fieldName = columns.filter(col => !DEFAULT_EXCLUDED_COLUMNS.includes(col));
547
+ const excludedPresent = columns.filter(col => DEFAULT_AUDIT_COLUMNS.includes(col));
548
+ payloadData.fieldName = columns.filter(col => !DEFAULT_AUDIT_COLUMNS.includes(col));
539
549
  if (excludedPresent.length > 0) {
540
550
  console.log(`Auto-managed columns excluded: ${excludedPresent.join(', ')}`);
541
551
  } else {
@@ -639,86 +649,6 @@ class SchemaValidator {
639
649
  this.outputDir = outputDir;
640
650
  }
641
651
 
642
- /**
643
- * Normalisasi tipe data database ke kategori umum untuk perbandingan.
644
- * Tipe database bervariasi antar engine, fungsi ini memetakan ke nama yang konsisten.
645
- * @param {string} dataType - Tipe data dari information_schema
646
- * @param {string} udtName - UDT name (PostgreSQL specific)
647
- * @param {Object} col - Full column info object
648
- * @returns {string} Normalized type string
649
- */
650
- normalizeType(dataType, udtName, col) {
651
- const dt = (dataType || '').toLowerCase();
652
- const udt = (udtName || '').toLowerCase();
653
-
654
- // UUID
655
- if (dt === 'uuid' || udt === 'uuid') return 'uuid';
656
-
657
- // Boolean
658
- if (dt === 'boolean' || udt === 'bool') return 'boolean';
659
-
660
- // Integer family
661
- if (['integer', 'bigint', 'smallint', 'serial', 'bigserial', 'smallserial',
662
- 'int', 'tinyint', 'mediumint'].includes(dt) ||
663
- ['int4', 'int8', 'int2', 'serial4', 'serial8', 'serial2'].includes(udt)) {
664
- return 'integer';
665
- }
666
-
667
- // Numeric/decimal family
668
- if (['numeric', 'decimal', 'real', 'double precision', 'float', 'double'].includes(dt) ||
669
- ['numeric', 'float4', 'float8'].includes(udt)) {
670
- const precision = col && col.numeric_precision ? `(${col.numeric_precision}` : '';
671
- const scale = col && col.numeric_scale !== null && col.numeric_scale !== undefined
672
- ? `,${col.numeric_scale})` : precision ? ')' : '';
673
- return precision ? `numeric${precision}${scale}` : 'numeric';
674
- }
675
-
676
- // Text / varchar family
677
- if (['character varying', 'varchar', 'varchar2', 'nvarchar2', 'nvarchar'].includes(dt)) {
678
- const len = col && col.character_maximum_length ? `(${col.character_maximum_length})` : '';
679
- return `varchar${len}`;
680
- }
681
- if (['text', 'clob', 'nclob', 'longtext', 'mediumtext', 'tinytext'].includes(dt)) return 'text';
682
- if (['char', 'character', 'nchar'].includes(dt)) {
683
- const len = col && col.character_maximum_length ? `(${col.character_maximum_length})` : '';
684
- return `char${len}`;
685
- }
686
-
687
- // Date/time family
688
- if (dt === 'date') return 'date';
689
- if (dt === 'datetime') return 'timestamp';
690
- if (dt.startsWith('timestamp')) return 'timestamp';
691
- if (dt.startsWith('time')) return 'time';
692
-
693
- // JSON
694
- if (['json', 'jsonb'].includes(dt) || ['json', 'jsonb'].includes(udt)) return 'json';
695
-
696
- // Fallback
697
- return dt || 'unknown';
698
- }
699
-
700
- /**
701
- * Normalisasi tipe dari fieldValidation payload ke kategori yang sama.
702
- * @param {Object} validation - Object dari array fieldValidation
703
- * @returns {string} Normalized type string
704
- */
705
- normalizePayloadValidationType(validation) {
706
- if (!validation) return null;
707
- const t = (validation.type || '').toLowerCase();
708
- switch (t) {
709
- case 'uuid': return 'uuid';
710
- case 'integer': return 'integer';
711
- case 'number': return 'numeric';
712
- case 'boolean': return 'boolean';
713
- case 'date': return 'date';
714
- case 'datetime': return 'timestamp';
715
- case 'string': return 'varchar';
716
- case 'json':
717
- case 'array': return 'json';
718
- default: return t || null;
719
- }
720
- }
721
-
722
652
  /**
723
653
  * Cari semua file payload JSON di outputDir.
724
654
  * @returns {Array<{filePath: string, fileName: string, payload: Object}>}
@@ -781,120 +711,18 @@ class SchemaValidator {
781
711
  return match || null;
782
712
  }
783
713
 
784
- /**
785
- * Bandingkan satu payload dengan schema database aktual.
786
- * @param {Object} payload - Payload JSON object
787
- * @returns {Promise<Object>} Comparison result
788
- */
789
- async comparePayloadWithDatabase(payload) {
790
- const tableName = payload.tableName;
791
- const result = {
792
- tableName,
793
- status: 'ok', // ok, drift, error
794
- added: [], // kolom baru di database, tidak ada di payload
795
- removed: [], // ada di payload, tidak ada di database
796
- typeChanges: [], // tipe data berubah
797
- nullabilityChanges: [], // nullable berubah
798
- summary: ''
799
- };
800
-
801
- // Cek apakah table masih ada di database
802
- const dbColumns = await this.db.getDetailedColumnInfo(tableName);
803
- if (dbColumns.length === 0) {
804
- result.status = 'error';
805
- result.summary = `Table "${tableName}" not found in database`;
806
- return result;
807
- }
808
-
809
- // Build map dari database columns
810
- const dbColumnMap = {};
811
- for (const col of dbColumns) {
812
- dbColumnMap[col.column_name] = col;
813
- }
814
-
815
- // Build map dari payload fieldValidation
816
- const payloadValidationMap = {};
817
- if (payload.fieldValidation) {
818
- for (const fv of payload.fieldValidation) {
819
- payloadValidationMap[fv.name] = fv;
820
- }
821
- }
822
-
823
- const payloadFields = payload.fieldName || [];
824
- const dbColumnNames = dbColumns.map(c => c.column_name);
825
-
826
- // Detect kolom baru di database (tidak ada di payload).
827
- // Kolom audit default (DEFAULT_EXCLUDED_COLUMNS) yang tidak dicantumkan
828
- // user di payload akan di-skip karena memang sengaja di-exclude.
829
- for (const dbCol of dbColumns) {
830
- if (!payloadFields.includes(dbCol.column_name)) {
831
- if (DEFAULT_EXCLUDED_COLUMNS.includes(dbCol.column_name)) continue;
832
-
833
- const normalizedType = this.normalizeType(dbCol.data_type, dbCol.udt_name, dbCol);
834
- result.added.push({
835
- column: dbCol.column_name,
836
- type: normalizedType,
837
- nullable: dbCol.is_nullable === 'YES'
838
- });
839
- }
840
- }
841
-
842
- // Detect kolom yang dihapus dari database (masih ada di payload)
843
- for (const field of payloadFields) {
844
- if (!dbColumnMap[field]) {
845
- result.removed.push({
846
- column: field
847
- });
848
- }
849
- }
850
-
851
- // Detect perubahan tipe data (hanya untuk field yang ada di kedua sisi)
852
- for (const field of payloadFields) {
853
- const dbCol = dbColumnMap[field];
854
- if (!dbCol) continue;
855
-
856
- const dbType = this.normalizeType(dbCol.data_type, dbCol.udt_name, dbCol);
857
- const payloadValidation = payloadValidationMap[field];
858
-
859
- if (payloadValidation) {
860
- const payloadType = this.normalizePayloadValidationType(payloadValidation);
861
- // Perbandingan menggunakan base type (tanpa size detail)
862
- const dbBaseType = dbType.replace(/\(.*\)/, '');
863
- const payloadBaseType = payloadType ? payloadType.replace(/\(.*\)/, '') : null;
864
-
865
- if (payloadBaseType && dbBaseType !== payloadBaseType) {
866
- result.typeChanges.push({
867
- column: field,
868
- payloadType: payloadType,
869
- databaseType: dbType
870
- });
871
- }
872
- }
873
- }
874
-
875
- // Determine overall status
876
- if (result.added.length > 0 || result.removed.length > 0 || result.typeChanges.length > 0) {
877
- result.status = 'drift';
878
- }
879
-
880
- // Build summary
881
- const parts = [];
882
- if (result.added.length > 0) parts.push(`${result.added.length} new column(s) in database`);
883
- if (result.removed.length > 0) parts.push(`${result.removed.length} column(s) missing from database`);
884
- if (result.typeChanges.length > 0) parts.push(`${result.typeChanges.length} type change(s)`);
885
- result.summary = parts.length > 0 ? parts.join(', ') : 'Schema is in sync';
886
-
887
- return result;
888
- }
889
-
890
714
  /**
891
715
  * Format hasil perbandingan ke output console.
892
- * @param {Object} comparison - Hasil dari comparePayloadWithDatabase()
716
+ * Konsisten dengan `endpoint create` (audit-column-aware).
717
+ *
718
+ * @param {Object} comparison - Hasil dari compareSchemaStrict()
893
719
  * @param {string} fileName - Nama file payload
894
- * @param {boolean} showDiffDetail - Tampilkan detail diff
720
+ * @param {boolean} showDiffDetail - Tampilkan detail per-column drift
895
721
  */
896
722
  printComparisonResult(comparison, fileName, showDiffDetail = false) {
897
- const statusIcon = comparison.status === 'ok' ? '[OK]'
723
+ // Pad status icon ke lebar tetap 7 char ([DRIFT]/[ERROR]) supaya kolom
724
+ // fileName tampil rata antar baris.
725
+ const statusIcon = comparison.status === 'ok' ? '[OK] '
898
726
  : comparison.status === 'drift' ? '[DRIFT]'
899
727
  : '[ERROR]';
900
728
 
@@ -915,24 +743,29 @@ class SchemaValidator {
915
743
  // Status: drift
916
744
  console.log(` ${comparison.summary}`);
917
745
 
918
- if (showDiffDetail) {
919
- if (comparison.added.length > 0) {
920
- for (const col of comparison.added) {
921
- const nullable = col.nullable ? ', nullable' : ', not null';
922
- console.log(` [+] ${col.column} (${col.type}${nullable})`);
923
- }
746
+ if (!showDiffDetail) return;
747
+
748
+ if (Array.isArray(comparison.removed)) {
749
+ for (const item of comparison.removed) {
750
+ console.log(` [-] ${item.column} (in payload, not in database)`);
924
751
  }
925
- if (comparison.removed.length > 0) {
926
- for (const col of comparison.removed) {
927
- console.log(` [-] ${col.column}`);
928
- }
752
+ }
753
+ if (Array.isArray(comparison.typeChanges)) {
754
+ for (const item of comparison.typeChanges) {
755
+ console.log(` [~] ${item.column} (type: ${item.payloadType} -> ${item.databaseType})`);
929
756
  }
930
- if (comparison.typeChanges.length > 0) {
931
- for (const col of comparison.typeChanges) {
932
- console.log(` [~] ${col.column}: ${col.payloadType} -> ${col.databaseType}`);
933
- }
757
+ }
758
+ if (Array.isArray(comparison.added)) {
759
+ for (const item of comparison.added) {
760
+ const nullable = item.nullable ? ', nullable' : ', not null';
761
+ console.log(` [+] ${item.column} (${item.type}${nullable}) (in database, not in payload)`);
934
762
  }
935
763
  }
764
+ if (Array.isArray(comparison.auditMissing) && comparison.auditMissing.length > 0) {
765
+ const cols = comparison.auditMissing.join(', ');
766
+ console.log(` [+] ${cols}`);
767
+ console.log(' (required by auditColumns=true, not in database)');
768
+ }
936
769
  }
937
770
 
938
771
  /**
@@ -976,7 +809,7 @@ class SchemaValidator {
976
809
  let ok = 0, drift = 0, error = 0;
977
810
 
978
811
  for (const { fileName, payload } of payloads) {
979
- const comparison = await this.comparePayloadWithDatabase(payload);
812
+ const comparison = await compareSchemaStrict(payload, this.db);
980
813
  results.push({ fileName, comparison });
981
814
  this.printComparisonResult(comparison, fileName, false);
982
815
 
@@ -1047,7 +880,7 @@ class SchemaValidator {
1047
880
  console.log('-'.repeat(60));
1048
881
  console.log();
1049
882
 
1050
- const comparison = await this.comparePayloadWithDatabase(payload);
883
+ const comparison = await compareSchemaStrict(payload, this.db);
1051
884
  results.push({ fileName, comparison });
1052
885
  this.printComparisonResult(comparison, fileName, true);
1053
886
  console.log();
@@ -1154,7 +987,7 @@ class SchemaValidator {
1154
987
  let synced = 0, skipped = 0, errorCount = 0;
1155
988
 
1156
989
  for (const { filePath, fileName, payload } of payloads) {
1157
- const comparison = await this.comparePayloadWithDatabase(payload);
990
+ const comparison = await compareSchemaStrict(payload, this.db);
1158
991
 
1159
992
  if (comparison.status === 'error') {
1160
993
  console.log(` [ERROR] ${fileName} - ${comparison.summary}`);
@@ -1168,15 +1001,16 @@ class SchemaValidator {
1168
1001
  continue;
1169
1002
  }
1170
1003
 
1171
- // Status: drift - perlu sync
1172
- // 1. Archive file lama
1004
+ // Status: drift (termasuk audit drift) — archive + regenerate.
1173
1005
  const archivePath = this.archiveFile(filePath);
1174
1006
  const archiveName = path.basename(archivePath);
1175
1007
  console.log(` [ARCHIVE] ${fileName} -> ${archiveName}`);
1176
1008
 
1177
- // 2. Re-generate payload dari database
1009
+ // Re-generate payload dari database. regeneratePayload menerapkan audit
1010
+ // columns resolution secara internal (set auditColumns: false bila DB
1011
+ // tidak punya kolom audit standar).
1178
1012
  try {
1179
- const updatedPayload = await this.regeneratePayload(payload, comparison);
1013
+ const updatedPayload = await this.regeneratePayload(payload, comparison, { fileName });
1180
1014
  fs.writeFileSync(filePath, JSON.stringify(updatedPayload, null, 4), 'utf8');
1181
1015
  console.log(` [SYNCED] ${fileName} - ${comparison.summary}`);
1182
1016
  synced++;
@@ -1205,12 +1039,17 @@ class SchemaValidator {
1205
1039
  /**
1206
1040
  * Regenerate payload berdasarkan perubahan yang terdeteksi.
1207
1041
  * Mempertahankan konfigurasi payload lama (action, filters, dll),
1208
- * hanya update fieldName, fieldValidation, dateTimeFields, dan query.
1042
+ * hanya update fieldName, fieldValidation, dateTimeFields, query, dan
1043
+ * (sejak Phase 03) field `auditColumns` bila terjadi misalignment dengan
1044
+ * tabel database.
1045
+ *
1209
1046
  * @param {Object} oldPayload - Payload JSON lama
1210
1047
  * @param {Object} comparison - Hasil comparePayloadWithDatabase
1048
+ * @param {Object} [options]
1049
+ * @param {string} [options.fileName] - Nama file payload untuk pesan log
1211
1050
  * @returns {Promise<Object>} Updated payload
1212
1051
  */
1213
- async regeneratePayload(oldPayload, comparison) {
1052
+ async regeneratePayload(oldPayload, comparison, options = {}) {
1214
1053
  const tableName = oldPayload.tableName;
1215
1054
 
1216
1055
  // Ambil data terbaru dari database
@@ -1233,7 +1072,7 @@ class SchemaValidator {
1233
1072
  // Tambahkan field baru dari database, skip kolom audit default
1234
1073
  for (const col of dbColumns) {
1235
1074
  if (newFieldName.includes(col)) continue;
1236
- if (DEFAULT_EXCLUDED_COLUMNS.includes(col)) continue;
1075
+ if (DEFAULT_AUDIT_COLUMNS.includes(col)) continue;
1237
1076
  newFieldName.push(col);
1238
1077
  }
1239
1078
 
@@ -1291,8 +1130,86 @@ class SchemaValidator {
1291
1130
  delete updatedPayload.fieldValidation;
1292
1131
  }
1293
1132
 
1133
+ // Audit columns resolution (Phase 03):
1134
+ // Pastikan field `auditColumns` di payload aligned dengan struktur tabel
1135
+ // database. Logic ini mencegah inkonsistensi antara `payload sync` (yang
1136
+ // sebelumnya tidak menyentuh auditColumns) dan `endpoint create` (yang
1137
+ // sudah audit-column-aware sejak Phase 01).
1138
+ this.resolveAuditColumnsForSync(updatedPayload, oldPayload, dbColumns, {
1139
+ tableName,
1140
+ fileName: options.fileName
1141
+ });
1142
+
1294
1143
  return updatedPayload;
1295
1144
  }
1145
+
1146
+ /**
1147
+ * Resolve nilai `auditColumns` untuk payload hasil sync. Memutasi
1148
+ * `updatedPayload` in-place.
1149
+ *
1150
+ * Aturan (lihat phase-03-audit-aware-sync.md):
1151
+ * - oldPayload.auditColumns === false/null -> preserve (no-op)
1152
+ * - oldPayload.auditColumns object form -> warn, no auto-override
1153
+ * - oldPayload.auditColumns === true + DB tidak full audit -> override
1154
+ * ke false + warning ke stderr
1155
+ * - oldPayload.auditColumns tidak di-set (default true) + DB tidak full
1156
+ * audit -> set false + info message
1157
+ * - DB punya keempat kolom audit standar -> preserve apapun bentuknya
1158
+ *
1159
+ * "DB tidak full audit" mencakup `none` (tidak satupun kolom audit) dan
1160
+ * `partial` (1-3 kolom audit). Partial di-treat sebagai incomplete: shape
1161
+ * audit tidak lengkap, generator tidak bisa inject 4 kolom sambil sebagian
1162
+ * tidak exist. Default strict yang aman.
1163
+ *
1164
+ * @param {Object} updatedPayload - Payload yang akan ditulis (mutated)
1165
+ * @param {Object} oldPayload - Payload lama (untuk inspect state asli)
1166
+ * @param {string[]} dbColumns - Daftar kolom database
1167
+ * @param {Object} ctx
1168
+ * @param {string} ctx.tableName - Nama table
1169
+ * @param {string} [ctx.fileName] - Nama file payload untuk pesan log
1170
+ * @returns {void}
1171
+ */
1172
+ resolveAuditColumnsForSync(updatedPayload, oldPayload, dbColumns, ctx) {
1173
+ const tableName = ctx.tableName;
1174
+ const fileLabel = ctx.fileName || `${tableName}.json`;
1175
+ const alignment = detectAuditAlignment(dbColumns);
1176
+ const hasField = Object.prototype.hasOwnProperty.call(oldPayload, 'auditColumns');
1177
+ const value = hasField ? oldPayload.auditColumns : undefined;
1178
+
1179
+ // Case 1: explicit false/null -> preserve (no-op)
1180
+ if (value === false || value === null) {
1181
+ return;
1182
+ }
1183
+
1184
+ // Case 2: object form (custom column mapping) -> warn, no auto-override
1185
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
1186
+ console.warn(
1187
+ `[restforge] Custom auditColumns object detected for "${tableName}". ` +
1188
+ 'Cannot auto-resolve. Verify column names manually.'
1189
+ );
1190
+ return;
1191
+ }
1192
+
1193
+ // Case 3: DB punya semua kolom audit -> preserve apa adanya
1194
+ if (alignment.all) {
1195
+ return;
1196
+ }
1197
+
1198
+ // Case 4: DB tidak full audit (none atau partial) + payload default/true
1199
+ // -> set updatedPayload.auditColumns = false
1200
+ if (value === true) {
1201
+ console.warn(
1202
+ `[restforge] Resetting auditColumns: true -> false for table "${tableName}" ` +
1203
+ 'because audit columns missing in database'
1204
+ );
1205
+ } else {
1206
+ console.log(
1207
+ `[restforge] Set "auditColumns": false in ${fileLabel} ` +
1208
+ 'because audit columns missing in database'
1209
+ );
1210
+ }
1211
+ updatedPayload.auditColumns = false;
1212
+ }
1296
1213
  }
1297
1214
 
1298
1215
  module.exports = {