@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.
- package/bin/sdf-tools.exe +0 -0
- package/build-info.json +2 -2
- package/cli/consumer-deploy.js +1 -1
- package/cli/consumer.js +1 -1
- package/generators/cli/endpoint/create.js +42 -3
- package/generators/cli/schema/apply.js +525 -0
- package/generators/cli/schema/diff.js +321 -0
- package/generators/cli/schema/generate-ddl.js +7 -10
- package/generators/cli/schema/init.js +95 -172
- package/generators/cli/schema/migrate.js +10 -16
- package/generators/cli/schema/models.js +8 -12
- package/generators/cli/schema/template.js +222 -0
- package/generators/cli/schema/validate.js +8 -12
- package/generators/cli-entry.js +17 -2
- package/generators/lib/dbschema-kit/apply-engine.js +582 -0
- package/generators/lib/dbschema-kit/diff-engine.js +703 -0
- package/generators/lib/dbschema-kit/diff-reporter.js +272 -0
- package/generators/lib/dbschema-kit/emitters/alter-table.js +275 -0
- package/generators/lib/payload/endpoint-schema-validator.js +171 -0
- package/generators/lib/payload/payload-runner.js +137 -220
- package/generators/lib/payload/schema-diff.js +277 -0
- package/generators/lib/utils/audit-columns.js +181 -0
- package/generators/lib/utils/cli-output.js +17 -0
- package/generators/lib/utils/database-introspector.js +16 -13
- package/integrity-manifest.json +8 -8
- package/package.json +1 -1
- package/scripts/verify-integrity.js +1 -1
- package/server.js +1 -1
- package/src/components/handlers/adjust_handler.js +1 -1
- package/src/components/handlers/audit_handler.js +1 -1
- package/src/components/handlers/delete_handler.js +1 -1
- package/src/components/handlers/export_handler.js +1 -1
- package/src/components/handlers/import_handler.js +1 -1
- package/src/components/handlers/insert_handler.js +1 -1
- package/src/components/handlers/update_handler.js +1 -1
- package/src/components/handlers/upload_handler.js +1 -1
- package/src/components/handlers/workflow_handler.js +1 -1
- package/src/components/integrations/webhook.js +1 -1
- package/src/consumers/baseConsumer.js +1 -1
- package/src/consumers/declarativeMapper.js +1 -1
- package/src/consumers/handlers/apiHandler.js +1 -1
- package/src/consumers/handlers/consoleHandler.js +1 -1
- package/src/consumers/handlers/databaseHandler.js +1 -1
- package/src/consumers/handlers/index.js +1 -1
- package/src/consumers/handlers/kafkaHandler.js +1 -1
- package/src/consumers/index.js +1 -1
- package/src/consumers/messageTransformer.js +1 -1
- package/src/consumers/validator.js +1 -1
- package/src/core/db/dialect/base-dialect.js +1 -1
- package/src/core/db/dialect/index.js +1 -1
- package/src/core/db/dialect/mysql-dialect.js +1 -1
- package/src/core/db/dialect/oracle-dialect.js +1 -1
- package/src/core/db/dialect/postgres-dialect.js +1 -1
- package/src/core/db/dialect/sqlite-dialect.js +1 -1
- package/src/core/db/flatten-helper.js +1 -1
- package/src/core/db/query-builder-error.js +1 -1
- package/src/core/db/query-builder.js +1 -1
- package/src/core/db/relation-helper.js +1 -1
- package/src/core/handlers/delete_handler.js +1 -1
- package/src/core/handlers/insert_handler.js +1 -1
- package/src/core/handlers/update_handler.js +1 -1
- package/src/core/models/base-model.js +1 -1
- package/src/core/utils/cache-manager.js +1 -1
- package/src/core/utils/component-engine.js +1 -1
- package/src/core/utils/context-builder.js +1 -1
- package/src/core/utils/datetime-formatter.js +1 -1
- package/src/core/utils/datetime-parser.js +1 -1
- package/src/core/utils/db.js +1 -1
- package/src/core/utils/logger.js +1 -1
- package/src/core/utils/payload-loader.js +1 -1
- package/src/core/utils/security-checks.js +1 -1
- package/src/middleware/body-options.js +1 -1
- package/src/middleware/cors.js +1 -1
- package/src/middleware/idempotency.js +1 -1
- package/src/middleware/rate-limiter.js +1 -1
- package/src/middleware/request-logger.js +1 -1
- package/src/middleware/security-headers.js +1 -1
- package/src/models/base-model-mysql.js +1 -1
- package/src/models/base-model-oracle.js +1 -1
- package/src/models/base-model-sqlite.js +1 -1
- package/src/models/base-model.js +1 -1
- package/src/pro/caching/redis-client.js +1 -1
- package/src/pro/caching/redis-helper.js +1 -1
- package/src/pro/consumers/baseConsumer.js +1 -1
- package/src/pro/consumers/declarativeMapper.js +1 -1
- package/src/pro/consumers/handlers/apiHandler.js +1 -1
- package/src/pro/consumers/handlers/consoleHandler.js +1 -1
- package/src/pro/consumers/handlers/databaseHandler.js +1 -1
- package/src/pro/consumers/handlers/index.js +1 -1
- package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
- package/src/pro/consumers/index.js +1 -1
- package/src/pro/consumers/messageTransformer.js +1 -1
- package/src/pro/consumers/validator.js +1 -1
- package/src/pro/database/base-model-mysql.js +1 -1
- package/src/pro/database/base-model-oracle.js +1 -1
- package/src/pro/database/base-model-sqlite.js +1 -1
- package/src/pro/database/db-mysql.js +1 -1
- package/src/pro/database/db-oracle.js +1 -1
- package/src/pro/database/db-sqlite.js +1 -1
- package/src/pro/excel/excel-generator.js +1 -1
- package/src/pro/excel/excel-parser.js +1 -1
- package/src/pro/excel/export-service.js +1 -1
- package/src/pro/excel/export_handler.js +1 -1
- package/src/pro/excel/import-service.js +1 -1
- package/src/pro/excel/import-validator.js +1 -1
- package/src/pro/excel/import_handler.js +1 -1
- package/src/pro/excel/upsert-builder.js +1 -1
- package/src/pro/idgen/idgen-routes.js +1 -1
- package/src/pro/integrations/lookup-resolver.js +1 -1
- package/src/pro/integrations/upload-handler-v2.js +1 -1
- package/src/pro/integrations/upload-handler.js +1 -1
- package/src/pro/integrations/webhook.js +1 -1
- package/src/pro/locking/lock-routes.js +1 -1
- package/src/pro/locking/resource-lock-manager.js +1 -1
- package/src/pro/messaging/kafkaConsumerService.js +1 -1
- package/src/pro/messaging/kafkaService.js +1 -1
- package/src/pro/messaging/messagehubService.js +1 -1
- package/src/pro/messaging/rabbitmqService.js +1 -1
- package/src/pro/scheduler/job-manager.js +1 -1
- package/src/pro/scheduler/job-routes.js +1 -1
- package/src/pro/scheduler/job-validator.js +1 -1
- package/src/pro/storage/base-storage-provider.js +1 -1
- package/src/pro/storage/file-metadata-helper.js +1 -1
- package/src/pro/storage/index.js +1 -1
- package/src/pro/storage/local-storage-provider.js +1 -1
- package/src/pro/storage/s3-storage-provider.js +1 -1
- package/src/pro/storage/upload-cleanup-job.js +1 -1
- package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
- package/src/pro/storage/upload-pending-tracker.js +1 -1
- package/src/pro/websocket/broadcast-helper.js +1 -1
- package/src/pro/websocket/index.js +1 -1
- package/src/pro/websocket/livesync-server.js +1 -1
- package/src/pro/websocket/ws-broadcaster.js +1 -1
- package/src/services/export-service.js +1 -1
- package/src/services/import-service.js +1 -1
- package/src/services/kafkaConsumerService.js +1 -1
- package/src/services/kafkaService.js +1 -1
- package/src/services/messagehubService.js +1 -1
- package/src/services/rabbitmqService.js +1 -1
- package/src/utils/cache-invalidation-registry.js +1 -1
- package/src/utils/cache-manager.js +1 -1
- package/src/utils/component-engine.js +1 -1
- package/src/utils/config-extractor.js +1 -1
- package/src/utils/consumerLogger.js +1 -1
- package/src/utils/context-builder.js +1 -1
- package/src/utils/dashboard-helpers.js +1 -1
- package/src/utils/dateHelper.js +1 -1
- package/src/utils/datetime-formatter.js +1 -1
- package/src/utils/datetime-parser.js +1 -1
- package/src/utils/db-bootstrap.js +1 -1
- package/src/utils/db-mysql.js +1 -1
- package/src/utils/db-oracle.js +1 -1
- package/src/utils/db-sqlite.js +1 -1
- package/src/utils/db.js +1 -1
- package/src/utils/demo-generator.js +1 -1
- package/src/utils/excel-generator.js +1 -1
- package/src/utils/excel-parser.js +1 -1
- package/src/utils/file-watcher.js +1 -1
- package/src/utils/id-generator.js +1 -1
- package/src/utils/idempotency-manager.js +1 -1
- package/src/utils/import-validator.js +1 -1
- package/src/utils/license-client.js +1 -1
- package/src/utils/lock-manager.js +1 -1
- package/src/utils/logger.js +1 -1
- package/src/utils/lookup-resolver.js +1 -1
- package/src/utils/payload-loader.js +1 -1
- package/src/utils/processor-response.js +1 -1
- package/src/utils/rabbitmq.js +1 -1
- package/src/utils/redis-client.js +1 -1
- package/src/utils/redis-helper.js +1 -1
- package/src/utils/request-scope.js +1 -1
- package/src/utils/security-checks.js +1 -1
- package/src/utils/service-resolver.js +1 -1
- package/src/utils/shutdown-coordinator.js +1 -1
- package/src/utils/trusted-keys.js +1 -1
- package/src/utils/upload-handler.js +1 -1
- package/src/utils/upsert-builder.js +1 -1
- package/src/utils/workflow-hook-executor.js +1 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema Diff - Shared payload-vs-database comparison
|
|
5
|
+
*
|
|
6
|
+
* Module ini ekstraksi logic dari `SchemaValidator` di payload-runner.js untuk
|
|
7
|
+
* dipakai bersama oleh `endpoint create` (validasi pre-codegen) tanpa mengikat
|
|
8
|
+
* caller ke seluruh ergonomi `payload diff` command (mis. summary console output
|
|
9
|
+
* yang fokus ke batch validation).
|
|
10
|
+
*
|
|
11
|
+
* Audit-column-awareness:
|
|
12
|
+
* Default behavior `runtime` adalah inject 4 kolom audit (`created_at`,
|
|
13
|
+
* `created_by`, `updated_at`, `updated_by`) ke INSERT/UPDATE saat
|
|
14
|
+
* `payload.auditColumns` tidak di-set `false`/`null`. Fungsi ini meng-compute
|
|
15
|
+
* effective field list (eksplisit + audit) dan men-flag kolom audit yang
|
|
16
|
+
* *required* tapi tidak ada di database secara eksplisit, sehingga error
|
|
17
|
+
* runtime "column does not exist" dapat di-detect saat generate-time.
|
|
18
|
+
*
|
|
19
|
+
* @module lib/payload/schema-diff
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { resolveEffectiveFieldList, resolveAuditColumnNames, DEFAULT_AUDIT_COLUMNS } = require('../utils/audit-columns');
|
|
23
|
+
|
|
24
|
+
const DEFAULT_EXCLUDED_COLUMNS = DEFAULT_AUDIT_COLUMNS;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalisasi tipe dari fieldValidation payload ke kategori yang sama dengan
|
|
28
|
+
* `normalizeDatabaseType`. Dipakai untuk men-detect type drift antara payload
|
|
29
|
+
* dan database.
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} validation - Entry dari payload.fieldValidation
|
|
32
|
+
* @returns {string|null}
|
|
33
|
+
*/
|
|
34
|
+
function normalizePayloadValidationType(validation) {
|
|
35
|
+
if (!validation) return null;
|
|
36
|
+
const t = (validation.type || '').toLowerCase();
|
|
37
|
+
switch (t) {
|
|
38
|
+
case 'uuid': return 'uuid';
|
|
39
|
+
case 'integer': return 'integer';
|
|
40
|
+
case 'number': return 'numeric';
|
|
41
|
+
case 'boolean': return 'boolean';
|
|
42
|
+
case 'date': return 'date';
|
|
43
|
+
case 'datetime': return 'timestamp';
|
|
44
|
+
case 'string': return 'varchar';
|
|
45
|
+
case 'json':
|
|
46
|
+
case 'array': return 'json';
|
|
47
|
+
default: return t || null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Normalisasi tipe data database ke kategori umum untuk perbandingan
|
|
53
|
+
* (mirror dari SchemaValidator.normalizeType di payload-runner.js).
|
|
54
|
+
*
|
|
55
|
+
* @param {string} dataType
|
|
56
|
+
* @param {string} udtName
|
|
57
|
+
* @param {Object} col
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
function normalizeDatabaseType(dataType, udtName, col) {
|
|
61
|
+
const dt = (dataType || '').toLowerCase();
|
|
62
|
+
const udt = (udtName || '').toLowerCase();
|
|
63
|
+
|
|
64
|
+
if (dt === 'uuid' || udt === 'uuid') return 'uuid';
|
|
65
|
+
if (dt === 'boolean' || udt === 'bool') return 'boolean';
|
|
66
|
+
|
|
67
|
+
if (['integer', 'bigint', 'smallint', 'serial', 'bigserial', 'smallserial',
|
|
68
|
+
'int', 'tinyint', 'mediumint'].includes(dt) ||
|
|
69
|
+
['int4', 'int8', 'int2', 'serial4', 'serial8', 'serial2'].includes(udt)) {
|
|
70
|
+
return 'integer';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (['numeric', 'decimal', 'real', 'double precision', 'float', 'double'].includes(dt) ||
|
|
74
|
+
['numeric', 'float4', 'float8'].includes(udt)) {
|
|
75
|
+
return 'numeric';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (['character varying', 'varchar', 'varchar2', 'nvarchar2', 'nvarchar'].includes(dt)) return 'varchar';
|
|
79
|
+
if (['text', 'clob', 'nclob', 'longtext', 'mediumtext', 'tinytext'].includes(dt)) return 'text';
|
|
80
|
+
if (['char', 'character', 'nchar'].includes(dt)) return 'char';
|
|
81
|
+
|
|
82
|
+
if (dt === 'date') return 'date';
|
|
83
|
+
if (dt === 'datetime') return 'timestamp';
|
|
84
|
+
if (dt.startsWith('timestamp')) return 'timestamp';
|
|
85
|
+
if (dt.startsWith('time')) return 'time';
|
|
86
|
+
|
|
87
|
+
if (['json', 'jsonb'].includes(dt) || ['json', 'jsonb'].includes(udt)) return 'json';
|
|
88
|
+
|
|
89
|
+
return dt || 'unknown';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Bandingkan satu payload dengan schema database aktual.
|
|
94
|
+
*
|
|
95
|
+
* Mode strict ini audit-column-aware:
|
|
96
|
+
* - tidak men-skip kolom audit default dari sisi "added" (kolom audit yang
|
|
97
|
+
* di-required oleh payload tapi tidak ada di database dilaporkan sebagai
|
|
98
|
+
* `auditMissing`).
|
|
99
|
+
* - compute removed = field di payload (eksplisit) yang tidak ada di database
|
|
100
|
+
* - compute typeChanges = field yang ada di payload + database tapi tipenya
|
|
101
|
+
* beda (pakai `fieldValidation` di payload sebagai source of truth)
|
|
102
|
+
*
|
|
103
|
+
* Dipakai bersama oleh endpoint create, payload validate, payload diff, dan
|
|
104
|
+
* payload sync sebagai source of truth tunggal untuk drift detection.
|
|
105
|
+
*
|
|
106
|
+
* @param {Object} payload - Processed payload object
|
|
107
|
+
* @param {Object} db - DatabaseIntrospector terkoneksi
|
|
108
|
+
* @returns {Promise<{
|
|
109
|
+
* tableName: string,
|
|
110
|
+
* status: 'ok' | 'drift' | 'error',
|
|
111
|
+
* removed: Array<{ column: string, source: 'payload' | 'audit' }>,
|
|
112
|
+
* added: Array<{ column: string, type: string, nullable: boolean }>,
|
|
113
|
+
* typeChanges: Array<{ column: string, payloadType: string, databaseType: string }>,
|
|
114
|
+
* auditMissing: string[],
|
|
115
|
+
* summary: string
|
|
116
|
+
* }>}
|
|
117
|
+
*/
|
|
118
|
+
async function compareSchemaStrict(payload, db) {
|
|
119
|
+
const tableName = payload.tableName;
|
|
120
|
+
const result = {
|
|
121
|
+
tableName,
|
|
122
|
+
status: 'ok',
|
|
123
|
+
removed: [],
|
|
124
|
+
added: [],
|
|
125
|
+
typeChanges: [],
|
|
126
|
+
auditMissing: [],
|
|
127
|
+
summary: ''
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const dbColumns = await db.getDetailedColumnInfo(tableName);
|
|
131
|
+
if (!Array.isArray(dbColumns) || dbColumns.length === 0) {
|
|
132
|
+
result.status = 'error';
|
|
133
|
+
result.summary = `Table "${tableName}" not found in database`;
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const dbColumnSet = new Set(dbColumns.map(c => c.column_name));
|
|
138
|
+
const dbColumnList = dbColumns.map(c => c.column_name);
|
|
139
|
+
const dbColumnMap = {};
|
|
140
|
+
for (const col of dbColumns) {
|
|
141
|
+
dbColumnMap[col.column_name] = col;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const { explicit, audit } = resolveEffectiveFieldList(payload);
|
|
145
|
+
|
|
146
|
+
const explicitSet = new Set(explicit);
|
|
147
|
+
for (const field of explicit) {
|
|
148
|
+
if (!dbColumnSet.has(field)) {
|
|
149
|
+
result.removed.push({ column: field, source: 'payload' });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const auditCol of audit) {
|
|
154
|
+
if (!dbColumnSet.has(auditCol)) {
|
|
155
|
+
result.auditMissing.push(auditCol);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const dbCol of dbColumns) {
|
|
160
|
+
const name = dbCol.column_name;
|
|
161
|
+
if (explicitSet.has(name)) continue;
|
|
162
|
+
if (DEFAULT_EXCLUDED_COLUMNS.includes(name)) continue;
|
|
163
|
+
if (audit.includes(name)) continue;
|
|
164
|
+
result.added.push({
|
|
165
|
+
column: name,
|
|
166
|
+
type: normalizeDatabaseType(dbCol.data_type, dbCol.udt_name, dbCol),
|
|
167
|
+
nullable: dbCol.is_nullable === 'YES'
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Type drift: hanya untuk field yang ada di kedua sisi DAN punya entry
|
|
172
|
+
// di payload.fieldValidation. Tanpa fieldValidation, type drift tidak bisa
|
|
173
|
+
// di-detect (payload tidak men-claim tipe spesifik).
|
|
174
|
+
const validationMap = {};
|
|
175
|
+
if (Array.isArray(payload.fieldValidation)) {
|
|
176
|
+
for (const fv of payload.fieldValidation) {
|
|
177
|
+
if (fv && fv.name) validationMap[fv.name] = fv;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const field of explicit) {
|
|
181
|
+
const dbCol = dbColumnMap[field];
|
|
182
|
+
if (!dbCol) continue;
|
|
183
|
+
const validation = validationMap[field];
|
|
184
|
+
if (!validation) continue;
|
|
185
|
+
|
|
186
|
+
const dbType = normalizeDatabaseType(dbCol.data_type, dbCol.udt_name, dbCol);
|
|
187
|
+
const payloadType = normalizePayloadValidationType(validation);
|
|
188
|
+
if (!payloadType) continue;
|
|
189
|
+
|
|
190
|
+
if (dbType !== payloadType) {
|
|
191
|
+
result.typeChanges.push({
|
|
192
|
+
column: field,
|
|
193
|
+
payloadType,
|
|
194
|
+
databaseType: dbType
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const driftCount = result.removed.length + result.added.length
|
|
200
|
+
+ result.auditMissing.length + result.typeChanges.length;
|
|
201
|
+
if (driftCount > 0) {
|
|
202
|
+
result.status = 'drift';
|
|
203
|
+
const parts = [];
|
|
204
|
+
if (result.removed.length > 0) parts.push(`${result.removed.length} payload field(s) missing from database`);
|
|
205
|
+
if (result.typeChanges.length > 0) parts.push(`${result.typeChanges.length} type change(s)`);
|
|
206
|
+
if (result.added.length > 0) parts.push(`${result.added.length} new database column(s) not in payload`);
|
|
207
|
+
if (result.auditMissing.length > 0) parts.push(`${result.auditMissing.length} audit column(s) required but missing`);
|
|
208
|
+
result.summary = parts.join(', ');
|
|
209
|
+
} else {
|
|
210
|
+
result.summary = 'Schema is in sync';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
result.totalColumnsChecked = dbColumnList.length;
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Format drift result ke output console untuk endpoint create.
|
|
219
|
+
* Format mengikuti spec phase-01-implementation.md:
|
|
220
|
+
* [-] kolom_a (in payload, not in database)
|
|
221
|
+
* [+] kolom_b (in database, not in payload)
|
|
222
|
+
* [+] created_at, ... (required by auditColumns=true, not in database)
|
|
223
|
+
*
|
|
224
|
+
* @param {Object} comparison - Hasil compareSchemaStrict()
|
|
225
|
+
* @param {Object} options
|
|
226
|
+
* @param {string} options.payloadFileName - Nama file payload (untuk resolution message)
|
|
227
|
+
* @param {string} options.tableName - Nama table
|
|
228
|
+
* @returns {string[]} Lines untuk di-print
|
|
229
|
+
*/
|
|
230
|
+
function formatDriftReport(comparison, options = {}) {
|
|
231
|
+
const lines = [];
|
|
232
|
+
const payloadFile = options.payloadFileName || comparison.tableName;
|
|
233
|
+
const tableName = comparison.tableName;
|
|
234
|
+
|
|
235
|
+
lines.push(' [BLOCKED] Payload-database drift detected:');
|
|
236
|
+
|
|
237
|
+
for (const item of comparison.removed) {
|
|
238
|
+
lines.push(` [-] ${item.column.padEnd(12)} (in payload, not in database)`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (Array.isArray(comparison.typeChanges)) {
|
|
242
|
+
for (const item of comparison.typeChanges) {
|
|
243
|
+
lines.push(` [~] ${item.column.padEnd(12)} (type: ${item.payloadType} -> ${item.databaseType})`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const item of comparison.added) {
|
|
248
|
+
lines.push(` [+] ${item.column.padEnd(12)} (in database, not in payload)`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (comparison.auditMissing.length > 0) {
|
|
252
|
+
const cols = comparison.auditMissing.join(', ');
|
|
253
|
+
lines.push(` [+] ${cols}`);
|
|
254
|
+
lines.push(' (required by auditColumns=true, not in database)');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
lines.push('');
|
|
258
|
+
lines.push(' Resolution:');
|
|
259
|
+
lines.push(` 1. Run: npx restforge payload sync --table=${tableName} --config=<ENV>`);
|
|
260
|
+
if (comparison.auditMissing.length > 0) {
|
|
261
|
+
lines.push(` 2. Add audit columns to schema OR set "auditColumns": false in payload/${payloadFile}`);
|
|
262
|
+
lines.push(' 3. Re-run endpoint create after resolving');
|
|
263
|
+
} else {
|
|
264
|
+
lines.push(' 2. Tambah kolom missing ke schema untuk match dengan payload');
|
|
265
|
+
lines.push(' 3. Re-run endpoint create after resolving');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return lines;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = {
|
|
272
|
+
DEFAULT_EXCLUDED_COLUMNS,
|
|
273
|
+
normalizeDatabaseType,
|
|
274
|
+
normalizePayloadValidationType,
|
|
275
|
+
compareSchemaStrict,
|
|
276
|
+
formatDriftReport
|
|
277
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Audit Columns Helper
|
|
5
|
+
*
|
|
6
|
+
* Resolve nama kolom audit yang efektif berdasarkan konfigurasi payload.
|
|
7
|
+
* Konvensi default mengikuti BaseModel runtime di `src/models/base-model.js`:
|
|
8
|
+
* createdAt -> created_at
|
|
9
|
+
* createdBy -> created_by
|
|
10
|
+
* updatedAt -> updated_at
|
|
11
|
+
* updatedBy -> updated_by
|
|
12
|
+
*
|
|
13
|
+
* Payload dapat override behavior:
|
|
14
|
+
* - `auditColumns: false` (atau null) -> disable audit columns
|
|
15
|
+
* - `auditColumns: { createdAt, createdBy, updatedAt, updatedBy }` -> custom mapping
|
|
16
|
+
* - tidak di-set / true -> pakai default 4 nama standar
|
|
17
|
+
*
|
|
18
|
+
* Helper ini dipakai oleh:
|
|
19
|
+
* - schema-diff: compute effective field list (fieldName ∪ audit columns)
|
|
20
|
+
* - endpoint create: validation pre-codegen
|
|
21
|
+
*
|
|
22
|
+
* @module lib/utils/audit-columns
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const DEFAULT_AUDIT_COLUMN_MAP = {
|
|
26
|
+
createdAt: 'created_at',
|
|
27
|
+
createdBy: 'created_by',
|
|
28
|
+
updatedAt: 'updated_at',
|
|
29
|
+
updatedBy: 'updated_by'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const DEFAULT_AUDIT_COLUMNS = Object.values(DEFAULT_AUDIT_COLUMN_MAP);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Cek apakah audit columns aktif untuk payload ini.
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} payload - Processed payload object
|
|
38
|
+
* @returns {boolean} true jika audit columns akan di-inject runtime
|
|
39
|
+
*/
|
|
40
|
+
function isAuditColumnsEnabled(payload) {
|
|
41
|
+
if (!payload) return false;
|
|
42
|
+
if (!Object.prototype.hasOwnProperty.call(payload, 'auditColumns')) return true;
|
|
43
|
+
const value = payload.auditColumns;
|
|
44
|
+
if (value === false || value === null) return false;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve nama kolom audit (sisi database) yang akan di-inject runtime.
|
|
50
|
+
* Mengembalikan empty array bila audit columns di-disable.
|
|
51
|
+
*
|
|
52
|
+
* @param {Object} payload - Processed payload object
|
|
53
|
+
* @returns {string[]} Array nama kolom database (snake_case)
|
|
54
|
+
*/
|
|
55
|
+
function resolveAuditColumnNames(payload) {
|
|
56
|
+
if (!isAuditColumnsEnabled(payload)) return [];
|
|
57
|
+
|
|
58
|
+
const value = payload && payload.auditColumns;
|
|
59
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const key of Object.keys(DEFAULT_AUDIT_COLUMN_MAP)) {
|
|
62
|
+
const mapped = value[key];
|
|
63
|
+
if (typeof mapped === 'string' && mapped.length > 0) {
|
|
64
|
+
out.push(mapped);
|
|
65
|
+
} else {
|
|
66
|
+
out.push(DEFAULT_AUDIT_COLUMN_MAP[key]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return DEFAULT_AUDIT_COLUMNS.slice();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve effective field list yang dipakai generator codegen:
|
|
77
|
+
* gabungan fieldName eksplisit di payload + audit columns yang akan
|
|
78
|
+
* di-inject runtime.
|
|
79
|
+
*
|
|
80
|
+
* Dipakai oleh schema validation untuk men-detect drift yang berasal
|
|
81
|
+
* dari implicit audit columns (kasus umum: auditColumns aktif tapi
|
|
82
|
+
* tabel database tidak punya kolom audit).
|
|
83
|
+
*
|
|
84
|
+
* @param {Object} payload - Processed payload object
|
|
85
|
+
* @returns {{ fields: string[], audit: string[], explicit: string[] }}
|
|
86
|
+
*/
|
|
87
|
+
function resolveEffectiveFieldList(payload) {
|
|
88
|
+
const explicit = Array.isArray(payload && payload.fieldName)
|
|
89
|
+
? payload.fieldName.slice()
|
|
90
|
+
: [];
|
|
91
|
+
const audit = resolveAuditColumnNames(payload);
|
|
92
|
+
|
|
93
|
+
const merged = [];
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
for (const f of explicit) {
|
|
96
|
+
if (!seen.has(f)) {
|
|
97
|
+
seen.add(f);
|
|
98
|
+
merged.push(f);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const f of audit) {
|
|
102
|
+
if (!seen.has(f)) {
|
|
103
|
+
seen.add(f);
|
|
104
|
+
merged.push(f);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
fields: merged,
|
|
110
|
+
audit,
|
|
111
|
+
explicit
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Deteksi alignment kolom audit standar antara payload generator dan tabel
|
|
117
|
+
* database aktual. Helper ini dipakai oleh sync flow (payload-runner) dan
|
|
118
|
+
* initial generation untuk men-set `auditColumns: false` bila kolom audit
|
|
119
|
+
* tidak tersedia di database.
|
|
120
|
+
*
|
|
121
|
+
* Konvensi: kolom audit standar mengikuti DEFAULT_AUDIT_COLUMNS (snake_case).
|
|
122
|
+
* Match dilakukan exact lowercase; bila tabel pakai naming convention lain
|
|
123
|
+
* (mis. `CreatedAt`), audit detection akan miss.
|
|
124
|
+
*
|
|
125
|
+
* @param {string[]} dbColumns - Daftar nama kolom dari tabel database
|
|
126
|
+
* @returns {{
|
|
127
|
+
* all: boolean, // semua 4 kolom audit standar ada di database
|
|
128
|
+
* partial: boolean, // sebagian (1-3) kolom audit ada
|
|
129
|
+
* none: boolean, // tidak satupun kolom audit ada
|
|
130
|
+
* present: string[], // kolom audit yang ada di database
|
|
131
|
+
* missing: string[] // kolom audit yang tidak ada di database
|
|
132
|
+
* }}
|
|
133
|
+
*/
|
|
134
|
+
function detectAuditAlignment(dbColumns) {
|
|
135
|
+
const columns = Array.isArray(dbColumns) ? dbColumns : [];
|
|
136
|
+
const present = DEFAULT_AUDIT_COLUMNS.filter(col => columns.includes(col));
|
|
137
|
+
const missing = DEFAULT_AUDIT_COLUMNS.filter(col => !columns.includes(col));
|
|
138
|
+
return {
|
|
139
|
+
all: present.length === DEFAULT_AUDIT_COLUMNS.length,
|
|
140
|
+
partial: present.length > 0 && present.length < DEFAULT_AUDIT_COLUMNS.length,
|
|
141
|
+
none: present.length === 0,
|
|
142
|
+
present,
|
|
143
|
+
missing
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Cek apakah konfigurasi `auditColumns` di payload memerlukan resolusi
|
|
149
|
+
* berdasarkan alignment dengan database. Dipakai sync flow untuk men-detect
|
|
150
|
+
* "audit-only drift" yang tidak ter-detect oleh comparator schema reguler.
|
|
151
|
+
*
|
|
152
|
+
* Aturan resolusi:
|
|
153
|
+
* - `auditColumns: false`/`null` -> no-op (preserved)
|
|
154
|
+
* - `auditColumns: { ... }` (object form) -> no auto-override (manual)
|
|
155
|
+
* - `auditColumns: true` atau tidak di-set (default true) -> butuh resolusi
|
|
156
|
+
* bila database tidak punya keempat kolom audit standar (none atau partial)
|
|
157
|
+
*
|
|
158
|
+
* @param {Object} payload - Processed payload object
|
|
159
|
+
* @param {ReturnType<typeof detectAuditAlignment>} auditAlignment
|
|
160
|
+
* @returns {boolean} true bila sync wajib regenerate untuk men-set false
|
|
161
|
+
*/
|
|
162
|
+
function needsAuditColumnResolution(payload, auditAlignment) {
|
|
163
|
+
if (!payload || !auditAlignment) return false;
|
|
164
|
+
const hasField = Object.prototype.hasOwnProperty.call(payload, 'auditColumns');
|
|
165
|
+
const value = hasField ? payload.auditColumns : undefined;
|
|
166
|
+
|
|
167
|
+
if (value === false || value === null) return false;
|
|
168
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return false;
|
|
169
|
+
|
|
170
|
+
return !auditAlignment.all;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
DEFAULT_AUDIT_COLUMN_MAP,
|
|
175
|
+
DEFAULT_AUDIT_COLUMNS,
|
|
176
|
+
isAuditColumnsEnabled,
|
|
177
|
+
resolveAuditColumnNames,
|
|
178
|
+
resolveEffectiveFieldList,
|
|
179
|
+
detectAuditAlignment,
|
|
180
|
+
needsAuditColumnResolution
|
|
181
|
+
};
|
|
@@ -67,6 +67,9 @@ class CliOutput {
|
|
|
67
67
|
? `${data.config.database} (force overwrite)`
|
|
68
68
|
: data.config.database;
|
|
69
69
|
out(` Database ${dbLine}`);
|
|
70
|
+
if (data.config.config) {
|
|
71
|
+
out(` Config ${data.config.config}`);
|
|
72
|
+
}
|
|
70
73
|
out('');
|
|
71
74
|
|
|
72
75
|
if (data.payload) {
|
|
@@ -79,6 +82,20 @@ class CliOutput {
|
|
|
79
82
|
out('');
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
if (data.schemaValidation) {
|
|
86
|
+
out('Schema Validation:');
|
|
87
|
+
if (data.schemaValidation.status === 'ok') {
|
|
88
|
+
const n = data.schemaValidation.columnsChecked || 0;
|
|
89
|
+
out(` Status OK (${n} column${n === 1 ? '' : 's'} match database)`);
|
|
90
|
+
} else if (data.schemaValidation.status === 'skipped') {
|
|
91
|
+
out(' Status SKIPPED (--skip-schema-check)');
|
|
92
|
+
if (data.schemaValidation.reason) {
|
|
93
|
+
out(` Reason ${data.schemaValidation.reason}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
out('');
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
if (data.archive && data.archive.count > 0) {
|
|
83
100
|
out('Archive (force):');
|
|
84
101
|
out(` Generation ${data.archive.generation} - ${data.archive.count} file(s) archived`);
|
|
@@ -242,22 +242,25 @@ class DatabaseIntrospector {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
|
-
* Close database connection
|
|
245
|
+
* Close database connection. Idempotent — panggilan kedua jadi no-op
|
|
246
|
+
* setelah `this.pool` di-null-kan. Ini mencegah pesan "Database connection
|
|
247
|
+
* closed" muncul dua kali ketika caller punya cleanup ganda (mis. cleanup
|
|
248
|
+
* eksplisit + cleanup di catch block).
|
|
246
249
|
*/
|
|
247
250
|
async close() {
|
|
248
|
-
if (this.pool)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
} catch (e) {
|
|
256
|
-
// ignore close errors
|
|
257
|
-
}
|
|
258
|
-
if (!this.quiet) {
|
|
259
|
-
console.log('Database connection closed');
|
|
251
|
+
if (!this.pool) return;
|
|
252
|
+
try {
|
|
253
|
+
if (this.dbType === 'oracle') {
|
|
254
|
+
await this.pool.close(0);
|
|
255
|
+
} else {
|
|
256
|
+
await this.pool.end();
|
|
260
257
|
}
|
|
258
|
+
} catch (e) {
|
|
259
|
+
// ignore close errors
|
|
260
|
+
}
|
|
261
|
+
this.pool = null;
|
|
262
|
+
if (!this.quiet) {
|
|
263
|
+
console.log('Database connection closed');
|
|
261
264
|
}
|
|
262
265
|
}
|
|
263
266
|
|
package/integrity-manifest.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "4.
|
|
3
|
-
"generated": "2026-05-
|
|
2
|
+
"version": "4.2.8",
|
|
3
|
+
"generated": "2026-05-21T01:20:53.402Z",
|
|
4
4
|
"generator": "restforge-build-system",
|
|
5
5
|
"algorithm": "sha256",
|
|
6
6
|
"files": {
|
|
7
|
-
"server.js": "
|
|
8
|
-
"src/utils/license-client.js": "
|
|
9
|
-
"src/utils/trusted-keys.js": "
|
|
10
|
-
"cli/consumer.js": "
|
|
11
|
-
"src/utils/security-checks.js": "
|
|
12
|
-
"scripts/verify-integrity.js": "
|
|
7
|
+
"server.js": "91f5255aacad771c0ff9370c6c76be4a081fa6f264adf1fb5bc77a1f585ddce9",
|
|
8
|
+
"src/utils/license-client.js": "2f9cfbc101e87c9e179a36d7c7e1b8dc2fd78999331efb3752bcb66dafb2364e",
|
|
9
|
+
"src/utils/trusted-keys.js": "2a02c10de6ca639ace51417ac93c74feb00db51a55ebf98f18a831eefef60516",
|
|
10
|
+
"cli/consumer.js": "2815eeba5613515349853107034f3763347a218d1a0ec3f484456a5fb4674143",
|
|
11
|
+
"src/utils/security-checks.js": "04aca0bfcb331befd114e454e0fdeff37a201874394afe0cdba79c9969aed776",
|
|
12
|
+
"scripts/verify-integrity.js": "2f4d06552da2d9204565c3473cde414c24ed6aef326f177acf30ba0ee7d243b8"
|
|
13
13
|
},
|
|
14
14
|
"stats": {
|
|
15
15
|
"totalFiles": 6,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@restforgejs/platform",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.8",
|
|
4
4
|
"description": "RESTForge Platform — Schema-driven backend framework and code generator for full-stack Node.js applications. Generates production backend APIs with multi-database support (PostgreSQL, MySQL, Oracle). A platform builder and backend runtime, not an API testing or client tool.",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|