@restforgejs/platform 5.2.4 → 5.2.11

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 (176) hide show
  1. package/bin/drift-check-linux +0 -0
  2. package/bin/sdf-tools-linux +0 -0
  3. package/build-info.json +2 -2
  4. package/cli/consumer-deploy.js +1 -1
  5. package/cli/consumer.js +1 -1
  6. package/generators/cli/fast-track.js +235 -30
  7. package/generators/cli/schema/init.js +20 -6
  8. package/generators/cli/schema/template.js +24 -9
  9. package/generators/lib/migrate/backend-payload-migrator.js +39 -17
  10. package/generators/lib/migrate/field-type-resolver.js +64 -7
  11. package/generators/lib/payload/payload-runner.js +72 -6
  12. package/generators/lib/templates/dashboard-catalog.js +1 -1
  13. package/generators/lib/templates/db-connection-env.js +1 -1
  14. package/generators/lib/templates/dbschema-catalog.js +1 -1
  15. package/generators/lib/templates/field-validation-catalog.js +1 -1
  16. package/generators/lib/templates/mysql-template.js +1 -1
  17. package/generators/lib/templates/oracle-template.js +1 -1
  18. package/generators/lib/templates/postgres-template.js +1 -1
  19. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  20. package/generators/lib/templates/sqlite-template.js +1 -1
  21. package/integrity-manifest.json +18 -18
  22. package/package.json +1 -1
  23. package/scripts/verify-integrity.js +1 -1
  24. package/server.js +1 -1
  25. package/src/components/handlers/adjust_handler.js +1 -1
  26. package/src/components/handlers/audit_handler.js +1 -1
  27. package/src/components/handlers/delete_handler.js +1 -1
  28. package/src/components/handlers/export_handler.js +1 -1
  29. package/src/components/handlers/import_handler.js +1 -1
  30. package/src/components/handlers/insert_handler.js +1 -1
  31. package/src/components/handlers/update_handler.js +1 -1
  32. package/src/components/handlers/upload_handler.js +1 -1
  33. package/src/components/handlers/workflow_handler.js +1 -1
  34. package/src/components/integrations/webhook.js +1 -1
  35. package/src/consumers/baseConsumer.js +1 -1
  36. package/src/consumers/declarativeMapper.js +1 -1
  37. package/src/consumers/handlers/apiHandler.js +1 -1
  38. package/src/consumers/handlers/consoleHandler.js +1 -1
  39. package/src/consumers/handlers/databaseHandler.js +1 -1
  40. package/src/consumers/handlers/index.js +1 -1
  41. package/src/consumers/handlers/kafkaHandler.js +1 -1
  42. package/src/consumers/index.js +1 -1
  43. package/src/consumers/messageTransformer.js +1 -1
  44. package/src/consumers/validator.js +1 -1
  45. package/src/core/db/dialect/base-dialect.js +1 -1
  46. package/src/core/db/dialect/index.js +1 -1
  47. package/src/core/db/dialect/mysql-dialect.js +1 -1
  48. package/src/core/db/dialect/oracle-dialect.js +1 -1
  49. package/src/core/db/dialect/postgres-dialect.js +1 -1
  50. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  51. package/src/core/db/flatten-helper.js +1 -1
  52. package/src/core/db/query-builder-error.js +1 -1
  53. package/src/core/db/query-builder.js +1 -1
  54. package/src/core/db/relation-helper.js +1 -1
  55. package/src/core/handlers/delete_handler.js +1 -1
  56. package/src/core/handlers/insert_handler.js +1 -1
  57. package/src/core/handlers/update_handler.js +1 -1
  58. package/src/core/models/base-model.js +1 -1
  59. package/src/core/utils/cache-manager.js +1 -1
  60. package/src/core/utils/component-engine.js +1 -1
  61. package/src/core/utils/context-builder.js +1 -1
  62. package/src/core/utils/datetime-formatter.js +1 -1
  63. package/src/core/utils/datetime-parser.js +1 -1
  64. package/src/core/utils/db.js +1 -1
  65. package/src/core/utils/logger.js +1 -1
  66. package/src/core/utils/payload-loader.js +1 -1
  67. package/src/core/utils/security-checks.js +1 -1
  68. package/src/middleware/body-options.js +1 -1
  69. package/src/middleware/cors.js +1 -1
  70. package/src/middleware/idempotency.js +1 -1
  71. package/src/middleware/rate-limiter.js +1 -1
  72. package/src/middleware/request-logger.js +1 -1
  73. package/src/middleware/security-headers.js +1 -1
  74. package/src/models/base-model-mysql.js +1 -1
  75. package/src/models/base-model-oracle.js +1 -1
  76. package/src/models/base-model-sqlite.js +1 -1
  77. package/src/models/base-model.js +1 -1
  78. package/src/pro/caching/redis-client.js +1 -1
  79. package/src/pro/caching/redis-helper.js +1 -1
  80. package/src/pro/consumers/baseConsumer.js +1 -1
  81. package/src/pro/consumers/declarativeMapper.js +1 -1
  82. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  83. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  84. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  85. package/src/pro/consumers/handlers/index.js +1 -1
  86. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  87. package/src/pro/consumers/index.js +1 -1
  88. package/src/pro/consumers/messageTransformer.js +1 -1
  89. package/src/pro/consumers/validator.js +1 -1
  90. package/src/pro/database/base-model-mysql.js +1 -1
  91. package/src/pro/database/base-model-oracle.js +1 -1
  92. package/src/pro/database/base-model-sqlite.js +1 -1
  93. package/src/pro/database/db-mysql.js +1 -1
  94. package/src/pro/database/db-oracle.js +1 -1
  95. package/src/pro/database/db-sqlite.js +1 -1
  96. package/src/pro/excel/excel-generator.js +1 -1
  97. package/src/pro/excel/excel-parser.js +1 -1
  98. package/src/pro/excel/export-service.js +1 -1
  99. package/src/pro/excel/export_handler.js +1 -1
  100. package/src/pro/excel/import-service.js +1 -1
  101. package/src/pro/excel/import-validator.js +1 -1
  102. package/src/pro/excel/import_handler.js +1 -1
  103. package/src/pro/excel/upsert-builder.js +1 -1
  104. package/src/pro/idgen/idgen-routes.js +1 -1
  105. package/src/pro/integrations/lookup-resolver.js +1 -1
  106. package/src/pro/integrations/upload-handler-v2.js +1 -1
  107. package/src/pro/integrations/upload-handler.js +1 -1
  108. package/src/pro/integrations/webhook.js +1 -1
  109. package/src/pro/locking/lock-routes.js +1 -1
  110. package/src/pro/locking/resource-lock-manager.js +1 -1
  111. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  112. package/src/pro/messaging/kafkaService.js +1 -1
  113. package/src/pro/messaging/messagehubService.js +1 -1
  114. package/src/pro/messaging/rabbitmqService.js +1 -1
  115. package/src/pro/scheduler/job-manager.js +1 -1
  116. package/src/pro/scheduler/job-routes.js +1 -1
  117. package/src/pro/scheduler/job-validator.js +1 -1
  118. package/src/pro/storage/base-storage-provider.js +1 -1
  119. package/src/pro/storage/file-metadata-helper.js +1 -1
  120. package/src/pro/storage/index.js +1 -1
  121. package/src/pro/storage/local-storage-provider.js +1 -1
  122. package/src/pro/storage/s3-storage-provider.js +1 -1
  123. package/src/pro/storage/upload-cleanup-job.js +1 -1
  124. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  125. package/src/pro/storage/upload-pending-tracker.js +1 -1
  126. package/src/pro/websocket/broadcast-helper.js +1 -1
  127. package/src/pro/websocket/index.js +1 -1
  128. package/src/pro/websocket/livesync-server.js +1 -1
  129. package/src/pro/websocket/ws-broadcaster.js +1 -1
  130. package/src/services/export-service.js +1 -1
  131. package/src/services/import-service.js +1 -1
  132. package/src/services/kafkaConsumerService.js +1 -1
  133. package/src/services/kafkaService.js +1 -1
  134. package/src/services/messagehubService.js +1 -1
  135. package/src/services/rabbitmqService.js +1 -1
  136. package/src/utils/cache-invalidation-registry.js +1 -1
  137. package/src/utils/cache-manager.js +1 -1
  138. package/src/utils/component-engine.js +1 -1
  139. package/src/utils/config-extractor.js +1 -1
  140. package/src/utils/consumerLogger.js +1 -1
  141. package/src/utils/context-builder.js +1 -1
  142. package/src/utils/dashboard-helpers.js +1 -1
  143. package/src/utils/dateHelper.js +1 -1
  144. package/src/utils/datetime-formatter.js +1 -1
  145. package/src/utils/datetime-parser.js +1 -1
  146. package/src/utils/db-bootstrap.js +1 -1
  147. package/src/utils/db-mysql.js +1 -1
  148. package/src/utils/db-oracle.js +1 -1
  149. package/src/utils/db-sqlite.js +1 -1
  150. package/src/utils/db.js +1 -1
  151. package/src/utils/demo-generator.js +1 -1
  152. package/src/utils/excel-generator.js +1 -1
  153. package/src/utils/excel-parser.js +1 -1
  154. package/src/utils/file-watcher.js +1 -1
  155. package/src/utils/id-generator.js +1 -1
  156. package/src/utils/idempotency-manager.js +1 -1
  157. package/src/utils/import-validator.js +1 -1
  158. package/src/utils/license-client.js +1 -1
  159. package/src/utils/lock-manager.js +1 -1
  160. package/src/utils/logger.js +1 -1
  161. package/src/utils/lookup-resolver.js +1 -1
  162. package/src/utils/payload-loader.js +1 -1
  163. package/src/utils/processor-response.js +1 -1
  164. package/src/utils/rabbitmq.js +1 -1
  165. package/src/utils/redis-client.js +1 -1
  166. package/src/utils/redis-helper.js +1 -1
  167. package/src/utils/request-scope.js +1 -1
  168. package/src/utils/security-checks.js +1 -1
  169. package/src/utils/service-resolver.js +1 -1
  170. package/src/utils/shutdown-coordinator.js +1 -1
  171. package/src/utils/soft-delete-dashboard-guard.js +1 -1
  172. package/src/utils/sql-table-extractor.js +1 -1
  173. package/src/utils/trusted-keys.js +1 -1
  174. package/src/utils/upload-handler.js +1 -1
  175. package/src/utils/upsert-builder.js +1 -1
  176. package/src/utils/workflow-hook-executor.js +1 -1
@@ -398,6 +398,111 @@ async function collectConfig(args, ask, fileCfg = {}) {
398
398
  return cfg;
399
399
  }
400
400
 
401
+ /**
402
+ * Versi non-interaktif dari `collectConfig`: hitung cfg final dari fileCfg +
403
+ * DEFAULTS dengan urutan prioritas yang SAMA PERSIS dengan default per-field
404
+ * yang dipakai `collectConfig` (lihat masing-masing `askField` di atas), tanpa
405
+ * menanyakan apa pun ke user. Dipakai saat user memilih "Continue" pada
406
+ * existing-config summary.
407
+ */
408
+ function defaultCfgFromFile(args, fileCfg = {}) {
409
+ const cfg = {};
410
+ cfg.LICENSE = args.license || fileCfg.LICENSE || DEFAULTS.LICENSE;
411
+ cfg.SERVER_ADDRESS = fileCfg.SERVER_ADDRESS || DEFAULTS.SERVER_ADDRESS;
412
+ cfg.SERVER_PORT = fileCfg.SERVER_PORT || DEFAULTS.SERVER_PORT;
413
+ cfg.WEB_SERVER_PORT = DEFAULTS.WEB_SERVER_PORT;
414
+ cfg.DB_TYPE = (fileCfg.DB_TYPE || DEFAULTS.DB_TYPE).toLowerCase();
415
+
416
+ if (cfg.DB_TYPE === 'sqlite') {
417
+ cfg.DB_FILE = fileCfg.DB_FILE || DEFAULTS.DB_FILE;
418
+ cfg.DB_NAME = cfg.DB_FILE;
419
+ } else {
420
+ const dbDef = DB_TYPE_DEFAULTS[cfg.DB_TYPE] || {};
421
+ cfg.DB_HOST = fileCfg.DB_HOST || DEFAULTS.DB_HOST;
422
+ cfg.DB_PORT = fileCfg.DB_PORT || dbDef.DB_PORT || DEFAULTS.DB_PORT;
423
+ cfg.DB_USER = fileCfg.DB_USER || dbDef.DB_USER || DEFAULTS.DB_USER;
424
+ cfg.DB_PASSWORD = fileCfg.DB_PASSWORD || DEFAULTS.DB_PASSWORD;
425
+ cfg.DB_NAME = fileCfg.DB_NAME || dbDef.DB_NAME || DEFAULTS.DB_NAME;
426
+ }
427
+
428
+ return cfg;
429
+ }
430
+
431
+ /**
432
+ * Tampilkan ringkasan config yang sudah ada (hasil `resolveSourceConfig`),
433
+ * dalam bentuk cfg yang sudah di-default-kan (`defaultCfgFromFile`).
434
+ */
435
+ function printExistingConfigSummary({ project, schemaFlag, configName, cfg, overwrite }) {
436
+ console.log('');
437
+ console.log(rule('='));
438
+ console.log(' RESTForge Fast-Track — Existing Configuration');
439
+ console.log(rule('='));
440
+ console.log('');
441
+ console.log(` Project : ${project}`);
442
+ console.log(` Schema : ${schemaFlag}`);
443
+ console.log(` Config : ${configName}`);
444
+ console.log(` License : ${maskLicense(cfg.LICENSE)}`);
445
+ console.log(` REST API : ${cfg.SERVER_ADDRESS}:${cfg.SERVER_PORT}`);
446
+ console.log(` Web server : ${cfg.SERVER_ADDRESS}:${cfg.WEB_SERVER_PORT}`);
447
+ console.log(` Mode : ${overwrite ? 'overwrite' : 'sync'}`);
448
+ console.log('');
449
+ console.log(' Database Configuration');
450
+ if (cfg.DB_TYPE === 'sqlite') {
451
+ console.log(` Type : ${cfg.DB_TYPE}`);
452
+ console.log(` File : ${cfg.DB_FILE}`);
453
+ } else {
454
+ console.log(` Type : ${cfg.DB_TYPE}`);
455
+ console.log(` Host : ${cfg.DB_HOST}`);
456
+ console.log(` Port : ${cfg.DB_PORT}`);
457
+ console.log(` User : ${cfg.DB_USER}`);
458
+ console.log(` Password : ${cfg.DB_PASSWORD}`);
459
+ console.log(` DB Name : ${cfg.DB_NAME}`);
460
+ }
461
+ }
462
+
463
+ // Menu pemilihan aksi setelah existing-config summary ditampilkan. Default
464
+ // 'continue' (Enter langsung lanjut tanpa edit), selaras dengan pola
465
+ // SCOPE_MENU/DEFAULT_SCOPE di bawah.
466
+ const CONFIG_ACTION_MENU = [
467
+ { key: '1', text: 'Edit Configuration' },
468
+ { key: '2', text: 'Continue' }
469
+ ];
470
+ const DEFAULT_CONFIG_ACTION = '2';
471
+
472
+ /**
473
+ * Tanya user mau "Edit Configuration" atau "Continue" terhadap existing
474
+ * config yang baru ditampilkan. Mengikuti pola selektor `selectScope`
475
+ * (arrow-key untuk TTY, numbered fallback untuk non-TTY/piped input).
476
+ *
477
+ * @returns {Promise<'edit'|'continue'>}
478
+ */
479
+ async function selectConfigAction(prompter) {
480
+ const ask = prompter.ask;
481
+
482
+ if (!process.stdin.isTTY) {
483
+ console.log('');
484
+ for (const m of CONFIG_ACTION_MENU) console.log(` ${m.key}. ${m.text}`);
485
+ console.log('');
486
+
487
+ let choice = null;
488
+ while (!choice) {
489
+ const input = (await ask(` Choice (1-2) [${DEFAULT_CONFIG_ACTION}]: `)).trim() || DEFAULT_CONFIG_ACTION;
490
+ choice = CONFIG_ACTION_MENU.find((m) => m.key === input);
491
+ if (!choice) console.log(' Invalid choice. Enter 1 or 2.');
492
+ }
493
+ return choice.key === '1' ? 'edit' : 'continue';
494
+ }
495
+
496
+ const initialIndex = CONFIG_ACTION_MENU.findIndex((m) => m.key === DEFAULT_CONFIG_ACTION);
497
+ const chosen = await arrowSelect({
498
+ title: '',
499
+ items: CONFIG_ACTION_MENU.map((m) => m.text),
500
+ initialIndex: initialIndex >= 0 ? initialIndex : 0,
501
+ prompter
502
+ });
503
+ return CONFIG_ACTION_MENU[chosen].key === '1' ? 'edit' : 'continue';
504
+ }
505
+
401
506
  // ---------------------------------------------------------------------------
402
507
  // Fase pemilihan scope generate (menu)
403
508
  // ---------------------------------------------------------------------------
@@ -991,6 +1096,35 @@ async function waitForHealth(url, { timeoutMs = 30000, intervalMs = 600 } = {})
991
1096
  return { ok: false, elapsedMs: Date.now() - start };
992
1097
  }
993
1098
 
1099
+ /**
1100
+ * Satu kali GET <url>; resolve true bila ADA respons HTTP apa pun (status
1101
+ * berapa saja). Beda dari `pingOnce` yang strict butuh 200 - dipakai untuk
1102
+ * static file server (`npx serve`) yang sering me-redirect (301) request
1103
+ * eksplisit ke "/index.html" menuju "/". 301 tetap berarti server SUDAH
1104
+ * hidup; menunggu 200 di path tersebut bisa tidak pernah tercapai dan
1105
+ * menghabiskan timeout penuh secara percuma.
1106
+ */
1107
+ function pingAnyResponse(url, perReqTimeoutMs) {
1108
+ return new Promise((resolve) => {
1109
+ const req = http.get(url, (res) => {
1110
+ res.resume(); // drain body, status/isi tidak relevan di sini
1111
+ resolve(true);
1112
+ });
1113
+ req.setTimeout(perReqTimeoutMs, () => { req.destroy(); resolve(false); });
1114
+ req.on('error', () => resolve(false));
1115
+ });
1116
+ }
1117
+
1118
+ /** Poll <url> sampai ADA respons (lihat `pingAnyResponse`) atau timeout. */
1119
+ async function waitForHttpUp(url, { timeoutMs = 10000, intervalMs = 300 } = {}) {
1120
+ const start = Date.now();
1121
+ while (Date.now() - start < timeoutMs) {
1122
+ if (await pingAnyResponse(url, 2000)) return { ok: true, elapsedMs: Date.now() - start };
1123
+ await sleep(intervalMs);
1124
+ }
1125
+ return { ok: false, elapsedMs: Date.now() - start };
1126
+ }
1127
+
994
1128
  /** Host untuk health URL: 0.0.0.0/kosong -> localhost (mirror banner runtime). */
995
1129
  function healthHost(serverAddress) {
996
1130
  return (!serverAddress || serverAddress === '0.0.0.0') ? 'localhost' : serverAddress;
@@ -1157,18 +1291,17 @@ function printFinalSummary(ctx) {
1157
1291
  console.log(rule('='));
1158
1292
  }
1159
1293
 
1160
- /** Konfirmasi lalu jalankan runtime server di window CMD baru. */
1161
- async function maybeRunServer(ctx, ask) {
1294
+ /**
1295
+ * Jalankan runtime server di window CMD baru + tunggu health check.
1296
+ * Eksekusi murni (tanpa prompt) - dipakai baik oleh `maybeRunServer` (scope
1297
+ * REST API Only) maupun `maybeRunServerAndFrontend` (scope REST API +
1298
+ * Frontend, server harus start lebih dulu sebelum frontend).
1299
+ */
1300
+ async function startServerNow(ctx) {
1162
1301
  // Samakan dengan pola server-start.bat: serve + --watch (auto-restart pada
1163
1302
  // perubahan src/). Format log rapi (pino-pretty) berasal dari NODE_ENV
1164
1303
  // development yang di-set saat spawn di bawah.
1165
1304
  const serveCmd = `npx restforge serve --project=${ctx.project} --config=${ctx.configFlag} --watch`;
1166
- console.log('');
1167
- const answer = (await ask(' Run Runtime Server now in a new window? (Y/n): ')).trim().toLowerCase();
1168
- if (answer === 'n' || answer === 'no') {
1169
- console.log(` Skipped. Start later: ${serveCmd}`);
1170
- return;
1171
- }
1172
1305
  freePort(ctx.cfg.SERVER_PORT);
1173
1306
  const title = `RESTForge Server - ${ctx.project}`;
1174
1307
  console.log(`\n Opening new window: "${title}"`);
@@ -1182,7 +1315,7 @@ async function maybeRunServer(ctx, ask) {
1182
1315
  const r = spawnSync('cmd', ['/C', 'start', title, 'cmd', '/k', serveCmd], { cwd: ctx.cwd, stdio: 'inherit', env: serveEnv });
1183
1316
  if (r.error) {
1184
1317
  console.log(` Failed to open server window: ${r.error.message}`);
1185
- return;
1318
+ return false;
1186
1319
  }
1187
1320
  console.log(' ✓ Server window opened. Keep it open. Stop with Ctrl+C.');
1188
1321
 
@@ -1198,26 +1331,37 @@ async function maybeRunServer(ctx, ask) {
1198
1331
  console.log(` ⚠ Health check timed out after ${(h.elapsedMs / 1000).toFixed(0)}s.`);
1199
1332
  console.log(' Server may still be starting; check the server window for errors.');
1200
1333
  }
1334
+ return true;
1335
+ }
1336
+
1337
+ /** Konfirmasi lalu jalankan runtime server di window CMD baru. Dipakai scope REST API Only. */
1338
+ async function maybeRunServer(ctx, ask) {
1339
+ const serveCmd = `npx restforge serve --project=${ctx.project} --config=${ctx.configFlag} --watch`;
1340
+ console.log('');
1341
+ const answer = (await ask(' Run Runtime Server now in a new window? (Y/n): ')).trim().toLowerCase();
1342
+ if (answer === 'n' || answer === 'no') {
1343
+ console.log(` Skipped. Start later: ${serveCmd}`);
1344
+ return;
1345
+ }
1346
+ await startServerNow(ctx);
1201
1347
  }
1202
1348
 
1203
- /** Konfirmasi lalu jalankan aplikasi frontend (app-start.bat) di window CMD baru. */
1204
- async function maybeRunFrontend(ctx, ask) {
1349
+ /**
1350
+ * Jalankan aplikasi frontend di window CMD baru, tunggu static server siap,
1351
+ * lalu otomatis buka browser default ke index.html agar user tidak perlu
1352
+ * membukanya manual. Eksekusi murni (tanpa prompt).
1353
+ */
1354
+ async function startFrontendNow(ctx) {
1205
1355
  const appDir = path.join(ctx.cwd, 'frontend', 'apps', ctx.project);
1206
1356
  const webPort = ctx.cfg.WEB_SERVER_PORT;
1207
1357
  // Jalankan langsung `npx serve . -l <port>` (identik untuk Windows & Linux),
1208
1358
  // tidak bergantung pada app-start.bat/.sh. File launcher itu urusan generator.
1209
1359
  const serveCmd = `npx serve . -l ${webPort}`;
1210
- console.log('');
1211
- const answer = (await ask(' Run Frontend Application now in a new window? (Y/n): ')).trim().toLowerCase();
1212
- if (answer === 'n' || answer === 'no') {
1213
- console.log(` Skipped. Start later (in ${appDir}): ${serveCmd}`);
1214
- return;
1215
- }
1216
1360
  const indexHtml = path.join(appDir, 'index.html');
1217
1361
  if (!fs.existsSync(indexHtml)) {
1218
1362
  console.log(` Frontend app not found: ${indexHtml}`);
1219
1363
  console.log(' Frontend generation may have failed; cannot launch.');
1220
- return;
1364
+ return false;
1221
1365
  }
1222
1366
  freePort(webPort);
1223
1367
  const title = `RESTForge Frontend - ${ctx.project}`;
@@ -1226,10 +1370,49 @@ async function maybeRunFrontend(ctx, ask) {
1226
1370
  const r = spawnSync('cmd', ['/C', 'start', title, 'cmd', '/k', serveCmd], { cwd: appDir, stdio: 'inherit' });
1227
1371
  if (r.error) {
1228
1372
  console.log(` Failed to open frontend window: ${r.error.message}`);
1229
- } else {
1230
- console.log(` ✓ Frontend window opened (WEB_SERVER_PORT ${webPort}).`);
1231
- console.log(` Open: http://localhost:${webPort}/index.html`);
1373
+ return false;
1374
+ }
1375
+ console.log(` Frontend window opened (WEB_SERVER_PORT ${webPort}).`);
1376
+
1377
+ const url = `http://localhost:${webPort}/index.html`;
1378
+ // Tunggu static server (`npx serve`) benar-benar siap sebelum buka browser,
1379
+ // supaya tidak membuka tab dengan error "connection refused" (npx serve
1380
+ // bisa butuh beberapa saat pada first-run, mis. resolve package). Pakai
1381
+ // waitForHttpUp (bukan waitForHealth/200-strict) karena `serve` me-redirect
1382
+ // "/index.html" -> "/" dengan 301 - menunggu 200 di path ini bisa tidak
1383
+ // pernah tercapai dan menghabiskan timeout penuh secara percuma walau
1384
+ // server sebenarnya sudah hidup sejak request pertama.
1385
+ const ready = await waitForHttpUp(url, { timeoutMs: 10000, intervalMs: 300 });
1386
+ console.log(` Open: ${url}`);
1387
+ if (!ready.ok) {
1388
+ console.log(' ⚠ Frontend belum merespons - buka URL di atas manual bila browser tidak otomatis terbuka.');
1389
+ }
1390
+ const openResult = spawnSync('cmd', ['/C', 'start', '""', url], { stdio: 'ignore' });
1391
+ if (openResult.error) {
1392
+ console.log(` (Could not open browser automatically: ${openResult.error.message})`);
1393
+ }
1394
+ return true;
1395
+ }
1396
+
1397
+ /**
1398
+ * Dialog gabungan untuk scope REST API + Frontend: SATU konfirmasi saja
1399
+ * ("Run Runtime Server and frontend application now in a new window?"),
1400
+ * tapi urutan eksekusi tetap wajib runtime server lebih dulu (frontend
1401
+ * butuh API sudah hidup), baru lanjut frontend setelah server window
1402
+ * terbuka + health check selesai.
1403
+ */
1404
+ async function maybeRunServerAndFrontend(ctx, ask) {
1405
+ const serveCmd = `npx restforge serve --project=${ctx.project} --config=${ctx.configFlag} --watch`;
1406
+ const frontendCmd = `npx serve . -l ${ctx.cfg.WEB_SERVER_PORT}`;
1407
+ console.log('');
1408
+ const answer = (await ask(' Run Runtime Server and frontend application now in a new window? (Y/n): ')).trim().toLowerCase();
1409
+ if (answer === 'n' || answer === 'no') {
1410
+ console.log(` Skipped. Start later: ${serveCmd}`);
1411
+ console.log(` Skipped. Start later (in frontend/apps/${ctx.project}): ${frontendCmd}`);
1412
+ return;
1232
1413
  }
1414
+ await startServerNow(ctx);
1415
+ await startFrontendNow(ctx);
1233
1416
  }
1234
1417
 
1235
1418
  // ---------------------------------------------------------------------------
@@ -1287,8 +1470,26 @@ module.exports = {
1287
1470
  const { configName, fileCfg } = resolveSourceConfig(cwd, args.config);
1288
1471
 
1289
1472
  // 1) Input konfigurasi (LICENSE + database), gaya fast-track.mjs.
1290
- // Default tiap field mengikuti fileCfg bila tersedia.
1291
- const cfg = await collectConfig(args, prompter.ask, fileCfg);
1473
+ // Bila config existing terdeteksi (fileCfg punya isi), tampilkan
1474
+ // ringkasannya dulu dan tawarkan "Continue" (skip prompt sama
1475
+ // sekali) atau "Edit Configuration" (alur prompt lama). Bila
1476
+ // tidak ada config existing (0 file), langsung ke prompt seperti
1477
+ // sebelumnya.
1478
+ let cfg;
1479
+ if (Object.keys(fileCfg).length > 0) {
1480
+ const previewCfg = defaultCfgFromFile(args, fileCfg);
1481
+ printExistingConfigSummary({
1482
+ project: args.project,
1483
+ schemaFlag: args['schema-path'],
1484
+ configName,
1485
+ cfg: previewCfg,
1486
+ overwrite: args.overwrite
1487
+ });
1488
+ const action = await selectConfigAction(prompter);
1489
+ cfg = action === 'continue' ? previewCfg : await collectConfig(args, prompter.ask, fileCfg);
1490
+ } else {
1491
+ cfg = await collectConfig(args, prompter.ask, fileCfg);
1492
+ }
1292
1493
 
1293
1494
  // 2) Menu pemilihan scope generate (REST API / frontend / all).
1294
1495
  const scope = await selectScope(prompter);
@@ -1355,14 +1556,16 @@ module.exports = {
1355
1556
 
1356
1557
  printFinalSummary(ctx);
1357
1558
 
1358
- // 7) Tawarkan menjalankan service: runtime server (backend) lalu
1359
- // aplikasi frontend, masing-masing di window CMD baru.
1360
- if (ctx.scope.backend) {
1559
+ // 7) Tawarkan menjalankan service. Scope REST API + Frontend -> SATU
1560
+ // dialog konfirmasi gabungan, tapi eksekusi tetap wajib runtime
1561
+ // server lebih dulu baru frontend (frontend butuh API hidup).
1562
+ // Scope REST API Only -> dialog server saja (tidak ada frontend
1563
+ // yang di-generate, SCOPES tidak punya opsi frontend-only).
1564
+ if (ctx.scope.backend && ctx.scope.frontend) {
1565
+ await maybeRunServerAndFrontend(ctx, prompter.ask);
1566
+ } else if (ctx.scope.backend) {
1361
1567
  await maybeRunServer(ctx, prompter.ask);
1362
1568
  }
1363
- if (ctx.scope.frontend) {
1364
- await maybeRunFrontend(ctx, prompter.ask);
1365
- }
1366
1569
  } finally {
1367
1570
  prompter.close();
1368
1571
  }
@@ -1376,6 +1579,8 @@ if (process.env.FASTTRACK_TEST === '1') {
1376
1579
  module.exports.__test = {
1377
1580
  loadModels, buildTableEntries, fkColumnsForEntry, tableToKebab,
1378
1581
  parseDesignerPlugins, pluginHasAuth, injectDesignerAuth,
1379
- DESIGNER_DEFAULT_PLUGIN, DESIGNER_AUTH_DEFAULTS
1582
+ DESIGNER_DEFAULT_PLUGIN, DESIGNER_AUTH_DEFAULTS,
1583
+ waitForHealth, waitForHttpUp,
1584
+ defaultCfgFromFile
1380
1585
  };
1381
1586
  }
@@ -21,8 +21,14 @@ const path = require('path');
21
21
  const { spawnSync } = require('child_process');
22
22
 
23
23
  function resolveBinaryPath() {
24
- if (os.platform() !== 'win32') return null;
25
- return path.resolve(__dirname, '..', '..', '..', 'bin', 'sdf-tools.exe');
24
+ const platform = os.platform();
25
+ if (platform === 'win32') {
26
+ return path.resolve(__dirname, '..', '..', '..', 'bin', 'sdf-tools.exe');
27
+ }
28
+ if (platform === 'linux') {
29
+ return path.resolve(__dirname, '..', '..', '..', 'bin', 'sdf-tools-linux');
30
+ }
31
+ return null;
26
32
  }
27
33
 
28
34
  module.exports = {
@@ -51,7 +57,7 @@ module.exports = {
51
57
  const binaryPath = resolveBinaryPath();
52
58
  if (!binaryPath) {
53
59
  const err = new Error(
54
- `schema init hanya tersedia di Windows (sdf-tools.exe). Platform saat ini: ${os.platform()}`
60
+ `schema init tidak didukung di platform ini (sdf-tools). Platform saat ini: ${os.platform()}`
55
61
  );
56
62
  err.exitCode = 3;
57
63
  throw err;
@@ -59,13 +65,21 @@ module.exports = {
59
65
 
60
66
  if (!fs.existsSync(binaryPath)) {
61
67
  const err = new Error(
62
- `sdf-tools.exe tidak ditemukan di ${binaryPath}. ` +
68
+ `Binary sdf-tools tidak ditemukan di ${binaryPath}. ` +
63
69
  'Pastikan binary sudah di-build dan tersedia di folder bin/ package.'
64
70
  );
65
71
  err.exitCode = 3;
66
72
  throw err;
67
73
  }
68
74
 
75
+ if (os.platform() !== 'win32') {
76
+ try {
77
+ fs.chmodSync(binaryPath, 0o755);
78
+ } catch {
79
+ // FS tak dukung chmod (mis. read-only mount) — lanjut, biarkan spawnSync gagal kalau memang tidak executable.
80
+ }
81
+ }
82
+
69
83
  const binaryArgs = [
70
84
  '--table=dummy',
71
85
  '--generate',
@@ -79,14 +93,14 @@ module.exports = {
79
93
  });
80
94
 
81
95
  if (result.error) {
82
- const err = new Error(`Gagal menjalankan sdf-tools.exe: ${result.error.message}`);
96
+ const err = new Error(`Gagal menjalankan sdf-tools: ${result.error.message}`);
83
97
  err.exitCode = 1;
84
98
  throw err;
85
99
  }
86
100
 
87
101
  const status = typeof result.status === 'number' ? result.status : 1;
88
102
  if (status !== 0) {
89
- const err = new Error(`sdf-tools.exe exit code ${status}`);
103
+ const err = new Error(`sdf-tools exit code ${status}`);
90
104
  err.exitCode = status;
91
105
  err.silent = true;
92
106
  throw err;
@@ -9,11 +9,12 @@
9
9
  * bin/. Semua filter dan display flag diteruskan ke binary; help text di sisi
10
10
  * Node mempertahankan kontrak CLI (contract validator + help generator).
11
11
  *
12
- * Binary lookup: <package-root>/bin/sdf-tools.exe relatif terhadap file ini
12
+ * Binary lookup: <package-root>/bin/sdf-tools.exe (Windows) atau
13
+ * <package-root>/bin/sdf-tools-linux (Linux), relatif terhadap file ini
13
14
  * (resolusi sama untuk source workspace dan installation di node_modules).
14
15
  *
15
- * Platform: saat ini hanya Windows (sdf-tools.exe). Pemanggilan di non-Windows
16
- * akan return error eksplisit, bukan crash diam-diam.
16
+ * Platform: Windows dan Linux x86_64. Platform lain (mis. macOS) akan
17
+ * return error eksplisit, bukan crash diam-diam.
17
18
  */
18
19
 
19
20
  const fs = require('fs');
@@ -36,8 +37,14 @@ const BOOLEAN_FLAGS = [
36
37
  ];
37
38
 
38
39
  function resolveBinaryPath() {
39
- if (os.platform() !== 'win32') return null;
40
- return path.resolve(__dirname, '..', '..', '..', 'bin', 'sdf-tools.exe');
40
+ const platform = os.platform();
41
+ if (platform === 'win32') {
42
+ return path.resolve(__dirname, '..', '..', '..', 'bin', 'sdf-tools.exe');
43
+ }
44
+ if (platform === 'linux') {
45
+ return path.resolve(__dirname, '..', '..', '..', 'bin', 'sdf-tools-linux');
46
+ }
47
+ return null;
41
48
  }
42
49
 
43
50
  function buildBinaryArgs(args) {
@@ -185,7 +192,7 @@ module.exports = {
185
192
  const binaryPath = resolveBinaryPath();
186
193
  if (!binaryPath) {
187
194
  const err = new Error(
188
- `schema template hanya tersedia di Windows (sdf-tools.exe). Platform saat ini: ${os.platform()}`
195
+ `schema template tidak didukung di platform ini (sdf-tools). Platform saat ini: ${os.platform()}`
189
196
  );
190
197
  err.exitCode = 3;
191
198
  throw err;
@@ -193,13 +200,21 @@ module.exports = {
193
200
 
194
201
  if (!fs.existsSync(binaryPath)) {
195
202
  const err = new Error(
196
- `sdf-tools.exe tidak ditemukan di ${binaryPath}. ` +
203
+ `Binary sdf-tools tidak ditemukan di ${binaryPath}. ` +
197
204
  'Pastikan binary sudah di-build dan tersedia di folder bin/ package.'
198
205
  );
199
206
  err.exitCode = 3;
200
207
  throw err;
201
208
  }
202
209
 
210
+ if (os.platform() !== 'win32') {
211
+ try {
212
+ fs.chmodSync(binaryPath, 0o755);
213
+ } catch {
214
+ // FS tak dukung chmod (mis. read-only mount) — lanjut, biarkan spawnSync gagal kalau memang tidak executable.
215
+ }
216
+ }
217
+
203
218
  const binaryArgs = buildBinaryArgs(args);
204
219
  const result = spawnSync(binaryPath, binaryArgs, {
205
220
  stdio: 'inherit',
@@ -207,14 +222,14 @@ module.exports = {
207
222
  });
208
223
 
209
224
  if (result.error) {
210
- const err = new Error(`Gagal menjalankan sdf-tools.exe: ${result.error.message}`);
225
+ const err = new Error(`Gagal menjalankan sdf-tools: ${result.error.message}`);
211
226
  err.exitCode = 1;
212
227
  throw err;
213
228
  }
214
229
 
215
230
  const status = typeof result.status === 'number' ? result.status : 1;
216
231
  if (status !== 0) {
217
- const err = new Error(`sdf-tools.exe exit code ${status}`);
232
+ const err = new Error(`sdf-tools exit code ${status}`);
218
233
  err.exitCode = status;
219
234
  err.silent = true;
220
235
  throw err;
@@ -79,33 +79,55 @@ function convertSinglePage(backend) {
79
79
 
80
80
  const hasSearch = datatablesWhere.length > 0
81
81
  && datatablesWhere.some(w => typeof w === 'string' && w !== 'all');
82
- const hasStatusFilter = resolvedFields.some(rf => rf.name === 'is_active' || rf.name === 'status');
83
82
 
84
83
  const features = {
85
84
  enableSearch: hasSearch,
86
85
  fieldLayout: 'vertical'
87
86
  };
88
87
 
89
- if (hasStatusFilter) {
88
+ const primaryStatusField = resolvedFields.find(rf => {
89
+ if (rf.name !== 'is_active' && rf.name !== 'status') return false;
90
+ if (rf.fieldType === 'checkbox') return true;
91
+ return rf.fieldType === 'select' && rf.extra && rf.extra.dataSource && rf.extra.dataSource.type === 'static';
92
+ });
93
+
94
+ if (primaryStatusField) {
90
95
  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
- }
96
+ if (primaryStatusField.fieldType === 'checkbox') {
97
+ const cbt = (primaryStatusField.extra && primaryStatusField.extra.checkboxText) || {};
98
+ const checked = typeof cbt.checked === 'string' ? cbt.checked : 'Active';
99
+ const unchecked = typeof cbt.unchecked === 'string' ? cbt.unchecked : 'Inactive';
100
+ features.statusFilter = {
101
+ field: primaryStatusField.name,
102
+ label: primaryStatusField.label,
103
+ options: [
104
+ { value: 'true', text: checked },
105
+ { value: 'false', text: unchecked }
106
+ ]
107
+ };
108
+ } else {
109
+ features.statusFilter = {
110
+ field: primaryStatusField.name,
111
+ label: primaryStatusField.label,
112
+ options: primaryStatusField.extra.dataSource.options
113
+ };
106
114
  }
107
115
  }
108
116
 
117
+ const dataFilters = resolvedFields
118
+ .filter(rf => {
119
+ if (rf.fieldType !== 'select' || !rf.extra || !rf.extra.dataSource) return false;
120
+ if (rf.extra.dataSource.type === 'api') return true;
121
+ if (rf.extra.dataSource.type === 'static') return rf !== primaryStatusField;
122
+ return false;
123
+ })
124
+ .map(rf => ({ name: rf.name, field: rf.name, label: rf.label, dataSource: rf.extra.dataSource }));
125
+
126
+ if (dataFilters.length > 0) {
127
+ features.enableDataFilter = true;
128
+ features.dataFilters = dataFilters;
129
+ }
130
+
109
131
  const fieldsArray = [];
110
132
  for (const rf of resolvedFields) {
111
133
  const fieldObj = { name: rf.name, label: rf.label, type: rf.fieldType };
@@ -251,15 +251,39 @@ class FieldTypeResolver {
251
251
  inTable,
252
252
  tableOrder,
253
253
  tableField: null,
254
- defaultValue: undefined,
254
+ defaultValue: extractDefault(constraints),
255
255
  extra: {
256
256
  dataSource: { type: 'static', options }
257
257
  }
258
258
  };
259
259
  }
260
260
 
261
- // Rule 6: Date
262
- if (fieldName.endsWith('_date') || fieldName === 'date') {
261
+ // Rule 6: Date/datetime/timestamp — valType adalah sumber kebenaran utama
262
+ // (REGARDLESS nama field). Heuristik nama hanya fallback bila field tidak
263
+ // punya entry fieldValidation (valType kosong).
264
+ //
265
+ // defaultValue: constraints.autoGenerate=true (representasi default:now()/
266
+ // CURRENT_TIMESTAMP, lihat payload-runner.js generateFieldValidation) di-
267
+ // terjemahkan ke keyword dinamis 'now'/'today' yang dikenali frontend
268
+ // (field_js_generator.rs dynamic_default_js: "today"/"now" -> JS expression
269
+ // tanggal/jam saat ini). Tanpa autoGenerate, fallback ke literal
270
+ // constraints.default biasa (handbook: default berlaku universal semua tipe).
271
+ if (valType === 'datetime' || valType === 'timestamp') {
272
+ return {
273
+ name: fieldName,
274
+ label,
275
+ fieldType: 'timestamp',
276
+ skip: false,
277
+ required,
278
+ inTable,
279
+ tableOrder,
280
+ tableField: null,
281
+ defaultValue: constraints.autoGenerate === true ? 'now' : extractDefault(constraints),
282
+ extra: {}
283
+ };
284
+ }
285
+
286
+ if (valType === 'date') {
263
287
  return {
264
288
  name: fieldName,
265
289
  label,
@@ -269,13 +293,14 @@ class FieldTypeResolver {
269
293
  inTable,
270
294
  tableOrder,
271
295
  tableField: null,
272
- defaultValue: undefined,
296
+ defaultValue: constraints.autoGenerate === true ? 'today' : extractDefault(constraints),
273
297
  extra: {}
274
298
  };
275
299
  }
276
300
 
277
- // Rule 7: Time
278
- if (fieldName.endsWith('_time') || fieldName === 'time') {
301
+ // Rule 7: Time (autoGenerate tidak didukung untuk time per handbook
302
+ // field-validation.md - hanya literal default yang relevan)
303
+ if (valType === 'time') {
279
304
  return {
280
305
  name: fieldName,
281
306
  label,
@@ -285,11 +310,43 @@ class FieldTypeResolver {
285
310
  inTable,
286
311
  tableOrder,
287
312
  tableField: null,
288
- defaultValue: undefined,
313
+ defaultValue: extractDefault(constraints),
289
314
  extra: {}
290
315
  };
291
316
  }
292
317
 
318
+ if (valType === '') {
319
+ if (fieldName.endsWith('_date') || fieldName === 'date') {
320
+ return {
321
+ name: fieldName,
322
+ label,
323
+ fieldType: 'date',
324
+ skip: false,
325
+ required,
326
+ inTable,
327
+ tableOrder,
328
+ tableField: null,
329
+ defaultValue: undefined,
330
+ extra: {}
331
+ };
332
+ }
333
+
334
+ if (fieldName.endsWith('_time') || fieldName === 'time') {
335
+ return {
336
+ name: fieldName,
337
+ label,
338
+ fieldType: 'time',
339
+ skip: false,
340
+ required,
341
+ inTable,
342
+ tableOrder,
343
+ tableField: null,
344
+ defaultValue: undefined,
345
+ extra: {}
346
+ };
347
+ }
348
+ }
349
+
293
350
  // Rule 8: Textarea
294
351
  const isTextarea = TEXTAREA_FIELDS.includes(fieldName)
295
352
  || TEXTAREA_PREFIXES.some(p => fieldName.startsWith(p));