@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.
Files changed (193) hide show
  1. package/bin/sdf-tools.exe +0 -0
  2. package/build-info.json +2 -2
  3. package/cli/consumer-deploy.js +1 -1
  4. package/cli/consumer.js +1 -1
  5. package/generators/cli/init.js +4 -104
  6. package/generators/cli/payload/migrate.js +96 -96
  7. package/generators/cli/schema/list.js +82 -18
  8. package/generators/cli/schema/migrate.js +23 -3
  9. package/generators/lib/dbschema-kit/apply-engine.js +211 -46
  10. package/generators/lib/dbschema-kit/diff-engine.js +715 -703
  11. package/generators/lib/dbschema-kit/emitters/alter-table.js +96 -2
  12. package/generators/lib/dbschema-kit/introspect-mapper.js +9 -0
  13. package/generators/lib/migrate/backend-payload-migrator.js +221 -221
  14. package/generators/lib/migrate/field-type-resolver.js +325 -319
  15. package/generators/lib/migrate/label-generator.js +38 -38
  16. package/generators/lib/migrate/migrate-runner.js +244 -38
  17. package/generators/lib/migrate/naming.js +52 -43
  18. package/generators/lib/migrate/sql-parser.js +124 -124
  19. package/generators/lib/templates/dashboard-catalog.js +1 -1
  20. package/generators/lib/templates/db-connection-env.js +1 -1
  21. package/generators/lib/templates/dbschema-catalog.js +1 -1
  22. package/generators/lib/templates/field-validation-catalog.js +1 -1
  23. package/generators/lib/templates/mysql-template.js +1 -1
  24. package/generators/lib/templates/oracle-template.js +1 -1
  25. package/generators/lib/templates/postgres-template.js +1 -1
  26. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  27. package/generators/lib/templates/sqlite-template.js +1 -1
  28. package/integrity-manifest.json +18 -18
  29. package/node_modules/brace-expansion/index.js +1 -1
  30. package/node_modules/brace-expansion/package.json +1 -1
  31. package/node_modules/dayjs/CHANGELOG.md +7 -0
  32. package/node_modules/dayjs/README.md +12 -10
  33. package/node_modules/dayjs/dayjs.min.js +1 -1
  34. package/node_modules/dayjs/esm/constant.js +1 -1
  35. package/node_modules/dayjs/esm/plugin/duration/index.js +5 -4
  36. package/node_modules/dayjs/locale.json +1 -1
  37. package/node_modules/dayjs/package.json +2 -2
  38. package/node_modules/dayjs/plugin/duration.js +1 -1
  39. package/node_modules/tmp/lib/tmp.js +37 -7
  40. package/node_modules/tmp/package.json +4 -16
  41. package/package.json +1 -1
  42. package/scripts/verify-integrity.js +1 -1
  43. package/server.js +1 -1
  44. package/src/components/handlers/adjust_handler.js +1 -1
  45. package/src/components/handlers/audit_handler.js +1 -1
  46. package/src/components/handlers/delete_handler.js +1 -1
  47. package/src/components/handlers/export_handler.js +1 -1
  48. package/src/components/handlers/import_handler.js +1 -1
  49. package/src/components/handlers/insert_handler.js +1 -1
  50. package/src/components/handlers/update_handler.js +1 -1
  51. package/src/components/handlers/upload_handler.js +1 -1
  52. package/src/components/handlers/workflow_handler.js +1 -1
  53. package/src/components/integrations/webhook.js +1 -1
  54. package/src/consumers/baseConsumer.js +1 -1
  55. package/src/consumers/declarativeMapper.js +1 -1
  56. package/src/consumers/handlers/apiHandler.js +1 -1
  57. package/src/consumers/handlers/consoleHandler.js +1 -1
  58. package/src/consumers/handlers/databaseHandler.js +1 -1
  59. package/src/consumers/handlers/index.js +1 -1
  60. package/src/consumers/handlers/kafkaHandler.js +1 -1
  61. package/src/consumers/index.js +1 -1
  62. package/src/consumers/messageTransformer.js +1 -1
  63. package/src/consumers/validator.js +1 -1
  64. package/src/core/db/dialect/base-dialect.js +1 -1
  65. package/src/core/db/dialect/index.js +1 -1
  66. package/src/core/db/dialect/mysql-dialect.js +1 -1
  67. package/src/core/db/dialect/oracle-dialect.js +1 -1
  68. package/src/core/db/dialect/postgres-dialect.js +1 -1
  69. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  70. package/src/core/db/flatten-helper.js +1 -1
  71. package/src/core/db/query-builder-error.js +1 -1
  72. package/src/core/db/query-builder.js +1 -1
  73. package/src/core/db/relation-helper.js +1 -1
  74. package/src/core/handlers/delete_handler.js +1 -1
  75. package/src/core/handlers/insert_handler.js +1 -1
  76. package/src/core/handlers/update_handler.js +1 -1
  77. package/src/core/models/base-model.js +1 -1
  78. package/src/core/utils/cache-manager.js +1 -1
  79. package/src/core/utils/component-engine.js +1 -1
  80. package/src/core/utils/context-builder.js +1 -1
  81. package/src/core/utils/datetime-formatter.js +1 -1
  82. package/src/core/utils/datetime-parser.js +1 -1
  83. package/src/core/utils/db.js +1 -1
  84. package/src/core/utils/logger.js +1 -1
  85. package/src/core/utils/payload-loader.js +1 -1
  86. package/src/core/utils/security-checks.js +1 -1
  87. package/src/middleware/body-options.js +1 -1
  88. package/src/middleware/cors.js +1 -1
  89. package/src/middleware/idempotency.js +1 -1
  90. package/src/middleware/rate-limiter.js +1 -1
  91. package/src/middleware/request-logger.js +1 -1
  92. package/src/middleware/security-headers.js +1 -1
  93. package/src/models/base-model-mysql.js +1 -1
  94. package/src/models/base-model-oracle.js +1 -1
  95. package/src/models/base-model-sqlite.js +1 -1
  96. package/src/models/base-model.js +1 -1
  97. package/src/pro/caching/redis-client.js +1 -1
  98. package/src/pro/caching/redis-helper.js +1 -1
  99. package/src/pro/consumers/baseConsumer.js +1 -1
  100. package/src/pro/consumers/declarativeMapper.js +1 -1
  101. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  102. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  103. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  104. package/src/pro/consumers/handlers/index.js +1 -1
  105. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  106. package/src/pro/consumers/index.js +1 -1
  107. package/src/pro/consumers/messageTransformer.js +1 -1
  108. package/src/pro/consumers/validator.js +1 -1
  109. package/src/pro/database/base-model-mysql.js +1 -1
  110. package/src/pro/database/base-model-oracle.js +1 -1
  111. package/src/pro/database/base-model-sqlite.js +1 -1
  112. package/src/pro/database/db-mysql.js +1 -1
  113. package/src/pro/database/db-oracle.js +1 -1
  114. package/src/pro/database/db-sqlite.js +1 -1
  115. package/src/pro/excel/excel-generator.js +1 -1
  116. package/src/pro/excel/excel-parser.js +1 -1
  117. package/src/pro/excel/export-service.js +1 -1
  118. package/src/pro/excel/export_handler.js +1 -1
  119. package/src/pro/excel/import-service.js +1 -1
  120. package/src/pro/excel/import-validator.js +1 -1
  121. package/src/pro/excel/import_handler.js +1 -1
  122. package/src/pro/excel/upsert-builder.js +1 -1
  123. package/src/pro/idgen/idgen-routes.js +1 -1
  124. package/src/pro/integrations/lookup-resolver.js +1 -1
  125. package/src/pro/integrations/upload-handler-v2.js +1 -1
  126. package/src/pro/integrations/upload-handler.js +1 -1
  127. package/src/pro/integrations/webhook.js +1 -1
  128. package/src/pro/locking/lock-routes.js +1 -1
  129. package/src/pro/locking/resource-lock-manager.js +1 -1
  130. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  131. package/src/pro/messaging/kafkaService.js +1 -1
  132. package/src/pro/messaging/messagehubService.js +1 -1
  133. package/src/pro/messaging/rabbitmqService.js +1 -1
  134. package/src/pro/scheduler/job-manager.js +1 -1
  135. package/src/pro/scheduler/job-routes.js +1 -1
  136. package/src/pro/scheduler/job-validator.js +1 -1
  137. package/src/pro/storage/base-storage-provider.js +1 -1
  138. package/src/pro/storage/file-metadata-helper.js +1 -1
  139. package/src/pro/storage/index.js +1 -1
  140. package/src/pro/storage/local-storage-provider.js +1 -1
  141. package/src/pro/storage/s3-storage-provider.js +1 -1
  142. package/src/pro/storage/upload-cleanup-job.js +1 -1
  143. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  144. package/src/pro/storage/upload-pending-tracker.js +1 -1
  145. package/src/pro/websocket/broadcast-helper.js +1 -1
  146. package/src/pro/websocket/index.js +1 -1
  147. package/src/pro/websocket/livesync-server.js +1 -1
  148. package/src/pro/websocket/ws-broadcaster.js +1 -1
  149. package/src/services/export-service.js +1 -1
  150. package/src/services/import-service.js +1 -1
  151. package/src/services/kafkaConsumerService.js +1 -1
  152. package/src/services/kafkaService.js +1 -1
  153. package/src/services/messagehubService.js +1 -1
  154. package/src/services/rabbitmqService.js +1 -1
  155. package/src/utils/cache-invalidation-registry.js +1 -1
  156. package/src/utils/cache-manager.js +1 -1
  157. package/src/utils/component-engine.js +1 -1
  158. package/src/utils/config-extractor.js +1 -1
  159. package/src/utils/consumerLogger.js +1 -1
  160. package/src/utils/context-builder.js +1 -1
  161. package/src/utils/dashboard-helpers.js +1 -1
  162. package/src/utils/dateHelper.js +1 -1
  163. package/src/utils/datetime-formatter.js +1 -1
  164. package/src/utils/datetime-parser.js +1 -1
  165. package/src/utils/db-bootstrap.js +1 -1
  166. package/src/utils/db-mysql.js +1 -1
  167. package/src/utils/db-oracle.js +1 -1
  168. package/src/utils/db-sqlite.js +1 -1
  169. package/src/utils/db.js +1 -1
  170. package/src/utils/demo-generator.js +1 -1
  171. package/src/utils/excel-generator.js +1 -1
  172. package/src/utils/excel-parser.js +1 -1
  173. package/src/utils/file-watcher.js +1 -1
  174. package/src/utils/id-generator.js +1 -1
  175. package/src/utils/idempotency-manager.js +1 -1
  176. package/src/utils/import-validator.js +1 -1
  177. package/src/utils/license-client.js +1 -1
  178. package/src/utils/lock-manager.js +1 -1
  179. package/src/utils/logger.js +1 -1
  180. package/src/utils/lookup-resolver.js +1 -1
  181. package/src/utils/payload-loader.js +1 -1
  182. package/src/utils/processor-response.js +1 -1
  183. package/src/utils/rabbitmq.js +1 -1
  184. package/src/utils/redis-client.js +1 -1
  185. package/src/utils/redis-helper.js +1 -1
  186. package/src/utils/request-scope.js +1 -1
  187. package/src/utils/security-checks.js +1 -1
  188. package/src/utils/service-resolver.js +1 -1
  189. package/src/utils/shutdown-coordinator.js +1 -1
  190. package/src/utils/trusted-keys.js +1 -1
  191. package/src/utils/upload-handler.js +1 -1
  192. package/src/utils/upsert-builder.js +1 -1
  193. 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 };