@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
@@ -1,38 +1,38 @@
1
- 'use strict';
2
-
3
- /**
4
- * Port dari packages/designer/src/migrators/label_generator.rs.
5
- * Generator label field dari snake_case ke human-readable format.
6
- */
7
-
8
- const { snakeToTitle } = require('./naming');
9
-
10
- const LABEL_OVERRIDES = {
11
- is_active: 'Status',
12
- uom: 'UOM',
13
- sku: 'SKU',
14
- email: 'Email',
15
- phone: 'Phone',
16
- url: 'URL',
17
- ip: 'IP',
18
- id: 'ID'
19
- };
20
-
21
- function generateLabel(fieldName, isForeignKey) {
22
- const name = String(fieldName || '');
23
-
24
- if (Object.prototype.hasOwnProperty.call(LABEL_OVERRIDES, name)) {
25
- return LABEL_OVERRIDES[name];
26
- }
27
-
28
- if (isForeignKey && name.endsWith('_id')) {
29
- const stripped = name.slice(0, -3);
30
- if (stripped.length > 0) {
31
- return snakeToTitle(stripped);
32
- }
33
- }
34
-
35
- return snakeToTitle(name);
36
- }
37
-
38
- module.exports = { generateLabel };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Port dari packages/designer/src/migrators/label_generator.rs.
5
+ * Generator label field dari snake_case ke human-readable format.
6
+ */
7
+
8
+ const { snakeToTitle } = require('./naming');
9
+
10
+ const LABEL_OVERRIDES = {
11
+ is_active: 'Status',
12
+ uom: 'UOM',
13
+ sku: 'SKU',
14
+ email: 'Email',
15
+ phone: 'Phone',
16
+ url: 'URL',
17
+ ip: 'IP',
18
+ id: 'ID'
19
+ };
20
+
21
+ function generateLabel(fieldName, isForeignKey) {
22
+ const name = String(fieldName || '');
23
+
24
+ if (Object.prototype.hasOwnProperty.call(LABEL_OVERRIDES, name)) {
25
+ return LABEL_OVERRIDES[name];
26
+ }
27
+
28
+ if (isForeignKey && name.endsWith('_id')) {
29
+ const stripped = name.slice(0, -3);
30
+ if (stripped.length > 0) {
31
+ return snakeToTitle(stripped);
32
+ }
33
+ }
34
+
35
+ return snakeToTitle(name);
36
+ }
37
+
38
+ module.exports = { generateLabel };
@@ -8,10 +8,15 @@
8
8
  * 2. Baca SERVER_ADDRESS dan SERVER_PORT dari config → konstruksi apiBaseUrl
9
9
  * dengan format `http://{host}:{port}/api/{project}`
10
10
  * 3. Load file payload backend (RDF) dari --name (relative ke cwd/payload/
11
- * atau absolute)
12
- * 4. Jalankan migrasi RDF UDF via BackendPayloadMigrator
13
- * 5. Tulis output JSON ke directory --output (file name = basename tanpa ext +
14
- * suffix `-app.json`)
11
+ * atau absolute) dan resolve referensi `file:` pada datatablesQuery/viewQuery
12
+ * 4. Discovery RDF terkait via JOIN di datatablesQuery: setiap tabel yang
13
+ * di-JOIN dimuat RDF-nya dari folder yang sama lalu dikonversi menjadi page
14
+ * tersendiri (1 RDF utama → multi page)
15
+ * 5. Jalankan migrasi RDF → UDF via BackendPayloadMigrator
16
+ * 6. Tulis output dalam format SPLIT multi-file:
17
+ * <output>/app-config.json → { appConfig }
18
+ * <output>/pages/<pageId>.json → { pages: [ <page> ] }
19
+ * <output>/<appCode>.json → aggregator (extends/homepage/include/navigation)
15
20
  */
16
21
 
17
22
  const fs = require('fs');
@@ -20,6 +25,8 @@ const path = require('path');
20
25
  const { resolveConfig, printDefaultConfigWarning } = require('../utils/config-resolver');
21
26
  const { readEnvFile } = require('../utils/env-manager');
22
27
  const { migrate } = require('./backend-payload-migrator');
28
+ const { parseDatatablesQuery } = require('./sql-parser');
29
+ const { kebabToTitle, snakeToKebab } = require('./naming');
23
30
 
24
31
  const DEFAULT_APP_NAME = 'My Application';
25
32
  const DEFAULT_APP_CODE_FALLBACK = 'my-app';
@@ -28,6 +35,8 @@ const DEFAULT_HOST = '127.0.0.1';
28
35
  const DEFAULT_BACKEND_PORT = 3000;
29
36
  const DEFAULT_FRONTEND_PORT = 8000;
30
37
 
38
+ const QUERY_REF_FIELDS = ['datatablesQuery', 'viewQuery'];
39
+
31
40
  function resolveInputPath(nameArg, cwd) {
32
41
  if (path.isAbsolute(nameArg)) return nameArg;
33
42
  const direct = path.resolve(cwd, nameArg);
@@ -38,21 +47,15 @@ function resolveInputPath(nameArg, cwd) {
38
47
  return direct;
39
48
  }
40
49
 
41
- function buildOutputPath(outputArg, inputBaseName, cwd) {
42
- const targetFileName = inputBaseName;
43
-
50
+ function buildOutputDir(outputArg, cwd) {
44
51
  if (!outputArg) {
45
- const defaultDir = path.resolve(cwd, 'frontend', 'payload');
46
- return path.join(defaultDir, targetFileName);
52
+ return path.resolve(cwd, 'frontend', 'payload');
47
53
  }
48
-
49
54
  const abs = path.isAbsolute(outputArg) ? outputArg : path.resolve(cwd, outputArg);
50
-
51
- // Treat as file path bila berakhiran .json (case insensitive)
52
- if (/\.json$/i.test(abs)) return abs;
53
-
54
- // Else treat as directory
55
- return path.join(abs, targetFileName);
55
+ // Toleransi bila --output diakhiri .json: pakai direktori induknya sebagai
56
+ // root output split (output bukan file tunggal lagi).
57
+ if (/\.json$/i.test(abs)) return path.dirname(abs);
58
+ return abs;
56
59
  }
57
60
 
58
61
  function readServerConfig(envFilePath) {
@@ -73,6 +76,116 @@ function buildApiBaseUrl(host, port, project) {
73
76
  return `http://${host}:${port}/api${projectSegment}`;
74
77
  }
75
78
 
79
+ /**
80
+ * Gabungkan aggregator `<project>.json` yang sudah ada dengan hasil run saat ini.
81
+ * Karena nama file aggregator = nama project, run berikutnya untuk project yang
82
+ * sama akan menambahkan (akumulasi) page baru, bukan menimpa.
83
+ *
84
+ * Aturan merge:
85
+ * - `pages[]` : union berdasarkan path `include` (entry existing didahulukan,
86
+ * page baru di-append). Re-migrate page yang sama = idempoten.
87
+ * - `navigation.items`: union berdasarkan `pageRef` (type=page). Item existing
88
+ * dipertahankan sehingga kustomisasi manual (mis. icon/label)
89
+ * pada page lama tidak hilang.
90
+ * - `homepage` : dipertahankan dari file existing (run pertama yang menetapkan).
91
+ * - `extends` : dipertahankan.
92
+ */
93
+ function mergeAggregator(existing, fresh) {
94
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
95
+ return fresh;
96
+ }
97
+
98
+ const seenInclude = new Set();
99
+ const mergedPages = [];
100
+ const pushPages = (list) => {
101
+ if (!Array.isArray(list)) return;
102
+ for (const entry of list) {
103
+ const key = (entry && typeof entry.include === 'string')
104
+ ? entry.include
105
+ : JSON.stringify(entry);
106
+ if (seenInclude.has(key)) continue;
107
+ seenInclude.add(key);
108
+ mergedPages.push(entry);
109
+ }
110
+ };
111
+ pushPages(existing.pages);
112
+ pushPages(fresh.pages);
113
+
114
+ const seenNav = new Set();
115
+ const mergedNav = [];
116
+ const pushNav = (items) => {
117
+ if (!Array.isArray(items)) return;
118
+ for (const item of items) {
119
+ const key = (item && item.type === 'page' && item.pageRef)
120
+ ? `page:${item.pageRef}`
121
+ : JSON.stringify(item);
122
+ if (seenNav.has(key)) continue;
123
+ seenNav.add(key);
124
+ mergedNav.push(item);
125
+ }
126
+ };
127
+ const existingNav = (existing.navigation && Array.isArray(existing.navigation.items))
128
+ ? existing.navigation.items : [];
129
+ const freshNav = (fresh.navigation && Array.isArray(fresh.navigation.items))
130
+ ? fresh.navigation.items : [];
131
+ pushNav(existingNav);
132
+ pushNav(freshNav);
133
+
134
+ return {
135
+ extends: existing.extends || fresh.extends,
136
+ homepage: (typeof existing.homepage === 'string' && existing.homepage)
137
+ ? existing.homepage
138
+ : fresh.homepage,
139
+ pages: mergedPages,
140
+ navigation: { items: mergedNav }
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Resolve referensi `file:` pada query fields (datatablesQuery, viewQuery)
146
+ * menjadi isi SQL inline. Dibaca langsung dari disk relatif terhadap lokasi
147
+ * file RDF, independen dari mode pkg (file SQL ada di project user, bukan
148
+ * di-bundle ke binary). Bila file tidak ditemukan, query di-kosongkan dan
149
+ * sebuah warning dikumpulkan agar parsing JOIN tidak crash.
150
+ */
151
+ function resolveQueryRefs(payload, payloadPath, warnings) {
152
+ const dir = path.dirname(payloadPath);
153
+ const out = Object.assign({}, payload);
154
+ for (const key of QUERY_REF_FIELDS) {
155
+ const val = out[key];
156
+ if (typeof val === 'string' && val.startsWith('file:')) {
157
+ const rel = val.slice(5);
158
+ const full = path.isAbsolute(rel) ? rel : path.resolve(dir, rel);
159
+ if (fs.existsSync(full)) {
160
+ out[key] = fs.readFileSync(full, 'utf8');
161
+ } else {
162
+ out[key] = '';
163
+ warnings.push(`${key} reference not found: ${full} (referenced by ${path.basename(payloadPath)})`);
164
+ }
165
+ }
166
+ }
167
+ return out;
168
+ }
169
+
170
+ function loadRdf(filePath) {
171
+ const content = fs.readFileSync(filePath, 'utf8');
172
+ return JSON.parse(content);
173
+ }
174
+
175
+ /**
176
+ * Petakan nama tabel JOIN (snake_case, mis. visitor_categories) ke file RDF
177
+ * sibling di folder payload. Coba bentuk kebab dulu (visitor-categories.json),
178
+ * lalu bentuk snake (visitor_categories.json).
179
+ */
180
+ function findRelatedRdfPath(tableName, payloadDir) {
181
+ const candidates = [`${snakeToKebab(tableName)}.json`, `${tableName}.json`];
182
+ for (const c of candidates) {
183
+ const p = path.join(payloadDir, c);
184
+ if (fs.existsSync(p)) return { path: p, candidates };
185
+ }
186
+ return { path: null, candidates };
187
+ }
188
+
76
189
  async function run(args) {
77
190
  const cwd = process.cwd();
78
191
 
@@ -110,7 +223,7 @@ async function run(args) {
110
223
  ? args.port
111
224
  : DEFAULT_FRONTEND_PORT;
112
225
 
113
- const appName = args.appName || DEFAULT_APP_NAME;
226
+ const appName = args.appName || kebabToTitle(project) || DEFAULT_APP_NAME;
114
227
  const appCode = args.appCode || project || DEFAULT_APP_CODE_FALLBACK;
115
228
  const plugin = args.plugin || DEFAULT_PLUGIN;
116
229
 
@@ -120,26 +233,49 @@ async function run(args) {
120
233
  err.exitCode = 3;
121
234
  throw err;
122
235
  }
123
- let backendPayload;
236
+
237
+ const warnings = [];
238
+
239
+ let mainPayloadRaw;
124
240
  try {
125
- const content = fs.readFileSync(inputPath, 'utf8');
126
- backendPayload = JSON.parse(content);
241
+ mainPayloadRaw = loadRdf(inputPath);
127
242
  } catch (e) {
128
243
  const err = new Error(`Failed to parse JSON from ${inputPath}: ${e.message}`);
129
244
  err.exitCode = 3;
130
245
  throw err;
131
246
  }
132
247
 
133
- const inputBaseName = path.basename(inputPath);
134
- const outputPath = buildOutputPath(args.output, inputBaseName, cwd);
248
+ // Resolve file: refs pada RDF utama agar JOIN dapat di-parse
249
+ const mainPayload = resolveQueryRefs(mainPayloadRaw, inputPath, warnings);
250
+ const mainTableClean = String(mainPayload.tableName || '').split('.').pop() || '';
135
251
 
136
- if (fs.existsSync(outputPath) && !args.overwrite) {
137
- const err = new Error(`Output file already exists: ${outputPath}. Use --overwrite to replace.`);
138
- err.exitCode = 1;
139
- throw err;
252
+ // Discovery tabel terkait via JOIN (1 level dari RDF utama)
253
+ const payloadDir = path.dirname(inputPath);
254
+ const parsedMain = parseDatatablesQuery(mainPayload.datatablesQuery || '');
255
+ const relatedPayloads = [];
256
+ const seenTables = new Set([mainTableClean]);
257
+ for (const join of parsedMain.joins) {
258
+ const t = String(join.tableName || '').split('.').pop() || '';
259
+ if (!t || seenTables.has(t)) continue;
260
+ seenTables.add(t);
261
+
262
+ const { path: relPath, candidates } = findRelatedRdfPath(t, payloadDir);
263
+ if (!relPath) {
264
+ warnings.push(`Related table '${t}' referenced by JOIN but no RDF file found in ${payloadDir} (tried: ${candidates.join(', ')}); page not generated, only the select reference is kept`);
265
+ continue;
266
+ }
267
+ try {
268
+ const relRaw = loadRdf(relPath);
269
+ relatedPayloads.push(resolveQueryRefs(relRaw, relPath, warnings));
270
+ } catch (e) {
271
+ warnings.push(`Failed to parse related RDF ${relPath}: ${e.message}`);
272
+ }
140
273
  }
141
274
 
142
- const result = migrate([backendPayload], appName, appCode, plugin, apiBaseUrl, frontendPortArg);
275
+ // Urutan page: tabel terkait (master) lebih dulu, RDF utama (detail) terakhir
276
+ const orderedPayloads = relatedPayloads.concat([mainPayload]);
277
+
278
+ const result = migrate(orderedPayloads, appName, appCode, plugin, apiBaseUrl, frontendPortArg);
143
279
  if (!result.success) {
144
280
  const msg = (result.errors && result.errors.length > 0)
145
281
  ? result.errors.join('; ')
@@ -148,30 +284,97 @@ async function run(args) {
148
284
  err.exitCode = 1;
149
285
  throw err;
150
286
  }
287
+ for (const w of (result.warnings || [])) warnings.push(w);
288
+
289
+ const pages = result.payload.pages;
290
+ const appConfig = result.payload.appConfig;
291
+ // homepage = page dari RDF utama (entry terakhir karena di-append paling akhir)
292
+ const mainPageId = pages.length > 0 ? pages[pages.length - 1].pageId : '';
293
+
294
+ const outputDir = buildOutputDir(args.output, cwd);
151
295
 
152
- const outputDir = path.dirname(outputPath);
153
- if (!fs.existsSync(outputDir)) {
154
- fs.mkdirSync(outputDir, { recursive: true });
296
+ // Susun semua file target untuk format split
297
+ const aggregatorPath = path.join(outputDir, `${appCode}.json`);
298
+
299
+ // File app-config + per-page: tunduk pada gate overwrite.
300
+ const pageTargets = [];
301
+ pageTargets.push({
302
+ path: path.join(outputDir, 'app-config.json'),
303
+ content: { appConfig }
304
+ });
305
+ for (const page of pages) {
306
+ pageTargets.push({
307
+ path: path.join(outputDir, 'pages', `${page.pageId}.json`),
308
+ content: { pages: [page] }
309
+ });
310
+ }
311
+
312
+ // Gate overwrite hanya untuk app-config + pages. Aggregator TIDAK dihitung
313
+ // sebagai konflik karena ia di-merge (akumulasi), bukan ditimpa.
314
+ if (!args.overwrite) {
315
+ const existing = pageTargets.filter(t => fs.existsSync(t.path)).map(t => t.path);
316
+ if (existing.length > 0) {
317
+ const err = new Error(`Output file(s) already exist:\n ${existing.join('\n ')}\nUse --overwrite to replace.`);
318
+ err.exitCode = 1;
319
+ throw err;
320
+ }
321
+ }
322
+
323
+ // Aggregator: gabungkan dengan file existing bila project sama (nama file sama)
324
+ const freshAggregator = {
325
+ extends: 'app-config.json',
326
+ homepage: mainPageId,
327
+ pages: pages.map(p => ({ include: `pages/${p.pageId}.json` })),
328
+ navigation: {
329
+ items: pages.map(p => ({ type: 'page', pageRef: p.pageId, label: p.pageTitle }))
330
+ }
331
+ };
332
+ let aggregatorMerged = false;
333
+ let aggregatorContent = freshAggregator;
334
+ if (fs.existsSync(aggregatorPath)) {
335
+ try {
336
+ const existingAgg = JSON.parse(fs.readFileSync(aggregatorPath, 'utf8'));
337
+ aggregatorContent = mergeAggregator(existingAgg, freshAggregator);
338
+ aggregatorMerged = true;
339
+ } catch (e) {
340
+ warnings.push(`Existing aggregator ${path.basename(aggregatorPath)} could not be parsed (${e.message}); it will be replaced`);
341
+ }
342
+ }
343
+
344
+ const targets = pageTargets.concat([{ path: aggregatorPath, content: aggregatorContent }]);
345
+ for (const t of targets) {
346
+ const dir = path.dirname(t.path);
347
+ if (!fs.existsSync(dir)) {
348
+ fs.mkdirSync(dir, { recursive: true });
349
+ }
350
+ fs.writeFileSync(t.path, JSON.stringify(t.content, null, 2), 'utf8');
155
351
  }
156
- fs.writeFileSync(outputPath, JSON.stringify(result.payload, null, 2), 'utf8');
157
352
 
158
353
  const stdout = process.stdout;
159
354
  stdout.write('============================================================\n');
160
- stdout.write('PAYLOAD MIGRATE - RDF (backend) -> UDF (frontend)\n');
355
+ stdout.write('PAYLOAD MIGRATE - RDF (backend) -> UDF (frontend, split)\n');
161
356
  stdout.write('============================================================\n\n');
162
357
  stdout.write(` Input : ${inputPath}\n`);
163
- stdout.write(` Output : ${outputPath}\n`);
358
+ stdout.write(` Output dir : ${outputDir}\n`);
164
359
  stdout.write(` Project : ${project}\n`);
165
360
  stdout.write(` apiBaseUrl : ${apiBaseUrl}\n`);
166
361
  stdout.write(` Backend port : ${backendPort}\n`);
167
362
  stdout.write(` Frontend port: ${frontendPortArg}\n`);
168
- stdout.write(` Pages : ${result.pageResults.length}\n\n`);
363
+ stdout.write(` Homepage : ${aggregatorContent.homepage}\n`);
364
+ stdout.write(` Pages (run) : ${result.pageResults.length}\n`);
365
+ stdout.write(` Pages (app) : ${aggregatorContent.pages.length}${aggregatorMerged ? ' (merged with existing)' : ''}\n\n`);
169
366
  for (const pr of result.pageResults) {
170
367
  stdout.write(` [OK] ${pr.pageId}: ${pr.fieldCount} field(s), ${pr.tableColCount} table column(s)\n`);
171
368
  }
172
- if (result.warnings && result.warnings.length > 0) {
369
+ stdout.write('\n Files written:\n');
370
+ for (const t of targets) {
371
+ const rel = path.relative(outputDir, t.path) || path.basename(t.path);
372
+ const tag = (t.path === aggregatorPath && aggregatorMerged) ? ' (merged)' : '';
373
+ stdout.write(` - ${rel}${tag}\n`);
374
+ }
375
+ if (warnings.length > 0) {
173
376
  stdout.write('\n Warnings:\n');
174
- for (const w of result.warnings) {
377
+ for (const w of warnings) {
175
378
  stdout.write(` - ${w}\n`);
176
379
  }
177
380
  }
@@ -181,7 +384,10 @@ async function run(args) {
181
384
  module.exports = {
182
385
  run,
183
386
  resolveInputPath,
184
- buildOutputPath,
387
+ buildOutputDir,
388
+ resolveQueryRefs,
389
+ findRelatedRdfPath,
185
390
  readServerConfig,
186
- buildApiBaseUrl
391
+ buildApiBaseUrl,
392
+ mergeAggregator
187
393
  };
@@ -1,43 +1,52 @@
1
- 'use strict';
2
-
3
- /**
4
- * Port dari packages/designer/src/utils/naming.rs.
5
- * Konversi naming convention: snake_case -> kebab-case / camelCase / PascalCase / Title Case.
6
- */
7
-
8
- function capitalize(s) {
9
- if (!s || s.length === 0) return '';
10
- return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
11
- }
12
-
13
- function snakeToKebab(name) {
14
- return String(name || '').replace(/_/g, '-');
15
- }
16
-
17
- function snakeToCamel(name) {
18
- const parts = String(name || '').split('_');
19
- if (parts.length === 0) return '';
20
- const first = parts.shift();
21
- return first + parts.map(capitalize).join('');
22
- }
23
-
24
- function snakeToPascal(name) {
25
- return String(name || '').split('_').map(capitalize).join('');
26
- }
27
-
28
- function snakeToTitle(name) {
29
- return String(name || '').split('_').map(capitalize).join(' ');
30
- }
31
-
32
- function toClassName(appCode) {
33
- return String(appCode || '').replace(/_/g, '-').split('-').map(capitalize).join('');
34
- }
35
-
36
- module.exports = {
37
- capitalize,
38
- snakeToKebab,
39
- snakeToCamel,
40
- snakeToPascal,
41
- snakeToTitle,
42
- toClassName
43
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Port dari packages/designer/src/utils/naming.rs.
5
+ * Konversi naming convention: snake_case -> kebab-case / camelCase / PascalCase / Title Case.
6
+ */
7
+
8
+ function capitalize(s) {
9
+ if (!s || s.length === 0) return '';
10
+ return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
11
+ }
12
+
13
+ function snakeToKebab(name) {
14
+ return String(name || '').replace(/_/g, '-');
15
+ }
16
+
17
+ function snakeToCamel(name) {
18
+ const parts = String(name || '').split('_');
19
+ if (parts.length === 0) return '';
20
+ const first = parts.shift();
21
+ return first + parts.map(capitalize).join('');
22
+ }
23
+
24
+ function snakeToPascal(name) {
25
+ return String(name || '').split('_').map(capitalize).join('');
26
+ }
27
+
28
+ function snakeToTitle(name) {
29
+ return String(name || '').split('_').map(capitalize).join(' ');
30
+ }
31
+
32
+ function kebabToTitle(name) {
33
+ return String(name || '')
34
+ .split(/[-_]+/)
35
+ .filter(Boolean)
36
+ .map(capitalize)
37
+ .join(' ');
38
+ }
39
+
40
+ function toClassName(appCode) {
41
+ return String(appCode || '').replace(/_/g, '-').split('-').map(capitalize).join('');
42
+ }
43
+
44
+ module.exports = {
45
+ capitalize,
46
+ snakeToKebab,
47
+ snakeToCamel,
48
+ snakeToPascal,
49
+ snakeToTitle,
50
+ kebabToTitle,
51
+ toClassName
52
+ };