@restforgejs/platform 4.3.8 → 5.0.1
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/init.js +4 -104
- package/generators/cli/payload/migrate.js +96 -96
- package/generators/cli/schema/list.js +82 -18
- package/generators/cli/schema/migrate.js +23 -3
- package/generators/lib/dbschema-kit/apply-engine.js +211 -46
- package/generators/lib/dbschema-kit/diff-engine.js +715 -703
- package/generators/lib/dbschema-kit/emitters/alter-table.js +96 -2
- package/generators/lib/dbschema-kit/introspect-mapper.js +9 -0
- package/generators/lib/migrate/backend-payload-migrator.js +221 -221
- package/generators/lib/migrate/field-type-resolver.js +325 -319
- package/generators/lib/migrate/label-generator.js +38 -38
- package/generators/lib/migrate/migrate-runner.js +244 -38
- package/generators/lib/migrate/naming.js +52 -43
- package/generators/lib/migrate/sql-parser.js +124 -124
- package/generators/lib/templates/dashboard-catalog.js +1 -1
- package/generators/lib/templates/db-connection-env.js +1 -1
- package/generators/lib/templates/dbschema-catalog.js +1 -1
- package/generators/lib/templates/field-validation-catalog.js +1 -1
- package/generators/lib/templates/mysql-template.js +1 -1
- package/generators/lib/templates/oracle-template.js +1 -1
- package/generators/lib/templates/postgres-template.js +1 -1
- package/generators/lib/templates/query-declarative-catalog.js +1 -1
- package/generators/lib/templates/sqlite-template.js +1 -1
- package/integrity-manifest.json +18 -18
- package/node_modules/brace-expansion/index.js +1 -1
- package/node_modules/brace-expansion/package.json +1 -1
- package/node_modules/dayjs/CHANGELOG.md +7 -0
- package/node_modules/dayjs/README.md +12 -10
- package/node_modules/dayjs/dayjs.min.js +1 -1
- package/node_modules/dayjs/esm/constant.js +1 -1
- package/node_modules/dayjs/esm/plugin/duration/index.js +5 -4
- package/node_modules/dayjs/locale.json +1 -1
- package/node_modules/dayjs/package.json +2 -2
- package/node_modules/dayjs/plugin/duration.js +1 -1
- package/node_modules/tmp/lib/tmp.js +37 -7
- package/node_modules/tmp/package.json +4 -16
- 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
|
@@ -1,319 +1,325 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Port dari packages/designer/src/migrators/field_type_resolver.rs.
|
|
5
|
-
*
|
|
6
|
-
* Rules engine untuk resolve tipe field frontend dari fieldValidations backend +
|
|
7
|
-
* struktur JOIN datatablesQuery. Output berupa ResolvedField yang mendeskripsikan
|
|
8
|
-
* tipe form input (text/number/date/select/checkbox/textarea/dll.), label,
|
|
9
|
-
* posisi tabel, dan extra metadata tambahan per tipe.
|
|
10
|
-
*
|
|
11
|
-
* Perbedaan dengan Rust source:
|
|
12
|
-
* - Rule 1a (PK skip) diperluas: SEMUA field dengan constraints.primaryKey === true
|
|
13
|
-
* akan di-skip dari output fields[] apapun tipenya (uuid, string, integer, ...).
|
|
14
|
-
* Rust source hanya skip jika type=uuid + autoGenerate. Hasil di Rust: tabel
|
|
15
|
-
* dengan PK type=string + autoGenerate (mis. visitors.visitor_id) tetap masuk
|
|
16
|
-
* ke fields[] sebagai text field. PK adalah identifier teknis, tidak perlu
|
|
17
|
-
* ditampilkan di UI form.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const { generateLabel } = require('./label-generator');
|
|
21
|
-
const { snakeToTitle } = require('./naming');
|
|
22
|
-
|
|
23
|
-
const AUDIT_FIELDS = ['created_at', 'created_by', 'updated_at', 'updated_by'];
|
|
24
|
-
const TEXTAREA_FIELDS = ['address', 'description', 'notes'];
|
|
25
|
-
const TEXTAREA_PREFIXES = ['remark'];
|
|
26
|
-
|
|
27
|
-
const CHECKBOX_TEXT_DEFAULT = { checked: 'Yes', unchecked: 'No' };
|
|
28
|
-
const CHECKBOX_TEXT_MAP = {
|
|
29
|
-
is_active: { checked: 'Active', unchecked: 'Inactive' },
|
|
30
|
-
status: { checked: 'Active', unchecked: 'Inactive' }
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const MAXLENGTH_DEFAULTS = {
|
|
34
|
-
code: 20,
|
|
35
|
-
name: 100,
|
|
36
|
-
email: 255,
|
|
37
|
-
phone: 20,
|
|
38
|
-
textarea: 500,
|
|
39
|
-
text: 255
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
function defaultMaxlength(fieldName) {
|
|
43
|
-
if (fieldName.endsWith('_code') || fieldName === 'code') return MAXLENGTH_DEFAULTS.code;
|
|
44
|
-
if (fieldName.endsWith('_name') || fieldName === 'name') return MAXLENGTH_DEFAULTS.name;
|
|
45
|
-
if (fieldName.includes('email')) return MAXLENGTH_DEFAULTS.email;
|
|
46
|
-
if (fieldName.includes('phone')) return MAXLENGTH_DEFAULTS.phone;
|
|
47
|
-
return MAXLENGTH_DEFAULTS.text;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function generatePlaceholder(fieldName, label) {
|
|
51
|
-
if (fieldName.includes('email')) return 'email@example.com';
|
|
52
|
-
if (fieldName.includes('phone')) return '+62xxx';
|
|
53
|
-
if (fieldName.endsWith('_code') || fieldName === 'code') {
|
|
54
|
-
return `Enter ${label.toLowerCase()}`;
|
|
55
|
-
}
|
|
56
|
-
return '';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function guessDisplayCol(join) {
|
|
60
|
-
return `${join.tableName}_name`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function makeSkipped(name) {
|
|
64
|
-
return { name, label: '', fieldType: '', skip: true, required: false, inTable: false, tableOrder: null, tableField: null, defaultValue: undefined, extra: {} };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
class FieldTypeResolver {
|
|
68
|
-
constructor(fieldValidations, parsedQuery, primaryKey) {
|
|
69
|
-
this.parsedQuery = parsedQuery || { selectColumns: [], joins: [], mainTable: '', mainAlias: '' };
|
|
70
|
-
this.primaryKey = String(primaryKey || '');
|
|
71
|
-
|
|
72
|
-
this.validationMap = new Map();
|
|
73
|
-
for (const fv of (fieldValidations || [])) {
|
|
74
|
-
if (fv && typeof fv.name === 'string') {
|
|
75
|
-
this.validationMap.set(fv.name, fv);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
this.joinMap = new Map();
|
|
80
|
-
for (const join of this.parsedQuery.joins) {
|
|
81
|
-
this.joinMap.set(join.localColumn, join);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const joinAliasSet = new Set(this.parsedQuery.joins.map(j => j.tableAlias));
|
|
85
|
-
|
|
86
|
-
this.selectPositions = new Map();
|
|
87
|
-
this.joinDisplayFields = new Map();
|
|
88
|
-
let pkSkipped = false;
|
|
89
|
-
let adjustedPos = 0;
|
|
90
|
-
|
|
91
|
-
for (const col of this.parsedQuery.selectColumns) {
|
|
92
|
-
if (joinAliasSet.has(col.tableAlias)) {
|
|
93
|
-
for (const join of this.parsedQuery.joins) {
|
|
94
|
-
if (join.tableAlias === col.tableAlias) {
|
|
95
|
-
this.joinDisplayFields.set(join.localColumn, col.name);
|
|
96
|
-
adjustedPos += 1;
|
|
97
|
-
this.selectPositions.set(join.localColumn, adjustedPos);
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
} else if (col.name === this.primaryKey && !pkSkipped) {
|
|
102
|
-
pkSkipped = true;
|
|
103
|
-
continue;
|
|
104
|
-
} else {
|
|
105
|
-
adjustedPos += 1;
|
|
106
|
-
this.selectPositions.set(col.name, adjustedPos);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
resolve(fieldName) {
|
|
112
|
-
const validation = this.validationMap.get(fieldName) || null;
|
|
113
|
-
const valType = (validation && typeof validation.type === 'string') ? validation.type : '';
|
|
114
|
-
const constraints = (validation && validation.constraints && typeof validation.constraints === 'object')
|
|
115
|
-
? validation.constraints
|
|
116
|
-
: {};
|
|
117
|
-
|
|
118
|
-
// Rule 0a: Audit fields → skip
|
|
119
|
-
if (AUDIT_FIELDS.includes(fieldName)) {
|
|
120
|
-
return makeSkipped(fieldName);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Rule 0b: Display-only field dari JOIN (column milik table_alias join) → skip
|
|
124
|
-
for (const join of this.parsedQuery.joins) {
|
|
125
|
-
if (!join.tableAlias) continue;
|
|
126
|
-
const belongsToJoin = this.parsedQuery.selectColumns.some(
|
|
127
|
-
col => col.tableAlias === join.tableAlias && col.name === fieldName
|
|
128
|
-
);
|
|
129
|
-
if (belongsToJoin) {
|
|
130
|
-
return makeSkipped(fieldName);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Rule 1a (FIX): SEMUA field dengan constraints.primaryKey=true → skip
|
|
135
|
-
// (extended dari Rust source yang hanya skip type=uuid + autoGenerate)
|
|
136
|
-
if (constraints.primaryKey === true) {
|
|
137
|
-
return makeSkipped(fieldName);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Rule 1b: PK by name tanpa validation → skip
|
|
141
|
-
if (fieldName === this.primaryKey && validation === null) {
|
|
142
|
-
return makeSkipped(fieldName);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// FK + label + posisi tabel
|
|
146
|
-
const isFk = this.joinMap.has(fieldName);
|
|
147
|
-
const label = generateLabel(fieldName, isFk);
|
|
148
|
-
const inTable = this.selectPositions.has(fieldName);
|
|
149
|
-
const tableOrder = this.selectPositions.get(fieldName) || null;
|
|
150
|
-
const tableField = this.joinDisplayFields.get(fieldName) || null;
|
|
151
|
-
const required = constraints.required === true;
|
|
152
|
-
|
|
153
|
-
// Rule 2: Boolean → checkbox
|
|
154
|
-
if (valType === 'boolean') {
|
|
155
|
-
const textMap = CHECKBOX_TEXT_MAP[fieldName] || CHECKBOX_TEXT_DEFAULT;
|
|
156
|
-
const defaultVal = Object.prototype.hasOwnProperty.call(constraints, 'default')
|
|
157
|
-
? constraints.default
|
|
158
|
-
: true;
|
|
159
|
-
return {
|
|
160
|
-
name: fieldName,
|
|
161
|
-
label,
|
|
162
|
-
fieldType: 'checkbox',
|
|
163
|
-
skip: false,
|
|
164
|
-
required: false,
|
|
165
|
-
inTable,
|
|
166
|
-
tableOrder,
|
|
167
|
-
tableField: null,
|
|
168
|
-
defaultValue: defaultVal,
|
|
169
|
-
extra: {
|
|
170
|
-
checkboxText: { checked: textMap.checked, unchecked: textMap.unchecked }
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Rule 3: FK with JOIN → select (API)
|
|
176
|
-
if (isFk) {
|
|
177
|
-
const join = this.joinMap.get(fieldName);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Port dari packages/designer/src/migrators/field_type_resolver.rs.
|
|
5
|
+
*
|
|
6
|
+
* Rules engine untuk resolve tipe field frontend dari fieldValidations backend +
|
|
7
|
+
* struktur JOIN datatablesQuery. Output berupa ResolvedField yang mendeskripsikan
|
|
8
|
+
* tipe form input (text/number/date/select/checkbox/textarea/dll.), label,
|
|
9
|
+
* posisi tabel, dan extra metadata tambahan per tipe.
|
|
10
|
+
*
|
|
11
|
+
* Perbedaan dengan Rust source:
|
|
12
|
+
* - Rule 1a (PK skip) diperluas: SEMUA field dengan constraints.primaryKey === true
|
|
13
|
+
* akan di-skip dari output fields[] apapun tipenya (uuid, string, integer, ...).
|
|
14
|
+
* Rust source hanya skip jika type=uuid + autoGenerate. Hasil di Rust: tabel
|
|
15
|
+
* dengan PK type=string + autoGenerate (mis. visitors.visitor_id) tetap masuk
|
|
16
|
+
* ke fields[] sebagai text field. PK adalah identifier teknis, tidak perlu
|
|
17
|
+
* ditampilkan di UI form.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { generateLabel } = require('./label-generator');
|
|
21
|
+
const { snakeToTitle, snakeToKebab } = require('./naming');
|
|
22
|
+
|
|
23
|
+
const AUDIT_FIELDS = ['created_at', 'created_by', 'updated_at', 'updated_by'];
|
|
24
|
+
const TEXTAREA_FIELDS = ['address', 'description', 'notes'];
|
|
25
|
+
const TEXTAREA_PREFIXES = ['remark'];
|
|
26
|
+
|
|
27
|
+
const CHECKBOX_TEXT_DEFAULT = { checked: 'Yes', unchecked: 'No' };
|
|
28
|
+
const CHECKBOX_TEXT_MAP = {
|
|
29
|
+
is_active: { checked: 'Active', unchecked: 'Inactive' },
|
|
30
|
+
status: { checked: 'Active', unchecked: 'Inactive' }
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const MAXLENGTH_DEFAULTS = {
|
|
34
|
+
code: 20,
|
|
35
|
+
name: 100,
|
|
36
|
+
email: 255,
|
|
37
|
+
phone: 20,
|
|
38
|
+
textarea: 500,
|
|
39
|
+
text: 255
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function defaultMaxlength(fieldName) {
|
|
43
|
+
if (fieldName.endsWith('_code') || fieldName === 'code') return MAXLENGTH_DEFAULTS.code;
|
|
44
|
+
if (fieldName.endsWith('_name') || fieldName === 'name') return MAXLENGTH_DEFAULTS.name;
|
|
45
|
+
if (fieldName.includes('email')) return MAXLENGTH_DEFAULTS.email;
|
|
46
|
+
if (fieldName.includes('phone')) return MAXLENGTH_DEFAULTS.phone;
|
|
47
|
+
return MAXLENGTH_DEFAULTS.text;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function generatePlaceholder(fieldName, label) {
|
|
51
|
+
if (fieldName.includes('email')) return 'email@example.com';
|
|
52
|
+
if (fieldName.includes('phone')) return '+62xxx';
|
|
53
|
+
if (fieldName.endsWith('_code') || fieldName === 'code') {
|
|
54
|
+
return `Enter ${label.toLowerCase()}`;
|
|
55
|
+
}
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function guessDisplayCol(join) {
|
|
60
|
+
return `${join.tableName}_name`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function makeSkipped(name) {
|
|
64
|
+
return { name, label: '', fieldType: '', skip: true, required: false, inTable: false, tableOrder: null, tableField: null, defaultValue: undefined, extra: {} };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class FieldTypeResolver {
|
|
68
|
+
constructor(fieldValidations, parsedQuery, primaryKey) {
|
|
69
|
+
this.parsedQuery = parsedQuery || { selectColumns: [], joins: [], mainTable: '', mainAlias: '' };
|
|
70
|
+
this.primaryKey = String(primaryKey || '');
|
|
71
|
+
|
|
72
|
+
this.validationMap = new Map();
|
|
73
|
+
for (const fv of (fieldValidations || [])) {
|
|
74
|
+
if (fv && typeof fv.name === 'string') {
|
|
75
|
+
this.validationMap.set(fv.name, fv);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.joinMap = new Map();
|
|
80
|
+
for (const join of this.parsedQuery.joins) {
|
|
81
|
+
this.joinMap.set(join.localColumn, join);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const joinAliasSet = new Set(this.parsedQuery.joins.map(j => j.tableAlias));
|
|
85
|
+
|
|
86
|
+
this.selectPositions = new Map();
|
|
87
|
+
this.joinDisplayFields = new Map();
|
|
88
|
+
let pkSkipped = false;
|
|
89
|
+
let adjustedPos = 0;
|
|
90
|
+
|
|
91
|
+
for (const col of this.parsedQuery.selectColumns) {
|
|
92
|
+
if (joinAliasSet.has(col.tableAlias)) {
|
|
93
|
+
for (const join of this.parsedQuery.joins) {
|
|
94
|
+
if (join.tableAlias === col.tableAlias) {
|
|
95
|
+
this.joinDisplayFields.set(join.localColumn, col.name);
|
|
96
|
+
adjustedPos += 1;
|
|
97
|
+
this.selectPositions.set(join.localColumn, adjustedPos);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} else if (col.name === this.primaryKey && !pkSkipped) {
|
|
102
|
+
pkSkipped = true;
|
|
103
|
+
continue;
|
|
104
|
+
} else {
|
|
105
|
+
adjustedPos += 1;
|
|
106
|
+
this.selectPositions.set(col.name, adjustedPos);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
resolve(fieldName) {
|
|
112
|
+
const validation = this.validationMap.get(fieldName) || null;
|
|
113
|
+
const valType = (validation && typeof validation.type === 'string') ? validation.type : '';
|
|
114
|
+
const constraints = (validation && validation.constraints && typeof validation.constraints === 'object')
|
|
115
|
+
? validation.constraints
|
|
116
|
+
: {};
|
|
117
|
+
|
|
118
|
+
// Rule 0a: Audit fields → skip
|
|
119
|
+
if (AUDIT_FIELDS.includes(fieldName)) {
|
|
120
|
+
return makeSkipped(fieldName);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Rule 0b: Display-only field dari JOIN (column milik table_alias join) → skip
|
|
124
|
+
for (const join of this.parsedQuery.joins) {
|
|
125
|
+
if (!join.tableAlias) continue;
|
|
126
|
+
const belongsToJoin = this.parsedQuery.selectColumns.some(
|
|
127
|
+
col => col.tableAlias === join.tableAlias && col.name === fieldName
|
|
128
|
+
);
|
|
129
|
+
if (belongsToJoin) {
|
|
130
|
+
return makeSkipped(fieldName);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Rule 1a (FIX): SEMUA field dengan constraints.primaryKey=true → skip
|
|
135
|
+
// (extended dari Rust source yang hanya skip type=uuid + autoGenerate)
|
|
136
|
+
if (constraints.primaryKey === true) {
|
|
137
|
+
return makeSkipped(fieldName);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Rule 1b: PK by name tanpa validation → skip
|
|
141
|
+
if (fieldName === this.primaryKey && validation === null) {
|
|
142
|
+
return makeSkipped(fieldName);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// FK + label + posisi tabel
|
|
146
|
+
const isFk = this.joinMap.has(fieldName);
|
|
147
|
+
const label = generateLabel(fieldName, isFk);
|
|
148
|
+
const inTable = this.selectPositions.has(fieldName);
|
|
149
|
+
const tableOrder = this.selectPositions.get(fieldName) || null;
|
|
150
|
+
const tableField = this.joinDisplayFields.get(fieldName) || null;
|
|
151
|
+
const required = constraints.required === true;
|
|
152
|
+
|
|
153
|
+
// Rule 2: Boolean → checkbox
|
|
154
|
+
if (valType === 'boolean') {
|
|
155
|
+
const textMap = CHECKBOX_TEXT_MAP[fieldName] || CHECKBOX_TEXT_DEFAULT;
|
|
156
|
+
const defaultVal = Object.prototype.hasOwnProperty.call(constraints, 'default')
|
|
157
|
+
? constraints.default
|
|
158
|
+
: true;
|
|
159
|
+
return {
|
|
160
|
+
name: fieldName,
|
|
161
|
+
label,
|
|
162
|
+
fieldType: 'checkbox',
|
|
163
|
+
skip: false,
|
|
164
|
+
required: false,
|
|
165
|
+
inTable,
|
|
166
|
+
tableOrder,
|
|
167
|
+
tableField: null,
|
|
168
|
+
defaultValue: defaultVal,
|
|
169
|
+
extra: {
|
|
170
|
+
checkboxText: { checked: textMap.checked, unchecked: textMap.unchecked }
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Rule 3: FK with JOIN → select (API)
|
|
176
|
+
if (isFk) {
|
|
177
|
+
const join = this.joinMap.get(fieldName);
|
|
178
|
+
// Display column diambil dari kolom JOIN aktual yang dipilih di SELECT
|
|
179
|
+
// (mis. category_name), bukan tebakan `<table>_name`, sehingga `select`
|
|
180
|
+
// dan `tableField` konsisten.
|
|
181
|
+
const displayCol = this.joinDisplayFields.get(fieldName) || guessDisplayCol(join);
|
|
182
|
+
return {
|
|
183
|
+
name: fieldName,
|
|
184
|
+
label,
|
|
185
|
+
fieldType: 'select',
|
|
186
|
+
skip: false,
|
|
187
|
+
required,
|
|
188
|
+
inTable,
|
|
189
|
+
tableOrder,
|
|
190
|
+
tableField,
|
|
191
|
+
defaultValue: undefined,
|
|
192
|
+
extra: {
|
|
193
|
+
dataSource: {
|
|
194
|
+
type: 'api',
|
|
195
|
+
// resource = kebab-case agar cocok dengan apiPath endpoint
|
|
196
|
+
// (POST <apiBaseUrl>/<resource>/lookup). tableName SQL bersifat
|
|
197
|
+
// snake_case (visitor_categories) sehingga harus dikonversi.
|
|
198
|
+
resource: snakeToKebab(join.tableName),
|
|
199
|
+
select: [join.remoteColumn, displayCol]
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Rule 4: Number
|
|
206
|
+
if (valType === 'number') {
|
|
207
|
+
const extra = {};
|
|
208
|
+
for (const key of ['min', 'max', 'step']) {
|
|
209
|
+
if (Object.prototype.hasOwnProperty.call(constraints, key)) {
|
|
210
|
+
extra[key] = constraints[key];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
name: fieldName,
|
|
215
|
+
label,
|
|
216
|
+
fieldType: 'number',
|
|
217
|
+
skip: false,
|
|
218
|
+
required,
|
|
219
|
+
inTable,
|
|
220
|
+
tableOrder,
|
|
221
|
+
tableField: null,
|
|
222
|
+
defaultValue: undefined,
|
|
223
|
+
extra
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Rule 5: String with enum → select (static)
|
|
228
|
+
if (valType === 'string' && Array.isArray(constraints.enum) && constraints.enum.length > 0) {
|
|
229
|
+
const options = constraints.enum
|
|
230
|
+
.filter(v => typeof v === 'string')
|
|
231
|
+
.map(s => ({ value: s, text: snakeToTitle(s) }));
|
|
232
|
+
return {
|
|
233
|
+
name: fieldName,
|
|
234
|
+
label,
|
|
235
|
+
fieldType: 'select',
|
|
236
|
+
skip: false,
|
|
237
|
+
required,
|
|
238
|
+
inTable,
|
|
239
|
+
tableOrder,
|
|
240
|
+
tableField: null,
|
|
241
|
+
defaultValue: undefined,
|
|
242
|
+
extra: {
|
|
243
|
+
dataSource: { type: 'static', options }
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Rule 6: Date
|
|
249
|
+
if (fieldName.endsWith('_date') || fieldName === 'date') {
|
|
250
|
+
return {
|
|
251
|
+
name: fieldName,
|
|
252
|
+
label,
|
|
253
|
+
fieldType: 'date',
|
|
254
|
+
skip: false,
|
|
255
|
+
required,
|
|
256
|
+
inTable,
|
|
257
|
+
tableOrder,
|
|
258
|
+
tableField: null,
|
|
259
|
+
defaultValue: undefined,
|
|
260
|
+
extra: {}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Rule 7: Time
|
|
265
|
+
if (fieldName.endsWith('_time') || fieldName === 'time') {
|
|
266
|
+
return {
|
|
267
|
+
name: fieldName,
|
|
268
|
+
label,
|
|
269
|
+
fieldType: 'time',
|
|
270
|
+
skip: false,
|
|
271
|
+
required,
|
|
272
|
+
inTable,
|
|
273
|
+
tableOrder,
|
|
274
|
+
tableField: null,
|
|
275
|
+
defaultValue: undefined,
|
|
276
|
+
extra: {}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Rule 8: Textarea
|
|
281
|
+
const isTextarea = TEXTAREA_FIELDS.includes(fieldName)
|
|
282
|
+
|| TEXTAREA_PREFIXES.some(p => fieldName.startsWith(p));
|
|
283
|
+
if (isTextarea) {
|
|
284
|
+
const maxlen = (typeof constraints.maxLength === 'number')
|
|
285
|
+
? constraints.maxLength
|
|
286
|
+
: MAXLENGTH_DEFAULTS.textarea;
|
|
287
|
+
return {
|
|
288
|
+
name: fieldName,
|
|
289
|
+
label,
|
|
290
|
+
fieldType: 'textarea',
|
|
291
|
+
skip: false,
|
|
292
|
+
required,
|
|
293
|
+
inTable,
|
|
294
|
+
tableOrder,
|
|
295
|
+
tableField: null,
|
|
296
|
+
defaultValue: undefined,
|
|
297
|
+
extra: { rows: 3, maxlength: maxlen }
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Rule 9: Default → text
|
|
302
|
+
const maxlen = (typeof constraints.maxLength === 'number')
|
|
303
|
+
? constraints.maxLength
|
|
304
|
+
: defaultMaxlength(fieldName);
|
|
305
|
+
const extra = { maxlength: maxlen };
|
|
306
|
+
const placeholder = generatePlaceholder(fieldName, label);
|
|
307
|
+
if (placeholder) {
|
|
308
|
+
extra.placeholder = placeholder;
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
name: fieldName,
|
|
312
|
+
label,
|
|
313
|
+
fieldType: 'text',
|
|
314
|
+
skip: false,
|
|
315
|
+
required,
|
|
316
|
+
inTable,
|
|
317
|
+
tableOrder,
|
|
318
|
+
tableField: null,
|
|
319
|
+
defaultValue: undefined,
|
|
320
|
+
extra
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = { FieldTypeResolver };
|