@restforgejs/platform 5.2.4 → 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 +1 -1
- package/cli/consumer.js +1 -1
- package/generators/cli/fast-track.js +235 -30
- package/generators/lib/migrate/backend-payload-migrator.js +39 -17
- package/generators/lib/migrate/field-type-resolver.js +64 -7
- package/generators/lib/payload/payload-runner.js +72 -6
- 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 +1 -1
- 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
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
-
/**
|
|
1161
|
-
|
|
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
|
-
/**
|
|
1204
|
-
|
|
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
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
-
//
|
|
1291
|
-
|
|
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
|
|
1359
|
-
//
|
|
1360
|
-
|
|
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
|
}
|
|
@@ -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));
|
|
@@ -1240,13 +1240,13 @@ class PayloadGenerator {
|
|
|
1240
1240
|
if (dataType === 'date') {
|
|
1241
1241
|
dateTimeFields[fieldName] = {
|
|
1242
1242
|
type: 'date',
|
|
1243
|
-
format: 'dd
|
|
1243
|
+
format: 'yyyy-MM-dd'
|
|
1244
1244
|
};
|
|
1245
1245
|
} else if (['timestamp', 'timestamp without time zone', 'timestamp with time zone'].includes(dataType)
|
|
1246
1246
|
|| dataType.startsWith('timestamp')) {
|
|
1247
1247
|
dateTimeFields[fieldName] = {
|
|
1248
1248
|
type: 'timestamp',
|
|
1249
|
-
format: 'dd
|
|
1249
|
+
format: 'yyyy-MM-dd HH:mm'
|
|
1250
1250
|
};
|
|
1251
1251
|
} else if (['time', 'time without time zone', 'time with time zone'].includes(dataType)) {
|
|
1252
1252
|
dateTimeFields[fieldName] = {
|
|
@@ -1257,7 +1257,7 @@ class PayloadGenerator {
|
|
|
1257
1257
|
// MySQL datetime type
|
|
1258
1258
|
dateTimeFields[fieldName] = {
|
|
1259
1259
|
type: 'timestamp',
|
|
1260
|
-
format: 'dd
|
|
1260
|
+
format: 'yyyy-MM-dd HH:mm'
|
|
1261
1261
|
};
|
|
1262
1262
|
}
|
|
1263
1263
|
}
|
|
@@ -1350,6 +1350,46 @@ class PayloadGenerator {
|
|
|
1350
1350
|
* @param {string} primaryKey - Primary key field name
|
|
1351
1351
|
* @returns {Array} fieldValidation array
|
|
1352
1352
|
*/
|
|
1353
|
+
/**
|
|
1354
|
+
* Ekstrak literal default value dari raw `column_default` hasil introspeksi
|
|
1355
|
+
* DB. Dipakai untuk tipe yang nilainya tetap berupa string mentah setelah
|
|
1356
|
+
* di-strip quote (string/text, DAN date/time yang literal-nya juga dikutip
|
|
1357
|
+
* dengan konvensi sama persis: 'YYYY-MM-DD'::date / 'HH:mm:ss'::time).
|
|
1358
|
+
* Berbeda dari integer/number/boolean (parseInt/parseFloat/match
|
|
1359
|
+
* 'true'/'false' otomatis mengabaikan noise), tipe-tipe ini butuh strip
|
|
1360
|
+
* eksplisit karena tiap dialect membungkus literal secara berbeda:
|
|
1361
|
+
* PostgreSQL: 'waiting'::character varying (quoted + type-cast suffix)
|
|
1362
|
+
* Oracle: 'waiting' (quoted, sudah di-trim)
|
|
1363
|
+
* SQLite: 'waiting' (quoted, dari pragma_table_info)
|
|
1364
|
+
* MySQL: waiting (bare, tanpa quote di versi baru)
|
|
1365
|
+
* Default berupa ekspresi/fungsi (uuid_generate_v4(), CURRENT_TIMESTAMP,
|
|
1366
|
+
* CURRENT_DATE, CURRENT_TIME, dst.) BUKAN literal statis -> return undefined
|
|
1367
|
+
* (tidak actionable sebagai default form/payload, konsisten dengan branch
|
|
1368
|
+
* integer/number yang skip non-numeric; juga konsisten dengan autoGenerate
|
|
1369
|
+
* yang menangani kasus "nilai dinamis saat insert" secara terpisah).
|
|
1370
|
+
* @param {string|null} rawDefault - col.column_default mentah dari introspeksi
|
|
1371
|
+
* @returns {string|undefined}
|
|
1372
|
+
*/
|
|
1373
|
+
extractLiteralDefault(rawDefault) {
|
|
1374
|
+
if (rawDefault === null || rawDefault === undefined) return undefined;
|
|
1375
|
+
const str = String(rawDefault).trim();
|
|
1376
|
+
if (str === '') return undefined;
|
|
1377
|
+
|
|
1378
|
+
// Quoted literal, dengan/tanpa type-cast suffix (::type). '' di dalam quote
|
|
1379
|
+
// adalah escape untuk satu single-quote literal (konvensi SQL standard).
|
|
1380
|
+
const quotedMatch = str.match(/^'((?:[^']|'')*)'(?:::[\w\s."]+)?$/);
|
|
1381
|
+
if (quotedMatch) {
|
|
1382
|
+
return quotedMatch[1].replace(/''/g, "'");
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// Bare literal (MySQL) vs ekspresi/fungsi/keyword -> tolak yang terakhir.
|
|
1386
|
+
if (/^[A-Za-z_][\w]*\s*\(.*\)$/.test(str)) return undefined;
|
|
1387
|
+
if (/^(NULL|CURRENT_TIMESTAMP|CURRENT_DATE|CURRENT_TIME)$/i.test(str)) return undefined;
|
|
1388
|
+
|
|
1389
|
+
return str;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
|
|
1353
1393
|
generateFieldValidation(detailedColumns, fieldNames, primaryKey) {
|
|
1354
1394
|
const fieldValidation = [];
|
|
1355
1395
|
|
|
@@ -1519,10 +1559,24 @@ class PayloadGenerator {
|
|
|
1519
1559
|
name: fieldName,
|
|
1520
1560
|
type: 'date',
|
|
1521
1561
|
constraints: {
|
|
1522
|
-
format: 'dd
|
|
1562
|
+
format: 'yyyy-MM-dd'
|
|
1523
1563
|
}
|
|
1524
1564
|
};
|
|
1525
1565
|
|
|
1566
|
+
// Detect auto-generate: CURRENT_DATE (mirror timestamp's now()/
|
|
1567
|
+
// CURRENT_TIMESTAMP/SYSTIMESTAMP). Didukung per handbook
|
|
1568
|
+
// field-validation.md ("autoGenerate ... Tipe yang didukung: uuid,
|
|
1569
|
+
// string, date, datetime, timestamp") tapi sebelumnya tidak pernah
|
|
1570
|
+
// di-deteksi generator ini.
|
|
1571
|
+
if (columnDefault.includes('current_date')) {
|
|
1572
|
+
entry.constraints.autoGenerate = true;
|
|
1573
|
+
} else {
|
|
1574
|
+
const defaultVal = this.extractLiteralDefault(col.column_default);
|
|
1575
|
+
if (defaultVal !== undefined) {
|
|
1576
|
+
entry.constraints.default = defaultVal;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1526
1580
|
fieldValidation.push(entry);
|
|
1527
1581
|
continue;
|
|
1528
1582
|
}
|
|
@@ -1537,7 +1591,7 @@ class PayloadGenerator {
|
|
|
1537
1591
|
name: fieldName,
|
|
1538
1592
|
type: 'datetime',
|
|
1539
1593
|
constraints: {
|
|
1540
|
-
format: 'dd
|
|
1594
|
+
format: 'yyyy-MM-dd HH:mm'
|
|
1541
1595
|
}
|
|
1542
1596
|
};
|
|
1543
1597
|
|
|
@@ -1557,10 +1611,17 @@ class PayloadGenerator {
|
|
|
1557
1611
|
name: fieldName,
|
|
1558
1612
|
type: 'time',
|
|
1559
1613
|
constraints: {
|
|
1560
|
-
format: 'HH:mm
|
|
1614
|
+
format: 'HH:mm'
|
|
1561
1615
|
}
|
|
1562
1616
|
};
|
|
1563
1617
|
|
|
1618
|
+
// autoGenerate TIDAK didukung untuk time per handbook
|
|
1619
|
+
// field-validation.md - hanya literal default yang relevan di sini.
|
|
1620
|
+
const defaultVal = this.extractLiteralDefault(col.column_default);
|
|
1621
|
+
if (defaultVal !== undefined) {
|
|
1622
|
+
entry.constraints.default = defaultVal;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1564
1625
|
fieldValidation.push(entry);
|
|
1565
1626
|
continue;
|
|
1566
1627
|
}
|
|
@@ -1609,6 +1670,11 @@ class PayloadGenerator {
|
|
|
1609
1670
|
entry.constraints.unique = true;
|
|
1610
1671
|
}
|
|
1611
1672
|
|
|
1673
|
+
const defaultVal = this.extractLiteralDefault(col.column_default);
|
|
1674
|
+
if (defaultVal !== undefined) {
|
|
1675
|
+
entry.constraints.default = defaultVal;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1612
1678
|
// Skip entry tanpa constraint agar payload tidak bloat — KECUALI type:'text'.
|
|
1613
1679
|
// Untuk text, tipe itu sendiri adalah sinyal "unbounded/long text" first-class
|
|
1614
1680
|
// yang harus bertahan di payload meski tidak ada constraint lain.
|