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