@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.
- package/build-info.json +2 -2
- package/cli/consumer-deploy.js +2 -2
- package/cli/consumer.js +2 -2
- package/generators/cli/fast-track.js +293 -35
- package/generators/lib/migrate/backend-payload-migrator.js +39 -17
- package/generators/lib/migrate/field-type-resolver.js +64 -7
- package/generators/lib/migrate/migrate-runner.js +12 -2
- package/generators/lib/migrate/sql-parser.js +5 -3
- package/generators/lib/payload/payload-runner.js +103 -11
- package/generators/lib/templates/dashboard-catalog.js +1 -1
- package/generators/lib/templates/db-connection-env.js +1 -1
- package/generators/lib/templates/dbschema-catalog.js +1 -1
- package/generators/lib/templates/field-validation-catalog.js +1 -1
- package/generators/lib/templates/mysql-template.js +1 -1
- package/generators/lib/templates/oracle-template.js +1 -1
- package/generators/lib/templates/postgres-template.js +1 -1
- package/generators/lib/templates/query-declarative-catalog.js +1 -1
- package/generators/lib/templates/sqlite-template.js +1 -1
- package/integrity-manifest.json +18 -18
- package/package.json +1 -1
- package/scripts/verify-integrity.js +1 -1
- package/server.js +2 -2
- package/src/components/handlers/adjust_handler.js +1 -1
- package/src/components/handlers/audit_handler.js +1 -1
- package/src/components/handlers/delete_handler.js +1 -1
- package/src/components/handlers/export_handler.js +1 -1
- package/src/components/handlers/import_handler.js +1 -1
- package/src/components/handlers/insert_handler.js +1 -1
- package/src/components/handlers/update_handler.js +1 -1
- package/src/components/handlers/upload_handler.js +1 -1
- package/src/components/handlers/workflow_handler.js +1 -1
- package/src/components/integrations/webhook.js +1 -1
- package/src/consumers/baseConsumer.js +1 -1
- package/src/consumers/declarativeMapper.js +1 -1
- package/src/consumers/handlers/apiHandler.js +1 -1
- package/src/consumers/handlers/consoleHandler.js +1 -1
- package/src/consumers/handlers/databaseHandler.js +1 -1
- package/src/consumers/handlers/index.js +1 -1
- package/src/consumers/handlers/kafkaHandler.js +1 -1
- package/src/consumers/index.js +1 -1
- package/src/consumers/messageTransformer.js +1 -1
- package/src/consumers/validator.js +1 -1
- package/src/core/db/dialect/base-dialect.js +1 -1
- package/src/core/db/dialect/index.js +1 -1
- package/src/core/db/dialect/mysql-dialect.js +1 -1
- package/src/core/db/dialect/oracle-dialect.js +1 -1
- package/src/core/db/dialect/postgres-dialect.js +1 -1
- package/src/core/db/dialect/sqlite-dialect.js +1 -1
- package/src/core/db/flatten-helper.js +1 -1
- package/src/core/db/query-builder-error.js +1 -1
- package/src/core/db/query-builder.js +1 -1
- package/src/core/db/relation-helper.js +1 -1
- package/src/core/handlers/delete_handler.js +1 -1
- package/src/core/handlers/insert_handler.js +1 -1
- package/src/core/handlers/update_handler.js +1 -1
- package/src/core/models/base-model.js +1 -1
- package/src/core/utils/cache-manager.js +1 -1
- package/src/core/utils/component-engine.js +1 -1
- package/src/core/utils/context-builder.js +1 -1
- package/src/core/utils/datetime-formatter.js +1 -1
- package/src/core/utils/datetime-parser.js +1 -1
- package/src/core/utils/db.js +1 -1
- package/src/core/utils/logger.js +1 -1
- package/src/core/utils/payload-loader.js +1 -1
- package/src/core/utils/security-checks.js +1 -1
- package/src/middleware/body-options.js +1 -1
- package/src/middleware/cors.js +1 -1
- package/src/middleware/idempotency.js +1 -1
- package/src/middleware/rate-limiter.js +1 -1
- package/src/middleware/request-logger.js +1 -1
- package/src/middleware/security-headers.js +1 -1
- package/src/models/base-model-mysql.js +1 -1
- package/src/models/base-model-oracle.js +1 -1
- package/src/models/base-model-sqlite.js +1 -1
- package/src/models/base-model.js +1 -1
- package/src/pro/caching/redis-client.js +1 -1
- package/src/pro/caching/redis-helper.js +1 -1
- package/src/pro/consumers/baseConsumer.js +1 -1
- package/src/pro/consumers/declarativeMapper.js +1 -1
- package/src/pro/consumers/handlers/apiHandler.js +1 -1
- package/src/pro/consumers/handlers/consoleHandler.js +1 -1
- package/src/pro/consumers/handlers/databaseHandler.js +1 -1
- package/src/pro/consumers/handlers/index.js +1 -1
- package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
- package/src/pro/consumers/index.js +1 -1
- package/src/pro/consumers/messageTransformer.js +1 -1
- package/src/pro/consumers/validator.js +1 -1
- package/src/pro/database/base-model-mysql.js +1 -1
- package/src/pro/database/base-model-oracle.js +1 -1
- package/src/pro/database/base-model-sqlite.js +1 -1
- package/src/pro/database/db-mysql.js +1 -1
- package/src/pro/database/db-oracle.js +1 -1
- package/src/pro/database/db-sqlite.js +1 -1
- package/src/pro/excel/excel-generator.js +1 -1
- package/src/pro/excel/excel-parser.js +1 -1
- package/src/pro/excel/export-service.js +1 -1
- package/src/pro/excel/export_handler.js +1 -1
- package/src/pro/excel/import-service.js +1 -1
- package/src/pro/excel/import-validator.js +1 -1
- package/src/pro/excel/import_handler.js +1 -1
- package/src/pro/excel/upsert-builder.js +1 -1
- package/src/pro/idgen/idgen-routes.js +1 -1
- package/src/pro/integrations/lookup-resolver.js +1 -1
- package/src/pro/integrations/upload-handler-v2.js +1 -1
- package/src/pro/integrations/upload-handler.js +1 -1
- package/src/pro/integrations/webhook.js +1 -1
- package/src/pro/locking/lock-routes.js +1 -1
- package/src/pro/locking/resource-lock-manager.js +1 -1
- package/src/pro/messaging/kafkaConsumerService.js +1 -1
- package/src/pro/messaging/kafkaService.js +1 -1
- package/src/pro/messaging/messagehubService.js +1 -1
- package/src/pro/messaging/rabbitmqService.js +1 -1
- package/src/pro/scheduler/job-manager.js +1 -1
- package/src/pro/scheduler/job-routes.js +1 -1
- package/src/pro/scheduler/job-validator.js +1 -1
- package/src/pro/storage/base-storage-provider.js +1 -1
- package/src/pro/storage/file-metadata-helper.js +1 -1
- package/src/pro/storage/index.js +1 -1
- package/src/pro/storage/local-storage-provider.js +1 -1
- package/src/pro/storage/s3-storage-provider.js +1 -1
- package/src/pro/storage/upload-cleanup-job.js +1 -1
- package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
- package/src/pro/storage/upload-pending-tracker.js +1 -1
- package/src/pro/websocket/broadcast-helper.js +1 -1
- package/src/pro/websocket/index.js +1 -1
- package/src/pro/websocket/livesync-server.js +1 -1
- package/src/pro/websocket/ws-broadcaster.js +1 -1
- package/src/services/export-service.js +1 -1
- package/src/services/import-service.js +1 -1
- package/src/services/kafkaConsumerService.js +1 -1
- package/src/services/kafkaService.js +1 -1
- package/src/services/messagehubService.js +1 -1
- package/src/services/rabbitmqService.js +1 -1
- package/src/utils/cache-invalidation-registry.js +1 -1
- package/src/utils/cache-manager.js +1 -1
- package/src/utils/component-engine.js +1 -1
- package/src/utils/config-extractor.js +1 -1
- package/src/utils/consumerLogger.js +1 -1
- package/src/utils/context-builder.js +1 -1
- package/src/utils/dashboard-helpers.js +1 -1
- package/src/utils/dateHelper.js +1 -1
- package/src/utils/datetime-formatter.js +1 -1
- package/src/utils/datetime-parser.js +1 -1
- package/src/utils/db-bootstrap.js +1 -1
- package/src/utils/db-mysql.js +1 -1
- package/src/utils/db-oracle.js +1 -1
- package/src/utils/db-sqlite.js +1 -1
- package/src/utils/db.js +1 -1
- package/src/utils/demo-generator.js +1 -1
- package/src/utils/excel-generator.js +1 -1
- package/src/utils/excel-parser.js +1 -1
- package/src/utils/file-watcher.js +1 -1
- package/src/utils/id-generator.js +1 -1
- package/src/utils/idempotency-manager.js +1 -1
- package/src/utils/import-validator.js +1 -1
- package/src/utils/license-client.js +1 -1
- package/src/utils/lock-manager.js +1 -1
- package/src/utils/logger.js +1 -1
- package/src/utils/lookup-resolver.js +1 -1
- package/src/utils/payload-loader.js +1 -1
- package/src/utils/processor-response.js +1 -1
- package/src/utils/rabbitmq.js +1 -1
- package/src/utils/redis-client.js +1 -1
- package/src/utils/redis-helper.js +1 -1
- package/src/utils/request-scope.js +1 -1
- package/src/utils/security-checks.js +1 -1
- package/src/utils/service-resolver.js +1 -1
- package/src/utils/shutdown-coordinator.js +1 -1
- package/src/utils/soft-delete-dashboard-guard.js +1 -1
- package/src/utils/sql-table-extractor.js +1 -1
- package/src/utils/trusted-keys.js +1 -1
- package/src/utils/upload-handler.js +1 -1
- package/src/utils/upsert-builder.js +1 -1
- package/src/utils/workflow-hook-executor.js +1 -1
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
1108
|
-
|
|
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
|
-
/**
|
|
1151
|
-
|
|
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
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
-
//
|
|
1238
|
-
|
|
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
|
|
1306
|
-
//
|
|
1307
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
296
|
+
defaultValue: constraints.autoGenerate === true ? 'today' : extractDefault(constraints),
|
|
273
297
|
extra: {}
|
|
274
298
|
};
|
|
275
299
|
}
|
|
276
300
|
|
|
277
|
-
// Rule 7: Time
|
|
278
|
-
|
|
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:
|
|
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;
|