@restforgejs/platform 5.2.0 → 5.2.10

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 (174) hide show
  1. package/build-info.json +2 -2
  2. package/cli/consumer-deploy.js +2 -2
  3. package/cli/consumer.js +2 -2
  4. package/generators/cli/fast-track.js +293 -35
  5. package/generators/lib/migrate/backend-payload-migrator.js +39 -17
  6. package/generators/lib/migrate/field-type-resolver.js +64 -7
  7. package/generators/lib/migrate/migrate-runner.js +12 -2
  8. package/generators/lib/migrate/sql-parser.js +5 -3
  9. package/generators/lib/payload/payload-runner.js +103 -11
  10. package/generators/lib/templates/dashboard-catalog.js +1 -1
  11. package/generators/lib/templates/db-connection-env.js +1 -1
  12. package/generators/lib/templates/dbschema-catalog.js +1 -1
  13. package/generators/lib/templates/field-validation-catalog.js +1 -1
  14. package/generators/lib/templates/mysql-template.js +1 -1
  15. package/generators/lib/templates/oracle-template.js +1 -1
  16. package/generators/lib/templates/postgres-template.js +1 -1
  17. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  18. package/generators/lib/templates/sqlite-template.js +1 -1
  19. package/integrity-manifest.json +18 -18
  20. package/package.json +1 -1
  21. package/scripts/verify-integrity.js +1 -1
  22. package/server.js +2 -2
  23. package/src/components/handlers/adjust_handler.js +1 -1
  24. package/src/components/handlers/audit_handler.js +1 -1
  25. package/src/components/handlers/delete_handler.js +1 -1
  26. package/src/components/handlers/export_handler.js +1 -1
  27. package/src/components/handlers/import_handler.js +1 -1
  28. package/src/components/handlers/insert_handler.js +1 -1
  29. package/src/components/handlers/update_handler.js +1 -1
  30. package/src/components/handlers/upload_handler.js +1 -1
  31. package/src/components/handlers/workflow_handler.js +1 -1
  32. package/src/components/integrations/webhook.js +1 -1
  33. package/src/consumers/baseConsumer.js +1 -1
  34. package/src/consumers/declarativeMapper.js +1 -1
  35. package/src/consumers/handlers/apiHandler.js +1 -1
  36. package/src/consumers/handlers/consoleHandler.js +1 -1
  37. package/src/consumers/handlers/databaseHandler.js +1 -1
  38. package/src/consumers/handlers/index.js +1 -1
  39. package/src/consumers/handlers/kafkaHandler.js +1 -1
  40. package/src/consumers/index.js +1 -1
  41. package/src/consumers/messageTransformer.js +1 -1
  42. package/src/consumers/validator.js +1 -1
  43. package/src/core/db/dialect/base-dialect.js +1 -1
  44. package/src/core/db/dialect/index.js +1 -1
  45. package/src/core/db/dialect/mysql-dialect.js +1 -1
  46. package/src/core/db/dialect/oracle-dialect.js +1 -1
  47. package/src/core/db/dialect/postgres-dialect.js +1 -1
  48. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  49. package/src/core/db/flatten-helper.js +1 -1
  50. package/src/core/db/query-builder-error.js +1 -1
  51. package/src/core/db/query-builder.js +1 -1
  52. package/src/core/db/relation-helper.js +1 -1
  53. package/src/core/handlers/delete_handler.js +1 -1
  54. package/src/core/handlers/insert_handler.js +1 -1
  55. package/src/core/handlers/update_handler.js +1 -1
  56. package/src/core/models/base-model.js +1 -1
  57. package/src/core/utils/cache-manager.js +1 -1
  58. package/src/core/utils/component-engine.js +1 -1
  59. package/src/core/utils/context-builder.js +1 -1
  60. package/src/core/utils/datetime-formatter.js +1 -1
  61. package/src/core/utils/datetime-parser.js +1 -1
  62. package/src/core/utils/db.js +1 -1
  63. package/src/core/utils/logger.js +1 -1
  64. package/src/core/utils/payload-loader.js +1 -1
  65. package/src/core/utils/security-checks.js +1 -1
  66. package/src/middleware/body-options.js +1 -1
  67. package/src/middleware/cors.js +1 -1
  68. package/src/middleware/idempotency.js +1 -1
  69. package/src/middleware/rate-limiter.js +1 -1
  70. package/src/middleware/request-logger.js +1 -1
  71. package/src/middleware/security-headers.js +1 -1
  72. package/src/models/base-model-mysql.js +1 -1
  73. package/src/models/base-model-oracle.js +1 -1
  74. package/src/models/base-model-sqlite.js +1 -1
  75. package/src/models/base-model.js +1 -1
  76. package/src/pro/caching/redis-client.js +1 -1
  77. package/src/pro/caching/redis-helper.js +1 -1
  78. package/src/pro/consumers/baseConsumer.js +1 -1
  79. package/src/pro/consumers/declarativeMapper.js +1 -1
  80. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  81. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  82. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  83. package/src/pro/consumers/handlers/index.js +1 -1
  84. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  85. package/src/pro/consumers/index.js +1 -1
  86. package/src/pro/consumers/messageTransformer.js +1 -1
  87. package/src/pro/consumers/validator.js +1 -1
  88. package/src/pro/database/base-model-mysql.js +1 -1
  89. package/src/pro/database/base-model-oracle.js +1 -1
  90. package/src/pro/database/base-model-sqlite.js +1 -1
  91. package/src/pro/database/db-mysql.js +1 -1
  92. package/src/pro/database/db-oracle.js +1 -1
  93. package/src/pro/database/db-sqlite.js +1 -1
  94. package/src/pro/excel/excel-generator.js +1 -1
  95. package/src/pro/excel/excel-parser.js +1 -1
  96. package/src/pro/excel/export-service.js +1 -1
  97. package/src/pro/excel/export_handler.js +1 -1
  98. package/src/pro/excel/import-service.js +1 -1
  99. package/src/pro/excel/import-validator.js +1 -1
  100. package/src/pro/excel/import_handler.js +1 -1
  101. package/src/pro/excel/upsert-builder.js +1 -1
  102. package/src/pro/idgen/idgen-routes.js +1 -1
  103. package/src/pro/integrations/lookup-resolver.js +1 -1
  104. package/src/pro/integrations/upload-handler-v2.js +1 -1
  105. package/src/pro/integrations/upload-handler.js +1 -1
  106. package/src/pro/integrations/webhook.js +1 -1
  107. package/src/pro/locking/lock-routes.js +1 -1
  108. package/src/pro/locking/resource-lock-manager.js +1 -1
  109. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  110. package/src/pro/messaging/kafkaService.js +1 -1
  111. package/src/pro/messaging/messagehubService.js +1 -1
  112. package/src/pro/messaging/rabbitmqService.js +1 -1
  113. package/src/pro/scheduler/job-manager.js +1 -1
  114. package/src/pro/scheduler/job-routes.js +1 -1
  115. package/src/pro/scheduler/job-validator.js +1 -1
  116. package/src/pro/storage/base-storage-provider.js +1 -1
  117. package/src/pro/storage/file-metadata-helper.js +1 -1
  118. package/src/pro/storage/index.js +1 -1
  119. package/src/pro/storage/local-storage-provider.js +1 -1
  120. package/src/pro/storage/s3-storage-provider.js +1 -1
  121. package/src/pro/storage/upload-cleanup-job.js +1 -1
  122. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  123. package/src/pro/storage/upload-pending-tracker.js +1 -1
  124. package/src/pro/websocket/broadcast-helper.js +1 -1
  125. package/src/pro/websocket/index.js +1 -1
  126. package/src/pro/websocket/livesync-server.js +1 -1
  127. package/src/pro/websocket/ws-broadcaster.js +1 -1
  128. package/src/services/export-service.js +1 -1
  129. package/src/services/import-service.js +1 -1
  130. package/src/services/kafkaConsumerService.js +1 -1
  131. package/src/services/kafkaService.js +1 -1
  132. package/src/services/messagehubService.js +1 -1
  133. package/src/services/rabbitmqService.js +1 -1
  134. package/src/utils/cache-invalidation-registry.js +1 -1
  135. package/src/utils/cache-manager.js +1 -1
  136. package/src/utils/component-engine.js +1 -1
  137. package/src/utils/config-extractor.js +1 -1
  138. package/src/utils/consumerLogger.js +1 -1
  139. package/src/utils/context-builder.js +1 -1
  140. package/src/utils/dashboard-helpers.js +1 -1
  141. package/src/utils/dateHelper.js +1 -1
  142. package/src/utils/datetime-formatter.js +1 -1
  143. package/src/utils/datetime-parser.js +1 -1
  144. package/src/utils/db-bootstrap.js +1 -1
  145. package/src/utils/db-mysql.js +1 -1
  146. package/src/utils/db-oracle.js +1 -1
  147. package/src/utils/db-sqlite.js +1 -1
  148. package/src/utils/db.js +1 -1
  149. package/src/utils/demo-generator.js +1 -1
  150. package/src/utils/excel-generator.js +1 -1
  151. package/src/utils/excel-parser.js +1 -1
  152. package/src/utils/file-watcher.js +1 -1
  153. package/src/utils/id-generator.js +1 -1
  154. package/src/utils/idempotency-manager.js +1 -1
  155. package/src/utils/import-validator.js +1 -1
  156. package/src/utils/license-client.js +1 -1
  157. package/src/utils/lock-manager.js +1 -1
  158. package/src/utils/logger.js +1 -1
  159. package/src/utils/lookup-resolver.js +1 -1
  160. package/src/utils/payload-loader.js +1 -1
  161. package/src/utils/processor-response.js +1 -1
  162. package/src/utils/rabbitmq.js +1 -1
  163. package/src/utils/redis-client.js +1 -1
  164. package/src/utils/redis-helper.js +1 -1
  165. package/src/utils/request-scope.js +1 -1
  166. package/src/utils/security-checks.js +1 -1
  167. package/src/utils/service-resolver.js +1 -1
  168. package/src/utils/shutdown-coordinator.js +1 -1
  169. package/src/utils/soft-delete-dashboard-guard.js +1 -1
  170. package/src/utils/sql-table-extractor.js +1 -1
  171. package/src/utils/trusted-keys.js +1 -1
  172. package/src/utils/upload-handler.js +1 -1
  173. package/src/utils/upsert-builder.js +1 -1
  174. 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
  // ---------------------------------------------------------------------------
@@ -856,19 +961,49 @@ function tableToKebab(table) {
856
961
  * displayCols = kolom code/name di luar PK & kolom audit, dipakai sebagai kolom
857
962
  * display saat tabel ini menjadi parent FK (untuk `payload sync --expand-fk`).
858
963
  */
964
+ // Postgres 'public' diperlakukan sebagai default implisit (selalu di search_path
965
+ // secara konvensi), SAMA seperti pengecualian di `qualifiedRefTable()`
966
+ // (generators/lib/payload/payload-runner.js). `schema introspect` SELALU menulis
967
+ // field `schema:` eksplisit di SDF hasil introspeksi, termasuk untuk tabel yang
968
+ // sebenarnya di schema default ('public') — tanpa pengecualian ini, SETIAP SDF
969
+ // hasil introspect Postgres default-schema akan ikut di-qualify ('public.visitors'),
970
+ // menghasilkan nama file RDF berprefix 'public-' yang tidak konsisten dengan JOIN
971
+ // SQL (qualifiedRefTable() di payload-runner.js TIDAK menulis prefix 'public.' pada
972
+ // klausa JOIN) — JOIN auto-discovery di `payload migrate` (migrate-runner.js) jadi
973
+ // gagal menemukan file RDF parent, dan page UDF parent tidak pernah dibuat.
974
+ const DEFAULT_SCHEMA_SENTINEL = 'public';
975
+ function isQualifyingSchema(schemaName) {
976
+ return !!schemaName && schemaName !== DEFAULT_SCHEMA_SENTINEL;
977
+ }
978
+
859
979
  function loadModels(schemaDir) {
860
980
  const models = loadSchemaPath(schemaDir);
861
981
  const entries = [];
862
982
  for (const model of models.values()) {
863
- const table = model.tableName;
983
+ // qualifiedName (mis. 'myschema.guest_book') dipakai, bukan tableName
984
+ // bare, agar `--table=` yang dikirim ke `payload generate`/`payload sync`
985
+ // tetap menemukan tabel yang ada di custom schema (tanpa prefix, resolver
986
+ // koneksi default ke schema 'public'/setara dan tabel tidak ditemukan).
987
+ // Schema 'public' dikecualikan (lihat isQualifyingSchema) karena memang
988
+ // sudah default tanpa perlu di-qualify.
989
+ const schemaName = model.schemaName || null;
990
+ const table = isQualifyingSchema(schemaName) ? (model.qualifiedName || model.tableName) : model.tableName;
864
991
  const primaryKey = Array.isArray(model.primaryKey) ? model.primaryKey : [];
865
992
 
866
993
  const fks = [];
867
994
  for (const rel of Object.values(model.relations || {})) {
868
995
  if (rel.type !== 'belongsTo' || !rel.localKey || !rel.target) continue;
996
+ // Konvensi SDF (restforge-handbook/catalogs/sdf/multi-schema.md): target
997
+ // relasi ditulis bare bila parent di schema YANG SAMA dengan child, dan
998
+ // fully-qualified hanya bila cross-schema. Qualify bare target dengan
999
+ // schema child di sini supaya konsisten dengan `table` qualified di atas
1000
+ // (byTable/parentTables dibangun dari nilai ini, lihat ctx.byTable).
1001
+ const parentTable = rel.target.includes('.')
1002
+ ? rel.target
1003
+ : (isQualifyingSchema(schemaName) ? `${schemaName}.${rel.target}` : rel.target);
869
1004
  fks.push({
870
1005
  childCol: rel.localKey,
871
- parentTable: rel.target,
1006
+ parentTable,
872
1007
  parentCol: rel.references || null
873
1008
  });
874
1009
  }
@@ -879,7 +1014,17 @@ function loadModels(schemaDir) {
879
1014
  return !isPk && !AUDIT_COLS.has(name) && /(code|name)/i.test(name);
880
1015
  });
881
1016
 
882
- entries.push({ kebab: tableToKebab(table), table, fks, displayCols });
1017
+ // `kebab` (qualified, mis. 'myschema-guest-book') HARUS dipakai untuk
1018
+ // mencocokkan nama file RDF/UDF di disk (`payload generate` menulis
1019
+ // baseFilename dari `--table=` literal, jadi ikut prefix schema).
1020
+ // `resourceName` (bare, mis. 'guest-book') dipakai khusus untuk identitas
1021
+ // REST endpoint (`endpoint create --name=`): endpoint TIDAK mengenal
1022
+ // schema (itu murni konsep koneksi database), dan `payload migrate`
1023
+ // (RDF -> UDF) sendiri sudah men-strip schema saat menurunkan apiPath/
1024
+ // pageId (lihat backend-payload-migrator.js: cleanTable = tableName.split('.').pop()).
1025
+ // Memakai `kebab` qualified untuk nama endpoint akan membuat route REST
1026
+ // tidak sinkron dengan apiPath yang dipanggil frontend.
1027
+ entries.push({ kebab: tableToKebab(table), resourceName: tableToKebab(model.tableName), table, fks, displayCols });
883
1028
  }
884
1029
  entries.sort((a, b) => a.table.localeCompare(b.table));
885
1030
  return entries;
@@ -894,13 +1039,24 @@ function buildTableEntries(schemaDir) {
894
1039
  * Turunkan daftar `--fk-columns` untuk `payload sync --expand-fk` dari SDF:
895
1040
  * `<parentTable>.<displayCol>` untuk tiap kolom display (code/name) tiap parent.
896
1041
  * Kosong bila tidak ada kolom display terdeteksi (jatuh ke mode AUTO command).
1042
+ *
1043
+ * `parentTable` di sini WAJIB bare (tanpa prefix schema), berbeda dari
1044
+ * `fk.parentTable` yang dipakai untuk lookup `byTable` (qualified). Alasannya:
1045
+ * `payload-runner.js parseFkColumns()` mem-validasi setiap entri persis 2 segmen
1046
+ * (`table.column`), dan nilainya dicocokkan terhadap `getForeignKeys().references.table`
1047
+ * hasil introspeksi DB — yang SELALU bare (`pg_class.relname`, schema ada di field
1048
+ * terpisah `references.schema`). Mengirim `sch02.company.company_name` (3 segmen)
1049
+ * ditolak validator; mengirim bare `company.company_name` cocok dengan hasil introspeksi.
897
1050
  */
898
1051
  function fkColumnsForEntry(entry, byTable) {
899
1052
  const cols = [];
900
1053
  for (const fk of entry.fks) {
901
1054
  const parent = byTable.get(fk.parentTable);
902
1055
  const disp = parent ? parent.displayCols : [];
903
- for (const dc of disp) cols.push(`${fk.parentTable}.${dc}`);
1056
+ const bareParentTable = fk.parentTable.includes('.')
1057
+ ? fk.parentTable.split('.').pop()
1058
+ : fk.parentTable;
1059
+ for (const dc of disp) cols.push(`${bareParentTable}.${dc}`);
904
1060
  }
905
1061
  return cols;
906
1062
  }
@@ -940,6 +1096,35 @@ async function waitForHealth(url, { timeoutMs = 30000, intervalMs = 600 } = {})
940
1096
  return { ok: false, elapsedMs: Date.now() - start };
941
1097
  }
942
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
+
943
1128
  /** Host untuk health URL: 0.0.0.0/kosong -> localhost (mirror banner runtime). */
944
1129
  function healthHost(serverAddress) {
945
1130
  return (!serverAddress || serverAddress === '0.0.0.0') ? 'localhost' : serverAddress;
@@ -998,7 +1183,9 @@ function runBackendPipeline(ctx) {
998
1183
 
999
1184
  phase('[4/4] REST endpoints');
1000
1185
  for (const t of ctx.tableEntries) {
1001
- run(`npx restforge endpoint create --project=${ctx.project} --name=${t.kebab} --payload=${t.kebab}.json ${cfgArg} --database=${dbFlag} --force`, ctx.cwd);
1186
+ // --name= = identitas REST endpoint (bare, lihat resourceName di loadModels).
1187
+ // --payload= = nama file RDF di disk (qualified/kebab, ikut prefix schema).
1188
+ run(`npx restforge endpoint create --project=${ctx.project} --name=${t.resourceName} --payload=${t.kebab}.json ${cfgArg} --database=${dbFlag} --force`, ctx.cwd);
1002
1189
  }
1003
1190
  }
1004
1191
 
@@ -1104,18 +1291,17 @@ function printFinalSummary(ctx) {
1104
1291
  console.log(rule('='));
1105
1292
  }
1106
1293
 
1107
- /** Konfirmasi lalu jalankan runtime server di window CMD baru. */
1108
- 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) {
1109
1301
  // Samakan dengan pola server-start.bat: serve + --watch (auto-restart pada
1110
1302
  // perubahan src/). Format log rapi (pino-pretty) berasal dari NODE_ENV
1111
1303
  // development yang di-set saat spawn di bawah.
1112
1304
  const serveCmd = `npx restforge serve --project=${ctx.project} --config=${ctx.configFlag} --watch`;
1113
- console.log('');
1114
- const answer = (await ask(' Run Runtime Server now in a new window? (Y/n): ')).trim().toLowerCase();
1115
- if (answer === 'n' || answer === 'no') {
1116
- console.log(` Skipped. Start later: ${serveCmd}`);
1117
- return;
1118
- }
1119
1305
  freePort(ctx.cfg.SERVER_PORT);
1120
1306
  const title = `RESTForge Server - ${ctx.project}`;
1121
1307
  console.log(`\n Opening new window: "${title}"`);
@@ -1129,7 +1315,7 @@ async function maybeRunServer(ctx, ask) {
1129
1315
  const r = spawnSync('cmd', ['/C', 'start', title, 'cmd', '/k', serveCmd], { cwd: ctx.cwd, stdio: 'inherit', env: serveEnv });
1130
1316
  if (r.error) {
1131
1317
  console.log(` Failed to open server window: ${r.error.message}`);
1132
- return;
1318
+ return false;
1133
1319
  }
1134
1320
  console.log(' ✓ Server window opened. Keep it open. Stop with Ctrl+C.');
1135
1321
 
@@ -1145,26 +1331,37 @@ async function maybeRunServer(ctx, ask) {
1145
1331
  console.log(` ⚠ Health check timed out after ${(h.elapsedMs / 1000).toFixed(0)}s.`);
1146
1332
  console.log(' Server may still be starting; check the server window for errors.');
1147
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);
1148
1347
  }
1149
1348
 
1150
- /** Konfirmasi lalu jalankan aplikasi frontend (app-start.bat) di window CMD baru. */
1151
- 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) {
1152
1355
  const appDir = path.join(ctx.cwd, 'frontend', 'apps', ctx.project);
1153
1356
  const webPort = ctx.cfg.WEB_SERVER_PORT;
1154
1357
  // Jalankan langsung `npx serve . -l <port>` (identik untuk Windows & Linux),
1155
1358
  // tidak bergantung pada app-start.bat/.sh. File launcher itu urusan generator.
1156
1359
  const serveCmd = `npx serve . -l ${webPort}`;
1157
- console.log('');
1158
- const answer = (await ask(' Run Frontend Application now in a new window? (Y/n): ')).trim().toLowerCase();
1159
- if (answer === 'n' || answer === 'no') {
1160
- console.log(` Skipped. Start later (in ${appDir}): ${serveCmd}`);
1161
- return;
1162
- }
1163
1360
  const indexHtml = path.join(appDir, 'index.html');
1164
1361
  if (!fs.existsSync(indexHtml)) {
1165
1362
  console.log(` Frontend app not found: ${indexHtml}`);
1166
1363
  console.log(' Frontend generation may have failed; cannot launch.');
1167
- return;
1364
+ return false;
1168
1365
  }
1169
1366
  freePort(webPort);
1170
1367
  const title = `RESTForge Frontend - ${ctx.project}`;
@@ -1173,10 +1370,49 @@ async function maybeRunFrontend(ctx, ask) {
1173
1370
  const r = spawnSync('cmd', ['/C', 'start', title, 'cmd', '/k', serveCmd], { cwd: appDir, stdio: 'inherit' });
1174
1371
  if (r.error) {
1175
1372
  console.log(` Failed to open frontend window: ${r.error.message}`);
1176
- } else {
1177
- console.log(` ✓ Frontend window opened (WEB_SERVER_PORT ${webPort}).`);
1178
- 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;
1179
1413
  }
1414
+ await startServerNow(ctx);
1415
+ await startFrontendNow(ctx);
1180
1416
  }
1181
1417
 
1182
1418
  // ---------------------------------------------------------------------------
@@ -1234,8 +1470,26 @@ module.exports = {
1234
1470
  const { configName, fileCfg } = resolveSourceConfig(cwd, args.config);
1235
1471
 
1236
1472
  // 1) Input konfigurasi (LICENSE + database), gaya fast-track.mjs.
1237
- // Default tiap field mengikuti fileCfg bila tersedia.
1238
- 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
+ }
1239
1493
 
1240
1494
  // 2) Menu pemilihan scope generate (REST API / frontend / all).
1241
1495
  const scope = await selectScope(prompter);
@@ -1302,14 +1556,16 @@ module.exports = {
1302
1556
 
1303
1557
  printFinalSummary(ctx);
1304
1558
 
1305
- // 7) Tawarkan menjalankan service: runtime server (backend) lalu
1306
- // aplikasi frontend, masing-masing di window CMD baru.
1307
- 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) {
1308
1567
  await maybeRunServer(ctx, prompter.ask);
1309
1568
  }
1310
- if (ctx.scope.frontend) {
1311
- await maybeRunFrontend(ctx, prompter.ask);
1312
- }
1313
1569
  } finally {
1314
1570
  prompter.close();
1315
1571
  }
@@ -1323,6 +1579,8 @@ if (process.env.FASTTRACK_TEST === '1') {
1323
1579
  module.exports.__test = {
1324
1580
  loadModels, buildTableEntries, fkColumnsForEntry, tableToKebab,
1325
1581
  parseDesignerPlugins, pluginHasAuth, injectDesignerAuth,
1326
- DESIGNER_DEFAULT_PLUGIN, DESIGNER_AUTH_DEFAULTS
1582
+ DESIGNER_DEFAULT_PLUGIN, DESIGNER_AUTH_DEFAULTS,
1583
+ waitForHealth, waitForHttpUp,
1584
+ defaultCfgFromFile
1327
1585
  };
1328
1586
  }
@@ -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));
@@ -176,9 +176,19 @@ function loadRdf(filePath) {
176
176
  * Petakan nama tabel JOIN (snake_case, mis. visitor_categories) ke file RDF
177
177
  * sibling di folder payload. Coba bentuk kebab dulu (visitor-categories.json),
178
178
  * lalu bentuk snake (visitor_categories.json).
179
+ *
180
+ * `qualifiedName` (opsional) = identifier JOIN apa adanya sebelum schema
181
+ * di-strip (mis. 'sch02.company'). Tabel di custom schema ditulis `payload
182
+ * generate` dengan baseFilename = `--table=` literal (lihat payload-runner.js),
183
+ * jadi file di disk-nya `sch02-company.json`, BUKAN `company.json`. Tanpa
184
+ * kandidat ini, related table di schema custom selalu gagal ditemukan
185
+ * walau file-nya ada (lihat bug: "no RDF file found ... tried: company.json, company.json").
179
186
  */
180
- function findRelatedRdfPath(tableName, payloadDir) {
187
+ function findRelatedRdfPath(tableName, payloadDir, qualifiedName) {
181
188
  const candidates = [`${snakeToKebab(tableName)}.json`, `${tableName}.json`];
189
+ if (qualifiedName && qualifiedName !== tableName) {
190
+ candidates.push(`${qualifiedName.replace(/[._]/g, '-')}.json`);
191
+ }
182
192
  for (const c of candidates) {
183
193
  const p = path.join(payloadDir, c);
184
194
  if (fs.existsSync(p)) return { path: p, candidates };
@@ -259,7 +269,7 @@ async function run(args) {
259
269
  if (!t || seenTables.has(t)) continue;
260
270
  seenTables.add(t);
261
271
 
262
- const { path: relPath, candidates } = findRelatedRdfPath(t, payloadDir);
272
+ const { path: relPath, candidates } = findRelatedRdfPath(t, payloadDir, join.tableQualified);
263
273
  if (!relPath) {
264
274
  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
275
  continue;