@restforgejs/platform 4.3.8 → 5.0.0
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/payload/migrate.js +96 -96
- package/generators/lib/dbschema-kit/apply-engine.js +211 -46
- package/generators/lib/dbschema-kit/diff-engine.js +14 -2
- 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 +319 -319
- package/generators/lib/migrate/label-generator.js +38 -38
- package/generators/lib/migrate/migrate-runner.js +187 -187
- package/generators/lib/migrate/naming.js +43 -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
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* - CREATE INDEX
|
|
11
11
|
* - DROP INDEX (dialect-aware)
|
|
12
12
|
* - ADD CONSTRAINT UNIQUE
|
|
13
|
-
* - DROP CONSTRAINT
|
|
13
|
+
* - DROP CONSTRAINT (unique)
|
|
14
|
+
* - ADD CONSTRAINT FOREIGN KEY
|
|
15
|
+
* - DROP CONSTRAINT FOREIGN KEY (MySQL: DROP FOREIGN KEY)
|
|
14
16
|
*
|
|
15
17
|
* Konvensi penamaan constraint/index reuse `naming.js` (generateConstraintName,
|
|
16
18
|
* deriveCompositeShortName) agar nama yang di-emit identik dengan create-table
|
|
@@ -262,6 +264,95 @@ function emitDropUnique(tableIR, columns, dialect, options) {
|
|
|
262
264
|
return `ALTER TABLE ${tableIdentifier} DROP CONSTRAINT ${dialect.quoteIdentifier(name)}`;
|
|
263
265
|
}
|
|
264
266
|
|
|
267
|
+
// ─────────────────────────────────────────────────────────────
|
|
268
|
+
// ADD / DROP FOREIGN KEY
|
|
269
|
+
// ─────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
// Cross-model-validator augments rel with `_targetSchemaName` + `_targetTableName`.
|
|
272
|
+
// Apply path tidak selalu menjalankan validator, jadi fallback dengan split
|
|
273
|
+
// `rel.target` di '.' bila tersedia, lalu treat sebagai schema-less target.
|
|
274
|
+
function resolveTargetIr(rel) {
|
|
275
|
+
if (rel && rel._targetTableName) {
|
|
276
|
+
return { schemaName: rel._targetSchemaName || null, tableName: rel._targetTableName };
|
|
277
|
+
}
|
|
278
|
+
const target = rel && rel.target;
|
|
279
|
+
if (typeof target === 'string' && target.includes('.')) {
|
|
280
|
+
const idx = target.indexOf('.');
|
|
281
|
+
return { schemaName: target.slice(0, idx), tableName: target.slice(idx + 1) };
|
|
282
|
+
}
|
|
283
|
+
return { schemaName: null, tableName: target };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function deriveForeignKeyConstraintName(tableName, relName, dialect, explicitName) {
|
|
287
|
+
if (typeof explicitName === 'string' && explicitName !== '') {
|
|
288
|
+
return explicitName;
|
|
289
|
+
}
|
|
290
|
+
return generateConstraintName('fk', tableName, relName, dialect.maxIdentifierLength);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function emitAddForeignKey(tableIR, relName, rel, dialect) {
|
|
294
|
+
if (!tableIR || typeof tableIR.tableName !== 'string') {
|
|
295
|
+
throw new Error('emitAddForeignKey: tableIR with tableName is required');
|
|
296
|
+
}
|
|
297
|
+
if (typeof relName !== 'string' || relName === '') {
|
|
298
|
+
throw new Error('emitAddForeignKey: relName must be a non-empty string');
|
|
299
|
+
}
|
|
300
|
+
if (!rel || typeof rel !== 'object') {
|
|
301
|
+
throw new Error(`emitAddForeignKey: rel definition required for '${relName}'`);
|
|
302
|
+
}
|
|
303
|
+
if (typeof rel.localKey !== 'string' || rel.localKey === '') {
|
|
304
|
+
throw new Error(`emitAddForeignKey: rel.localKey required for '${relName}'`);
|
|
305
|
+
}
|
|
306
|
+
if (typeof rel.references !== 'string' || rel.references === '') {
|
|
307
|
+
throw new Error(`emitAddForeignKey: rel.references required for '${relName}'`);
|
|
308
|
+
}
|
|
309
|
+
if (dialect.name === 'sqlite') {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`emitAddForeignKey: sqlite does not support ALTER TABLE ADD CONSTRAINT FOREIGN KEY ` +
|
|
312
|
+
`(requires table rebuild). Filter FK operations in apply-engine before calling emitter.`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const tableIdentifier = dialect.formatTableIdentifier(tableIR);
|
|
317
|
+
const constraintName = deriveForeignKeyConstraintName(tableIR.tableName, relName, dialect);
|
|
318
|
+
const local = dialect.quoteIdentifier(rel.localKey);
|
|
319
|
+
const target = dialect.formatTableIdentifier(resolveTargetIr(rel));
|
|
320
|
+
const ref = dialect.quoteIdentifier(rel.references);
|
|
321
|
+
|
|
322
|
+
let stmt = `ALTER TABLE ${tableIdentifier} ADD CONSTRAINT ${dialect.quoteIdentifier(constraintName)} `
|
|
323
|
+
+ `FOREIGN KEY (${local}) REFERENCES ${target} (${ref})`;
|
|
324
|
+
if (rel.onDelete) stmt += ` ON DELETE ${dialect.formatReferentialAction(rel.onDelete)}`;
|
|
325
|
+
if (rel.onUpdate) stmt += ` ON UPDATE ${dialect.formatReferentialAction(rel.onUpdate)}`;
|
|
326
|
+
return stmt;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function emitDropForeignKey(tableIR, relName, dialect, options) {
|
|
330
|
+
if (!tableIR || typeof tableIR.tableName !== 'string') {
|
|
331
|
+
throw new Error('emitDropForeignKey: tableIR with tableName is required');
|
|
332
|
+
}
|
|
333
|
+
if (typeof relName !== 'string' || relName === '') {
|
|
334
|
+
throw new Error('emitDropForeignKey: relName must be a non-empty string');
|
|
335
|
+
}
|
|
336
|
+
if (dialect.name === 'sqlite') {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`emitDropForeignKey: sqlite does not support ALTER TABLE DROP CONSTRAINT FOREIGN KEY ` +
|
|
339
|
+
`(requires table rebuild). Filter FK operations in apply-engine before calling emitter.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const explicitName = options && typeof options.name === 'string' && options.name.length > 0
|
|
344
|
+
? options.name
|
|
345
|
+
: null;
|
|
346
|
+
const constraintName = deriveForeignKeyConstraintName(tableIR.tableName, relName, dialect, explicitName);
|
|
347
|
+
const tableIdentifier = dialect.formatTableIdentifier(tableIR);
|
|
348
|
+
|
|
349
|
+
if (dialect.name === 'mysql') {
|
|
350
|
+
// MySQL: DROP FOREIGN KEY syntax (DROP CONSTRAINT FK hanya supported MySQL 8.0.19+)
|
|
351
|
+
return `ALTER TABLE ${tableIdentifier} DROP FOREIGN KEY ${dialect.quoteIdentifier(constraintName)}`;
|
|
352
|
+
}
|
|
353
|
+
return `ALTER TABLE ${tableIdentifier} DROP CONSTRAINT ${dialect.quoteIdentifier(constraintName)}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
265
356
|
module.exports = {
|
|
266
357
|
emitAddColumn,
|
|
267
358
|
emitDropColumn,
|
|
@@ -270,6 +361,9 @@ module.exports = {
|
|
|
270
361
|
emitDropIndex,
|
|
271
362
|
emitAddUnique,
|
|
272
363
|
emitDropUnique,
|
|
364
|
+
emitAddForeignKey,
|
|
365
|
+
emitDropForeignKey,
|
|
273
366
|
deriveIndexName,
|
|
274
|
-
deriveUniqueConstraintName
|
|
367
|
+
deriveUniqueConstraintName,
|
|
368
|
+
deriveForeignKeyConstraintName
|
|
275
369
|
};
|
|
@@ -120,6 +120,15 @@ function mapTableMetaToIR(tableMeta, dialect) {
|
|
|
120
120
|
_sourceField: null
|
|
121
121
|
};
|
|
122
122
|
|
|
123
|
+
// Preserve the actual DB constraint name (conname). relName above is derived
|
|
124
|
+
// from the target table for SDF generation and does not match the constraint
|
|
125
|
+
// stored in the database, so the apply-engine needs the real conname to emit a
|
|
126
|
+
// DROP that targets an existing constraint. Internal field (prefix `_`): the
|
|
127
|
+
// schema-printer whitelist (VALID_REL_FIELDS) keeps it out of SDF output.
|
|
128
|
+
if (typeof fk.name === 'string' && fk.name !== '') {
|
|
129
|
+
rel._dbConstraintName = fk.name;
|
|
130
|
+
}
|
|
131
|
+
|
|
123
132
|
const onDelete = mapReferentialAction(fk.onDelete);
|
|
124
133
|
if (onDelete) rel.onDelete = onDelete;
|
|
125
134
|
const onUpdate = mapReferentialAction(fk.onUpdate);
|
|
@@ -1,221 +1,221 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Port dari packages/designer/src/migrators/backend_payload_migrator.rs.
|
|
5
|
-
*
|
|
6
|
-
* Orchestrator utama konversi payload backend (RESTForge / RDF) menjadi
|
|
7
|
-
* frontend payload designer (UDF). Menggabungkan output sql-parser +
|
|
8
|
-
* field-type-resolver + label-generator untuk menghasilkan frontend payload
|
|
9
|
-
* deterministic.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const { parseDatatablesQuery } = require('./sql-parser');
|
|
13
|
-
const { FieldTypeResolver } = require('./field-type-resolver');
|
|
14
|
-
const { snakeToKebab, snakeToTitle } = require('./naming');
|
|
15
|
-
|
|
16
|
-
const DEFAULT_ICON = 'file-text';
|
|
17
|
-
const ICON_MAP = {
|
|
18
|
-
contact: 'users',
|
|
19
|
-
customer: 'users',
|
|
20
|
-
user: 'users',
|
|
21
|
-
supplier: 'truck',
|
|
22
|
-
vendor: 'truck',
|
|
23
|
-
category: 'tag',
|
|
24
|
-
item: 'package',
|
|
25
|
-
product: 'package',
|
|
26
|
-
stock: 'clipboard',
|
|
27
|
-
inventory: 'clipboard',
|
|
28
|
-
warehouse: 'building',
|
|
29
|
-
order: 'shopping-cart',
|
|
30
|
-
invoice: 'file-text',
|
|
31
|
-
city: 'map-pin',
|
|
32
|
-
country: 'globe',
|
|
33
|
-
department: 'briefcase',
|
|
34
|
-
employee: 'user'
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
function detectIcon(tableName) {
|
|
38
|
-
const lower = String(tableName || '').toLowerCase();
|
|
39
|
-
for (const keyword of Object.keys(ICON_MAP)) {
|
|
40
|
-
if (lower.includes(keyword)) return ICON_MAP[keyword];
|
|
41
|
-
}
|
|
42
|
-
return DEFAULT_ICON;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function detectDisplayField(resolvedFields, primaryKey) {
|
|
46
|
-
for (const rf of resolvedFields) {
|
|
47
|
-
if (rf.name.endsWith('_name') || rf.name === 'name') return rf.name;
|
|
48
|
-
}
|
|
49
|
-
for (const rf of resolvedFields) {
|
|
50
|
-
if (rf.name.endsWith('_code') || rf.name === 'code') return rf.name;
|
|
51
|
-
}
|
|
52
|
-
for (const rf of resolvedFields) {
|
|
53
|
-
if ((rf.fieldType === 'text' || rf.fieldType === 'textarea') && rf.name !== primaryKey) {
|
|
54
|
-
return rf.name;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (resolvedFields.length > 0) return resolvedFields[0].name;
|
|
58
|
-
return '';
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function convertSinglePage(backend) {
|
|
62
|
-
const warnings = [];
|
|
63
|
-
const tableName = String(backend.tableName || '');
|
|
64
|
-
const primaryKey = String(backend.primaryKey || '');
|
|
65
|
-
const fieldNames = Array.isArray(backend.fieldName) ? backend.fieldName.filter(n => typeof n === 'string') : [];
|
|
66
|
-
const datatablesQuery = String(backend.datatablesQuery || '');
|
|
67
|
-
const datatablesWhere = Array.isArray(backend.datatablesWhere) ? backend.datatablesWhere : [];
|
|
68
|
-
const fieldValidations = Array.isArray(backend.fieldValidation) ? backend.fieldValidation : [];
|
|
69
|
-
|
|
70
|
-
const cleanTable = tableName.split('.').pop() || tableName;
|
|
71
|
-
const parsedQuery = parseDatatablesQuery(datatablesQuery);
|
|
72
|
-
const resolver = new FieldTypeResolver(fieldValidations, parsedQuery, primaryKey);
|
|
73
|
-
|
|
74
|
-
const resolvedFields = fieldNames
|
|
75
|
-
.map(name => resolver.resolve(name))
|
|
76
|
-
.filter(rf => !rf.skip);
|
|
77
|
-
|
|
78
|
-
const displayField = detectDisplayField(resolvedFields, primaryKey);
|
|
79
|
-
|
|
80
|
-
const hasSearch = datatablesWhere.length > 0
|
|
81
|
-
&& datatablesWhere.some(w => typeof w === 'string' && w !== 'all');
|
|
82
|
-
const hasStatusFilter = resolvedFields.some(rf => rf.name === 'is_active' || rf.name === 'status');
|
|
83
|
-
|
|
84
|
-
const features = {
|
|
85
|
-
enableSearch: hasSearch,
|
|
86
|
-
fieldLayout: 'vertical'
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
if (hasStatusFilter) {
|
|
90
|
-
features.enableStatusFilter = true;
|
|
91
|
-
for (const rf of resolvedFields) {
|
|
92
|
-
if ((rf.name === 'is_active' || rf.name === 'status') && rf.fieldType === 'checkbox') {
|
|
93
|
-
const cbt = (rf.extra && rf.extra.checkboxText) || {};
|
|
94
|
-
const checked = typeof cbt.checked === 'string' ? cbt.checked : 'Active';
|
|
95
|
-
const unchecked = typeof cbt.unchecked === 'string' ? cbt.unchecked : 'Inactive';
|
|
96
|
-
features.statusFilter = {
|
|
97
|
-
field: rf.name,
|
|
98
|
-
label: rf.label,
|
|
99
|
-
options: [
|
|
100
|
-
{ value: 'true', text: checked },
|
|
101
|
-
{ value: 'false', text: unchecked }
|
|
102
|
-
]
|
|
103
|
-
};
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const fieldsArray = [];
|
|
110
|
-
for (const rf of resolvedFields) {
|
|
111
|
-
const fieldObj = { name: rf.name, label: rf.label, type: rf.fieldType };
|
|
112
|
-
if (rf.required) fieldObj.required = true;
|
|
113
|
-
if (rf.inTable) {
|
|
114
|
-
fieldObj.inTable = true;
|
|
115
|
-
if (rf.tableOrder !== null && rf.tableOrder !== undefined) {
|
|
116
|
-
fieldObj.tableOrder = rf.tableOrder;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (rf.tableField) fieldObj.tableField = rf.tableField;
|
|
120
|
-
if (rf.defaultValue !== undefined) fieldObj.defaultValue = rf.defaultValue;
|
|
121
|
-
if (rf.extra && typeof rf.extra === 'object') {
|
|
122
|
-
for (const [key, val] of Object.entries(rf.extra)) {
|
|
123
|
-
fieldObj[key] = val;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
fieldsArray.push(fieldObj);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (backend.masterDetail !== undefined && backend.masterDetail !== null) {
|
|
130
|
-
warnings.push(`${cleanTable}: masterDetail is detected — detail tables are not yet supported, only header fields will be converted`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const pageId = snakeToKebab(cleanTable);
|
|
134
|
-
const pageTitle = snakeToTitle(cleanTable);
|
|
135
|
-
|
|
136
|
-
const page = {
|
|
137
|
-
pageId,
|
|
138
|
-
pageTitle,
|
|
139
|
-
pageSubtitle: `Manage ${pageTitle.toLowerCase()} data`,
|
|
140
|
-
pageIcon: detectIcon(cleanTable),
|
|
141
|
-
apiPath: pageId,
|
|
142
|
-
primaryKey,
|
|
143
|
-
displayField,
|
|
144
|
-
features,
|
|
145
|
-
fields: fieldsArray
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
return { page, warnings };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function migrate(backendPayloads, appName, appCode, plugin, apiBaseUrl, port) {
|
|
152
|
-
if (!Array.isArray(backendPayloads) || backendPayloads.length === 0) {
|
|
153
|
-
return {
|
|
154
|
-
success: false,
|
|
155
|
-
payload: null,
|
|
156
|
-
pageResults: [],
|
|
157
|
-
errors: ['No backend payload was provided'],
|
|
158
|
-
warnings: []
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const pages = [];
|
|
163
|
-
const pageResults = [];
|
|
164
|
-
const allWarnings = [];
|
|
165
|
-
|
|
166
|
-
for (let i = 0; i < backendPayloads.length; i++) {
|
|
167
|
-
const backend = backendPayloads[i];
|
|
168
|
-
const tableName = (backend && typeof backend.tableName === 'string') ? backend.tableName : '';
|
|
169
|
-
if (!tableName) {
|
|
170
|
-
allWarnings.push(`Payload #${i + 1}: tableName is not defined, skipped`);
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const { page, warnings } = convertSinglePage(backend);
|
|
175
|
-
|
|
176
|
-
const fieldCount = Array.isArray(page.fields) ? page.fields.length : 0;
|
|
177
|
-
const tableColCount = Array.isArray(page.fields)
|
|
178
|
-
? page.fields.filter(f => f.inTable === true).length
|
|
179
|
-
: 0;
|
|
180
|
-
|
|
181
|
-
pages.push(page);
|
|
182
|
-
pageResults.push({
|
|
183
|
-
pageId: page.pageId,
|
|
184
|
-
fieldCount,
|
|
185
|
-
tableColCount,
|
|
186
|
-
warnings: warnings.slice()
|
|
187
|
-
});
|
|
188
|
-
for (const w of warnings) allWarnings.push(w);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (pages.length === 0) {
|
|
192
|
-
return {
|
|
193
|
-
success: false,
|
|
194
|
-
payload: null,
|
|
195
|
-
pageResults: [],
|
|
196
|
-
errors: ['No page was successfully converted'],
|
|
197
|
-
warnings: allWarnings
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const payload = {
|
|
202
|
-
appConfig: {
|
|
203
|
-
appName,
|
|
204
|
-
appCode,
|
|
205
|
-
plugin,
|
|
206
|
-
apiBaseUrl,
|
|
207
|
-
port
|
|
208
|
-
},
|
|
209
|
-
pages
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
return {
|
|
213
|
-
success: true,
|
|
214
|
-
payload,
|
|
215
|
-
pageResults,
|
|
216
|
-
errors: [],
|
|
217
|
-
warnings: allWarnings
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
module.exports = { migrate, convertSinglePage, detectIcon, detectDisplayField };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Port dari packages/designer/src/migrators/backend_payload_migrator.rs.
|
|
5
|
+
*
|
|
6
|
+
* Orchestrator utama konversi payload backend (RESTForge / RDF) menjadi
|
|
7
|
+
* frontend payload designer (UDF). Menggabungkan output sql-parser +
|
|
8
|
+
* field-type-resolver + label-generator untuk menghasilkan frontend payload
|
|
9
|
+
* deterministic.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { parseDatatablesQuery } = require('./sql-parser');
|
|
13
|
+
const { FieldTypeResolver } = require('./field-type-resolver');
|
|
14
|
+
const { snakeToKebab, snakeToTitle } = require('./naming');
|
|
15
|
+
|
|
16
|
+
const DEFAULT_ICON = 'file-text';
|
|
17
|
+
const ICON_MAP = {
|
|
18
|
+
contact: 'users',
|
|
19
|
+
customer: 'users',
|
|
20
|
+
user: 'users',
|
|
21
|
+
supplier: 'truck',
|
|
22
|
+
vendor: 'truck',
|
|
23
|
+
category: 'tag',
|
|
24
|
+
item: 'package',
|
|
25
|
+
product: 'package',
|
|
26
|
+
stock: 'clipboard',
|
|
27
|
+
inventory: 'clipboard',
|
|
28
|
+
warehouse: 'building',
|
|
29
|
+
order: 'shopping-cart',
|
|
30
|
+
invoice: 'file-text',
|
|
31
|
+
city: 'map-pin',
|
|
32
|
+
country: 'globe',
|
|
33
|
+
department: 'briefcase',
|
|
34
|
+
employee: 'user'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function detectIcon(tableName) {
|
|
38
|
+
const lower = String(tableName || '').toLowerCase();
|
|
39
|
+
for (const keyword of Object.keys(ICON_MAP)) {
|
|
40
|
+
if (lower.includes(keyword)) return ICON_MAP[keyword];
|
|
41
|
+
}
|
|
42
|
+
return DEFAULT_ICON;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function detectDisplayField(resolvedFields, primaryKey) {
|
|
46
|
+
for (const rf of resolvedFields) {
|
|
47
|
+
if (rf.name.endsWith('_name') || rf.name === 'name') return rf.name;
|
|
48
|
+
}
|
|
49
|
+
for (const rf of resolvedFields) {
|
|
50
|
+
if (rf.name.endsWith('_code') || rf.name === 'code') return rf.name;
|
|
51
|
+
}
|
|
52
|
+
for (const rf of resolvedFields) {
|
|
53
|
+
if ((rf.fieldType === 'text' || rf.fieldType === 'textarea') && rf.name !== primaryKey) {
|
|
54
|
+
return rf.name;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (resolvedFields.length > 0) return resolvedFields[0].name;
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function convertSinglePage(backend) {
|
|
62
|
+
const warnings = [];
|
|
63
|
+
const tableName = String(backend.tableName || '');
|
|
64
|
+
const primaryKey = String(backend.primaryKey || '');
|
|
65
|
+
const fieldNames = Array.isArray(backend.fieldName) ? backend.fieldName.filter(n => typeof n === 'string') : [];
|
|
66
|
+
const datatablesQuery = String(backend.datatablesQuery || '');
|
|
67
|
+
const datatablesWhere = Array.isArray(backend.datatablesWhere) ? backend.datatablesWhere : [];
|
|
68
|
+
const fieldValidations = Array.isArray(backend.fieldValidation) ? backend.fieldValidation : [];
|
|
69
|
+
|
|
70
|
+
const cleanTable = tableName.split('.').pop() || tableName;
|
|
71
|
+
const parsedQuery = parseDatatablesQuery(datatablesQuery);
|
|
72
|
+
const resolver = new FieldTypeResolver(fieldValidations, parsedQuery, primaryKey);
|
|
73
|
+
|
|
74
|
+
const resolvedFields = fieldNames
|
|
75
|
+
.map(name => resolver.resolve(name))
|
|
76
|
+
.filter(rf => !rf.skip);
|
|
77
|
+
|
|
78
|
+
const displayField = detectDisplayField(resolvedFields, primaryKey);
|
|
79
|
+
|
|
80
|
+
const hasSearch = datatablesWhere.length > 0
|
|
81
|
+
&& datatablesWhere.some(w => typeof w === 'string' && w !== 'all');
|
|
82
|
+
const hasStatusFilter = resolvedFields.some(rf => rf.name === 'is_active' || rf.name === 'status');
|
|
83
|
+
|
|
84
|
+
const features = {
|
|
85
|
+
enableSearch: hasSearch,
|
|
86
|
+
fieldLayout: 'vertical'
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (hasStatusFilter) {
|
|
90
|
+
features.enableStatusFilter = true;
|
|
91
|
+
for (const rf of resolvedFields) {
|
|
92
|
+
if ((rf.name === 'is_active' || rf.name === 'status') && rf.fieldType === 'checkbox') {
|
|
93
|
+
const cbt = (rf.extra && rf.extra.checkboxText) || {};
|
|
94
|
+
const checked = typeof cbt.checked === 'string' ? cbt.checked : 'Active';
|
|
95
|
+
const unchecked = typeof cbt.unchecked === 'string' ? cbt.unchecked : 'Inactive';
|
|
96
|
+
features.statusFilter = {
|
|
97
|
+
field: rf.name,
|
|
98
|
+
label: rf.label,
|
|
99
|
+
options: [
|
|
100
|
+
{ value: 'true', text: checked },
|
|
101
|
+
{ value: 'false', text: unchecked }
|
|
102
|
+
]
|
|
103
|
+
};
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const fieldsArray = [];
|
|
110
|
+
for (const rf of resolvedFields) {
|
|
111
|
+
const fieldObj = { name: rf.name, label: rf.label, type: rf.fieldType };
|
|
112
|
+
if (rf.required) fieldObj.required = true;
|
|
113
|
+
if (rf.inTable) {
|
|
114
|
+
fieldObj.inTable = true;
|
|
115
|
+
if (rf.tableOrder !== null && rf.tableOrder !== undefined) {
|
|
116
|
+
fieldObj.tableOrder = rf.tableOrder;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (rf.tableField) fieldObj.tableField = rf.tableField;
|
|
120
|
+
if (rf.defaultValue !== undefined) fieldObj.defaultValue = rf.defaultValue;
|
|
121
|
+
if (rf.extra && typeof rf.extra === 'object') {
|
|
122
|
+
for (const [key, val] of Object.entries(rf.extra)) {
|
|
123
|
+
fieldObj[key] = val;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
fieldsArray.push(fieldObj);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (backend.masterDetail !== undefined && backend.masterDetail !== null) {
|
|
130
|
+
warnings.push(`${cleanTable}: masterDetail is detected — detail tables are not yet supported, only header fields will be converted`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pageId = snakeToKebab(cleanTable);
|
|
134
|
+
const pageTitle = snakeToTitle(cleanTable);
|
|
135
|
+
|
|
136
|
+
const page = {
|
|
137
|
+
pageId,
|
|
138
|
+
pageTitle,
|
|
139
|
+
pageSubtitle: `Manage ${pageTitle.toLowerCase()} data`,
|
|
140
|
+
pageIcon: detectIcon(cleanTable),
|
|
141
|
+
apiPath: pageId,
|
|
142
|
+
primaryKey,
|
|
143
|
+
displayField,
|
|
144
|
+
features,
|
|
145
|
+
fields: fieldsArray
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return { page, warnings };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function migrate(backendPayloads, appName, appCode, plugin, apiBaseUrl, port) {
|
|
152
|
+
if (!Array.isArray(backendPayloads) || backendPayloads.length === 0) {
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
payload: null,
|
|
156
|
+
pageResults: [],
|
|
157
|
+
errors: ['No backend payload was provided'],
|
|
158
|
+
warnings: []
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const pages = [];
|
|
163
|
+
const pageResults = [];
|
|
164
|
+
const allWarnings = [];
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < backendPayloads.length; i++) {
|
|
167
|
+
const backend = backendPayloads[i];
|
|
168
|
+
const tableName = (backend && typeof backend.tableName === 'string') ? backend.tableName : '';
|
|
169
|
+
if (!tableName) {
|
|
170
|
+
allWarnings.push(`Payload #${i + 1}: tableName is not defined, skipped`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const { page, warnings } = convertSinglePage(backend);
|
|
175
|
+
|
|
176
|
+
const fieldCount = Array.isArray(page.fields) ? page.fields.length : 0;
|
|
177
|
+
const tableColCount = Array.isArray(page.fields)
|
|
178
|
+
? page.fields.filter(f => f.inTable === true).length
|
|
179
|
+
: 0;
|
|
180
|
+
|
|
181
|
+
pages.push(page);
|
|
182
|
+
pageResults.push({
|
|
183
|
+
pageId: page.pageId,
|
|
184
|
+
fieldCount,
|
|
185
|
+
tableColCount,
|
|
186
|
+
warnings: warnings.slice()
|
|
187
|
+
});
|
|
188
|
+
for (const w of warnings) allWarnings.push(w);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (pages.length === 0) {
|
|
192
|
+
return {
|
|
193
|
+
success: false,
|
|
194
|
+
payload: null,
|
|
195
|
+
pageResults: [],
|
|
196
|
+
errors: ['No page was successfully converted'],
|
|
197
|
+
warnings: allWarnings
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const payload = {
|
|
202
|
+
appConfig: {
|
|
203
|
+
appName,
|
|
204
|
+
appCode,
|
|
205
|
+
plugin,
|
|
206
|
+
apiBaseUrl,
|
|
207
|
+
port
|
|
208
|
+
},
|
|
209
|
+
pages
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
success: true,
|
|
214
|
+
payload,
|
|
215
|
+
pageResults,
|
|
216
|
+
errors: [],
|
|
217
|
+
warnings: allWarnings
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = { migrate, convertSinglePage, detectIcon, detectDisplayField };
|