@restforgejs/platform 5.1.0 → 5.1.6
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/data/pull.js +104 -0
- package/generators/cli/data/push.js +95 -0
- package/generators/cli/fast-track.js +12 -25
- package/generators/cli/init.js +347 -97
- package/generators/cli/schema/introspect.js +10 -10
- package/generators/lib/data/data-scope.js +138 -0
- package/generators/lib/data/db-executor.js +440 -0
- package/generators/lib/data/dialect-kit.js +56 -0
- package/generators/lib/data/envelope.js +236 -0
- package/generators/lib/data/pull-runner.js +414 -0
- package/generators/lib/data/push-runner.js +400 -0
- package/generators/lib/data/sdf-reader.js +132 -0
- package/generators/lib/data/table-order.js +126 -0
- package/generators/lib/data/value-codec.js +188 -0
- 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/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
package/generators/cli/init.js
CHANGED
|
@@ -1,97 +1,347 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Contract: init (verb global)
|
|
5
|
-
*
|
|
6
|
-
* Generate starter config file di working directory. Membuat
|
|
7
|
-
* `config
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Contract: init (verb global)
|
|
5
|
+
*
|
|
6
|
+
* Generate starter config file di working directory. Membuat
|
|
7
|
+
* `config/<nama-file>.env` sebagai template starting point.
|
|
8
|
+
*
|
|
9
|
+
* Dua mode operasi:
|
|
10
|
+
* 1. Interaktif (default, hanya saat stdin TTY dan TANPA --force):
|
|
11
|
+
* dialog konfirmasi gaya fast-track. Meminta input LICENSE, tipe database
|
|
12
|
+
* beserta atributnya, dan nama file config (default db-connection.env, bisa
|
|
13
|
+
* di-custom). Nilai input di-patch ke template lalu ditulis.
|
|
14
|
+
* 2. Non-interaktif (saat --force diberikan ATAU stdin bukan TTY):
|
|
15
|
+
* menulis template apa adanya ke config/db-connection.env (perilaku legacy).
|
|
16
|
+
* Jalur ini dipakai oleh orkestrator (fast-track) dan playbook yang
|
|
17
|
+
* memanggil `npx restforge init --force`, sehingga harus tetap deterministik
|
|
18
|
+
* tanpa prompt.
|
|
19
|
+
*
|
|
20
|
+
* Catatan kompatibilitas: `--force` sengaja mempertahankan semantik non-interaktif
|
|
21
|
+
* agar pemanggil existing (fast-track.js, restforge-playbook) tidak menggantung.
|
|
22
|
+
* Tidak ada flag baru ditambahkan sehingga snapshot contract tidak berubah.
|
|
23
|
+
*
|
|
24
|
+
* Catatan: contract ini global verb (snapshot key = `init`, bukan
|
|
25
|
+
* `<resource>/init`). Struktur file di `generators/cli/init.js` (root cli/
|
|
26
|
+
* directory, bukan subdirectory resource).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
const readline = require('node:readline');
|
|
32
|
+
const { DB_CONNECTION_ENV_TEMPLATE: TEMPLATE_ENV } = require('../lib/templates/db-connection-env');
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Default konfigurasi (selaras dengan placeholder pada template db-connection.env)
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
const DEFAULT_CONFIG_FILE = 'db-connection.env';
|
|
39
|
+
|
|
40
|
+
const DB_DEFAULTS = {
|
|
41
|
+
LICENSE: 'XXXX-XXXX-XXXX-XXXX',
|
|
42
|
+
DB_TYPE: 'postgresql',
|
|
43
|
+
DB_HOST: '127.0.0.1',
|
|
44
|
+
DB_PORT: '5432',
|
|
45
|
+
DB_USER: 'postgres',
|
|
46
|
+
DB_PASSWORD: 'your_password_here',
|
|
47
|
+
DB_NAME: 'your_database_name',
|
|
48
|
+
DB_FILE: './data/myapp.db'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Override DB_PORT/DB_USER/DB_NAME menyesuaikan DB_TYPE yang dipilih; bila tidak
|
|
52
|
+
// terdaftar, fallback ke DB_DEFAULTS (selaras dengan fast-track).
|
|
53
|
+
const DB_TYPE_DEFAULTS = {
|
|
54
|
+
postgresql: { DB_PORT: '5432', DB_USER: 'postgres' },
|
|
55
|
+
postgres: { DB_PORT: '5432', DB_USER: 'postgres' },
|
|
56
|
+
mysql: { DB_PORT: '3306', DB_USER: 'root' },
|
|
57
|
+
oracle: { DB_PORT: '1521', DB_USER: 'oracle', DB_NAME: 'ORCL' }
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const SUPPORTED_DB_TYPES = ['postgresql', 'mysql', 'oracle', 'sqlite'];
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Helper
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Prompter sederhana berbasis readline.question. init interaktif hanya berjalan
|
|
68
|
+
* pada TTY (lihat handler), sehingga tidak butuh penanganan input pipe seperti
|
|
69
|
+
* fast-track. `ask` dapat di-inject pada test via test seam.
|
|
70
|
+
*/
|
|
71
|
+
function createPrompter() {
|
|
72
|
+
const rl = readline.createInterface({
|
|
73
|
+
input: process.stdin,
|
|
74
|
+
output: process.stdout
|
|
75
|
+
});
|
|
76
|
+
const ask = (message) => new Promise((resolve) => {
|
|
77
|
+
rl.question(message, (answer) => resolve(answer));
|
|
78
|
+
});
|
|
79
|
+
return { ask, close: () => rl.close() };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Masking license untuk tampilan review: grup pertama dan terakhir terlihat,
|
|
84
|
+
* grup tengah disamarkan (mis. `8ECD-****-****-698Q`). Nilai asli tetap dipakai
|
|
85
|
+
* untuk penulisan file.
|
|
86
|
+
*/
|
|
87
|
+
function maskLicense(license) {
|
|
88
|
+
if (!license || typeof license !== 'string') return license;
|
|
89
|
+
const groups = license.split('-');
|
|
90
|
+
if (groups.length >= 3) {
|
|
91
|
+
return groups
|
|
92
|
+
.map((g, i) => (i === 0 || i === groups.length - 1) ? g : '*'.repeat(g.length))
|
|
93
|
+
.join('-');
|
|
94
|
+
}
|
|
95
|
+
if (license.length > 8) {
|
|
96
|
+
return `${license.slice(0, 4)}${'*'.repeat(license.length - 8)}${license.slice(-4)}`;
|
|
97
|
+
}
|
|
98
|
+
return license;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Default atribut database berdasarkan DB_TYPE (merge DB_DEFAULTS + override). */
|
|
102
|
+
function dbDefaultsFor(dbType) {
|
|
103
|
+
const override = DB_TYPE_DEFAULTS[dbType] || {};
|
|
104
|
+
return { ...DB_DEFAULTS, ...override };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Representasi database untuk baris review, berdasarkan konfigurasi input. */
|
|
108
|
+
function describeDatabase(cfg) {
|
|
109
|
+
if (cfg.DB_TYPE === 'sqlite') return `sqlite @ ${cfg.DB_FILE}`;
|
|
110
|
+
return `${cfg.DB_TYPE} @ ${cfg.DB_HOST}:${cfg.DB_PORT}/${cfg.DB_NAME}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Fase input konfigurasi interaktif: LICENSE lalu tipe database beserta
|
|
115
|
+
* atributnya. Mengembalikan objek cfg. `ask` di-inject agar dapat diuji.
|
|
116
|
+
*/
|
|
117
|
+
async function collectConfig(ask) {
|
|
118
|
+
const askField = async (label, def) => {
|
|
119
|
+
const input = (await ask(` ${label} (${def}): `)).trim();
|
|
120
|
+
return input || def;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
console.log('');
|
|
124
|
+
console.log('Enter configuration (press Enter to accept the default value):');
|
|
125
|
+
console.log('');
|
|
126
|
+
|
|
127
|
+
const cfg = {};
|
|
128
|
+
|
|
129
|
+
// 1) LICENSE
|
|
130
|
+
cfg.LICENSE = await askField('LICENSE', DB_DEFAULTS.LICENSE);
|
|
131
|
+
|
|
132
|
+
// 2) Tipe database + atributnya
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log(` Supported DB_TYPE: ${SUPPORTED_DB_TYPES.join(', ')}`);
|
|
135
|
+
cfg.DB_TYPE = (await askField('DB_TYPE', DB_DEFAULTS.DB_TYPE)).toLowerCase();
|
|
136
|
+
|
|
137
|
+
if (cfg.DB_TYPE === 'sqlite') {
|
|
138
|
+
console.log('');
|
|
139
|
+
console.log(' SQLite mode: DB_HOST, DB_PORT, DB_USER, DB_PASSWORD are ignored.');
|
|
140
|
+
console.log(' The database file path is set in DB_NAME (via DB_FILE).');
|
|
141
|
+
console.log('');
|
|
142
|
+
cfg.DB_FILE = await askField('DB_FILE (.db file path)', DB_DEFAULTS.DB_FILE);
|
|
143
|
+
} else {
|
|
144
|
+
const d = dbDefaultsFor(cfg.DB_TYPE);
|
|
145
|
+
cfg.DB_HOST = await askField('DB_HOST', d.DB_HOST);
|
|
146
|
+
cfg.DB_PORT = await askField('DB_PORT', d.DB_PORT);
|
|
147
|
+
cfg.DB_USER = await askField('DB_USER', d.DB_USER);
|
|
148
|
+
cfg.DB_PASSWORD = await askField('DB_PASSWORD', d.DB_PASSWORD);
|
|
149
|
+
cfg.DB_NAME = await askField('DB_NAME', d.DB_NAME);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return cfg;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Susun map nilai env yang akan di-patch ke template dari konfigurasi input. */
|
|
156
|
+
function envValuesFromCfg(cfg) {
|
|
157
|
+
const v = {
|
|
158
|
+
LICENSE: cfg.LICENSE,
|
|
159
|
+
DB_TYPE: cfg.DB_TYPE
|
|
160
|
+
};
|
|
161
|
+
if (cfg.DB_TYPE === 'sqlite') {
|
|
162
|
+
// Untuk SQLite, path file disimpan pada DB_NAME (dikonsumsi runtime).
|
|
163
|
+
v.DB_NAME = cfg.DB_FILE;
|
|
164
|
+
} else {
|
|
165
|
+
v.DB_HOST = cfg.DB_HOST;
|
|
166
|
+
v.DB_PORT = cfg.DB_PORT;
|
|
167
|
+
v.DB_USER = cfg.DB_USER;
|
|
168
|
+
v.DB_PASSWORD = cfg.DB_PASSWORD;
|
|
169
|
+
v.DB_NAME = cfg.DB_NAME;
|
|
170
|
+
}
|
|
171
|
+
return v;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Patch VALUE sejumlah KEY pada teks template tanpa mengubah baris lain
|
|
176
|
+
* (komentar, section Redis/Kafka/dll. tetap utuh). KEY yang belum ada di
|
|
177
|
+
* template ditambahkan di akhir.
|
|
178
|
+
*/
|
|
179
|
+
function applyConfigToTemplate(template, values) {
|
|
180
|
+
const eol = template.includes('\r\n') ? '\r\n' : '\n';
|
|
181
|
+
const lines = template.split(/\r?\n/);
|
|
182
|
+
const remaining = new Set(Object.keys(values));
|
|
183
|
+
for (let i = 0; i < lines.length; i++) {
|
|
184
|
+
const m = lines[i].match(/^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
185
|
+
if (m && remaining.has(m[2])) {
|
|
186
|
+
lines[i] = `${m[1]}${m[2]}=${values[m[2]]}`;
|
|
187
|
+
remaining.delete(m[2]);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const k of remaining) lines.push(`${k}=${values[k]}`);
|
|
191
|
+
return lines.join(eol);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Tulis file config ke config/<configFileName>, buat folder bila perlu. */
|
|
195
|
+
function writeConfigFile(workingDir, configFileName, content) {
|
|
196
|
+
const configDir = path.resolve(workingDir, 'config');
|
|
197
|
+
if (!fs.existsSync(configDir)) {
|
|
198
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
const absolutePath = path.resolve(configDir, configFileName);
|
|
201
|
+
fs.writeFileSync(absolutePath, content, 'utf8');
|
|
202
|
+
console.log(` Created: ${path.join('config', configFileName)}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Cetak ringkasan akhir + next steps. */
|
|
206
|
+
function printDone(configFileName) {
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log('Done! Sample files generated successfully.');
|
|
209
|
+
console.log('');
|
|
210
|
+
console.log('Next Steps:');
|
|
211
|
+
console.log('');
|
|
212
|
+
console.log(` 1. Edit config/${configFileName}`);
|
|
213
|
+
console.log(' Update database credentials (DB_HOST, DB_USER, DB_PASSWORD, DB_NAME)');
|
|
214
|
+
console.log('');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Review + konfirmasi sebelum menulis (mode interaktif). */
|
|
218
|
+
async function confirmWrite(ctx, ask) {
|
|
219
|
+
console.log('');
|
|
220
|
+
console.log('====================================================');
|
|
221
|
+
console.log(' Review configuration');
|
|
222
|
+
console.log('====================================================');
|
|
223
|
+
console.log(` License : ${maskLicense(ctx.cfg.LICENSE)}`);
|
|
224
|
+
console.log(` Database : ${describeDatabase(ctx.cfg)}`);
|
|
225
|
+
const existsNote = ctx.exists ? ' (EXISTING — will be overwritten)' : '';
|
|
226
|
+
console.log(` Target : config/${ctx.configFileName}${existsNote}`);
|
|
227
|
+
console.log('');
|
|
228
|
+
const answer = (await ask(' Write this config? (Y/n): ')).trim().toLowerCase();
|
|
229
|
+
return !(answer === 'n' || answer === 'no');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Jalur non-interaktif: tulis template apa adanya ke config/db-connection.env.
|
|
234
|
+
* Mempertahankan perilaku legacy (termasuk guard file existing tanpa --force).
|
|
235
|
+
*/
|
|
236
|
+
function runNonInteractive(workingDir, force) {
|
|
237
|
+
const configFileName = DEFAULT_CONFIG_FILE;
|
|
238
|
+
const relativePath = path.join('config', configFileName);
|
|
239
|
+
const absolutePath = path.resolve(workingDir, relativePath);
|
|
240
|
+
|
|
241
|
+
if (fs.existsSync(absolutePath) && !force) {
|
|
242
|
+
console.log('WARNING: The following files already exist:');
|
|
243
|
+
console.log('');
|
|
244
|
+
console.log(` ${relativePath}`);
|
|
245
|
+
console.log('');
|
|
246
|
+
console.log('Use --force to overwrite:');
|
|
247
|
+
console.log(' npx restforge init --force');
|
|
248
|
+
console.log('');
|
|
249
|
+
throw new Error('1 file(s) already exist. Use --force to overwrite.');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
writeConfigFile(workingDir, configFileName, TEMPLATE_ENV);
|
|
253
|
+
printDone(configFileName);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Jalur interaktif: kumpulkan input, konfirmasi, lalu tulis template ter-patch. */
|
|
257
|
+
async function runInteractive(workingDir, prompter) {
|
|
258
|
+
const { ask } = prompter;
|
|
259
|
+
|
|
260
|
+
// 1) LICENSE + 2) tipe database & atributnya.
|
|
261
|
+
const cfg = await collectConfig(ask);
|
|
262
|
+
|
|
263
|
+
// 3) Nama file config (terakhir; default db-connection.env, bisa di-custom).
|
|
264
|
+
console.log('');
|
|
265
|
+
const fnameInput = (await ask(` Config file name (${DEFAULT_CONFIG_FILE}): `)).trim();
|
|
266
|
+
// basename untuk mencegah path traversal; nama selalu relatif ke config/.
|
|
267
|
+
const configFileName = path.basename(fnameInput || DEFAULT_CONFIG_FILE);
|
|
268
|
+
|
|
269
|
+
const absolutePath = path.resolve(workingDir, 'config', configFileName);
|
|
270
|
+
const exists = fs.existsSync(absolutePath);
|
|
271
|
+
|
|
272
|
+
const ok = await confirmWrite({ cfg, configFileName, exists }, ask);
|
|
273
|
+
if (!ok) {
|
|
274
|
+
console.log('');
|
|
275
|
+
console.log(' Aborted. No changes were made.');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const content = applyConfigToTemplate(TEMPLATE_ENV, envValuesFromCfg(cfg));
|
|
280
|
+
console.log('');
|
|
281
|
+
writeConfigFile(workingDir, configFileName, content);
|
|
282
|
+
printDone(configFileName);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Contract
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
module.exports = {
|
|
290
|
+
verb: 'init',
|
|
291
|
+
description: 'Generate starter config file (config/db-connection.env) di working directory. Interaktif di terminal; non-interaktif dengan --force',
|
|
292
|
+
category: 'utility',
|
|
293
|
+
flags: {
|
|
294
|
+
force: {
|
|
295
|
+
type: 'boolean',
|
|
296
|
+
required: false,
|
|
297
|
+
default: false,
|
|
298
|
+
description: 'Timpa file existing tanpa konfirmasi'
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
examples: [
|
|
302
|
+
'npx restforge init',
|
|
303
|
+
'npx restforge init --force'
|
|
304
|
+
],
|
|
305
|
+
async handler(args) {
|
|
306
|
+
const workingDir = process.cwd();
|
|
307
|
+
|
|
308
|
+
console.log('');
|
|
309
|
+
console.log('RESTForge - Init');
|
|
310
|
+
console.log('====================');
|
|
311
|
+
|
|
312
|
+
// Interaktif hanya bila TTY dan TANPA --force. `--force` dan konteks
|
|
313
|
+
// non-TTY (pipe/CI/spawn dari orkestrator) memakai jalur non-interaktif
|
|
314
|
+
// deterministik agar tidak menggantung menunggu input.
|
|
315
|
+
const interactive = args.force !== true && Boolean(process.stdin.isTTY);
|
|
316
|
+
|
|
317
|
+
if (!interactive) {
|
|
318
|
+
runNonInteractive(workingDir, args.force === true);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const prompter = createPrompter();
|
|
323
|
+
try {
|
|
324
|
+
await runInteractive(workingDir, prompter);
|
|
325
|
+
} finally {
|
|
326
|
+
prompter.close();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Test seam (hanya aktif via env): mengekspos helper internal agar alur
|
|
332
|
+
// interaktif dapat diuji dengan `ask` ter-inject tanpa stdin TTY. Pada
|
|
333
|
+
// penggunaan CLI normal env ini tidak di-set sehingga export tetap contract murni.
|
|
334
|
+
if (process.env.INIT_TEST === '1') {
|
|
335
|
+
module.exports.__test = {
|
|
336
|
+
collectConfig,
|
|
337
|
+
applyConfigToTemplate,
|
|
338
|
+
envValuesFromCfg,
|
|
339
|
+
dbDefaultsFor,
|
|
340
|
+
describeDatabase,
|
|
341
|
+
maskLicense,
|
|
342
|
+
confirmWrite,
|
|
343
|
+
runInteractive,
|
|
344
|
+
DEFAULT_CONFIG_FILE,
|
|
345
|
+
SUPPORTED_DB_TYPES
|
|
346
|
+
};
|
|
347
|
+
}
|
|
@@ -180,7 +180,7 @@ function parseSchemaFlag(value) {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
function resolveMode(args, schemaList, tableQualified) {
|
|
183
|
-
const out = args
|
|
183
|
+
const out = args['schema-path'];
|
|
184
184
|
const isDry = args['dry-run'];
|
|
185
185
|
|
|
186
186
|
if (isDry && !out) {
|
|
@@ -200,7 +200,7 @@ function resolveMode(args, schemaList, tableQualified) {
|
|
|
200
200
|
if (expectedBase !== targetTableName) {
|
|
201
201
|
return {
|
|
202
202
|
error: `Single-mode output filename '${path.basename(out)}' does not match --table='${targetTableName}'. ` +
|
|
203
|
-
`Use --
|
|
203
|
+
`Use --schema-path=<folder> or rename the file to '${targetTableName}.js'.`
|
|
204
204
|
};
|
|
205
205
|
}
|
|
206
206
|
return { kind: 'single-file', file: path.resolve(out), table: targetTableName };
|
|
@@ -209,7 +209,7 @@ function resolveMode(args, schemaList, tableQualified) {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
if (looksLikeJs) {
|
|
212
|
-
return { error: `Bulk mode requires a folder for --
|
|
212
|
+
return { error: `Bulk mode requires a folder for --schema-path, got file '${out}'. Specify a folder or add --table=<name>.` };
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
const isMultiSchema = (args['all-schemas']) || (Array.isArray(schemaList) && schemaList.length > 1);
|
|
@@ -242,7 +242,7 @@ module.exports = {
|
|
|
242
242
|
default: null,
|
|
243
243
|
description: 'File config database (.env). Fallback ke `.restforge/defaults.json` bila tidak disediakan eksplisit (set via `config set-default`)'
|
|
244
244
|
},
|
|
245
|
-
|
|
245
|
+
'schema-path': {
|
|
246
246
|
type: 'string',
|
|
247
247
|
required: true,
|
|
248
248
|
description: 'Folder atau file output schema'
|
|
@@ -279,9 +279,9 @@ module.exports = {
|
|
|
279
279
|
}
|
|
280
280
|
},
|
|
281
281
|
examples: [
|
|
282
|
-
'npx restforge schema introspect --config=db.env --
|
|
283
|
-
'npx restforge schema introspect --config=db.env --
|
|
284
|
-
'npx restforge schema introspect --config=db.env --
|
|
282
|
+
'npx restforge schema introspect --config=db.env --schema-path=./schema',
|
|
283
|
+
'npx restforge schema introspect --config=db.env --schema-path=./schema --table=users --dry-run',
|
|
284
|
+
'npx restforge schema introspect --config=db.env --schema-path=./schema --all-schemas --force'
|
|
285
285
|
],
|
|
286
286
|
async handler(args) {
|
|
287
287
|
const resolvedConfigResult = resolveConfig(args.config, process.cwd());
|
|
@@ -291,9 +291,9 @@ module.exports = {
|
|
|
291
291
|
throw new Error('--config=<file> is required');
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
if (!args['dry-run'] && !args
|
|
295
|
-
console.error('Error: --
|
|
296
|
-
throw new Error('--
|
|
294
|
+
if (!args['dry-run'] && !args['schema-path']) {
|
|
295
|
+
console.error('Error: --schema-path=<path> is required (or use --dry-run to preview to stdout).');
|
|
296
|
+
throw new Error('--schema-path=<path> is required');
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
if (resolvedConfigResult.source === 'default') {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* data-scope — Helper bersama untuk scope flags & derivasi path file data.
|
|
5
|
+
*
|
|
6
|
+
* Dipakai oleh `data pull` dan `data push` agar kedua command berbagi satu-satunya
|
|
7
|
+
* sumber kebenaran untuk: (a) parsing `--schema` comma-separated, (b) validasi
|
|
8
|
+
* "tepat satu" dari `{--table, --schema, --all-schemas}`, (c) derivasi path file
|
|
9
|
+
* relatif (bersarang per schema vs flat), dan (d) resolusi scope flag → daftar IR.
|
|
10
|
+
*
|
|
11
|
+
* Model scope (mirror `schema introspect`, namun "tepat satu" alih-alih kombinasi):
|
|
12
|
+
* --table=<name> → satu IR via findTableOrThrow (qualified/unqualified).
|
|
13
|
+
* --schema=<LIST> → semua IR ber-schemaName ∈ LIST; 0 match = exit 2 (anti-typo).
|
|
14
|
+
* --all-schemas → seluruh IR (boleh kosong → caller tangani sebagai 0 tabel).
|
|
15
|
+
*
|
|
16
|
+
* Aturan path seragam (Q2): layout ditentukan MURNI oleh schema tabel di IR.
|
|
17
|
+
* ber-schema → `<schemaName>/<tableName>.json`
|
|
18
|
+
* tanpa schema → `<tableName>.json` (flat root)
|
|
19
|
+
*
|
|
20
|
+
* Murni in-memory, tanpa I/O maupun koneksi DB.
|
|
21
|
+
*
|
|
22
|
+
* @module generators/lib/data/data-scope
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const { findTableOrThrow, getNamespace } = require('./sdf-reader');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Buat Error dengan exitCode terlampir (precondition/usage = 2).
|
|
30
|
+
* @param {string} message
|
|
31
|
+
* @param {number} exitCode
|
|
32
|
+
* @returns {Error}
|
|
33
|
+
*/
|
|
34
|
+
function failWith(message, exitCode) {
|
|
35
|
+
const err = new Error(message);
|
|
36
|
+
err.exitCode = exitCode;
|
|
37
|
+
return err;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse flag `--schema` (single atau comma-separated) → array nama schema.
|
|
42
|
+
*
|
|
43
|
+
* Selaras pola `schema introspect` (introspect.js:175-180): split koma, trim tiap
|
|
44
|
+
* elemen, buang elemen kosong. Mengembalikan `null` bila bukan string atau hasil
|
|
45
|
+
* akhir kosong (mis. `--schema=` atau `--schema=,,`), sehingga caller dapat
|
|
46
|
+
* memperlakukan "schema tidak disebut" secara seragam.
|
|
47
|
+
*
|
|
48
|
+
* @param {*} value - Nilai flag --schema
|
|
49
|
+
* @returns {string[]|null}
|
|
50
|
+
*/
|
|
51
|
+
function parseSchemaFlag(value) {
|
|
52
|
+
if (typeof value !== 'string') return null;
|
|
53
|
+
const parts = value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
54
|
+
if (parts.length === 0) return null;
|
|
55
|
+
return parts;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Path file data relatif terhadap storage dir, diturunkan MURNI dari schema IR.
|
|
60
|
+
*
|
|
61
|
+
* Satu-satunya titik derivasi nama file untuk pull & push (menghindari drift tiga
|
|
62
|
+
* titik edit lama). Tabel ber-schema → bersarang `<schemaName>/<tableName>.json`;
|
|
63
|
+
* tabel tanpa schema → flat `<tableName>.json`. Pemanggil cukup
|
|
64
|
+
* `path.join(storageDir, relDataFilePath(ir))`.
|
|
65
|
+
*
|
|
66
|
+
* @param {Object} ir - IR model SDF satu tabel
|
|
67
|
+
* @returns {string} Path relatif (memakai separator OS via path.join)
|
|
68
|
+
*/
|
|
69
|
+
function relDataFilePath(ir) {
|
|
70
|
+
const { schemaName } = getNamespace(ir);
|
|
71
|
+
return schemaName
|
|
72
|
+
? path.join(schemaName, `${ir.tableName}.json`)
|
|
73
|
+
: `${ir.tableName}.json`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Tegakkan "tepat satu" dari `{--table, --schema, --all-schemas}`.
|
|
78
|
+
*
|
|
79
|
+
* Arg-parser tidak punya mekanisme XOR lintas-flag (hanya `required` per-flag),
|
|
80
|
+
* sehingga validasi tiga-arah ditegakkan di sini. Dipanggil SEBELUM load SDF /
|
|
81
|
+
* koneksi DB. Keduanya/lebih maupun tidak ada satupun → exit 2 dengan pesan sama.
|
|
82
|
+
*
|
|
83
|
+
* @param {Object} args - argumen ter-parse (args.table, args['all-schemas'])
|
|
84
|
+
* @param {string[]|null} schemaList - hasil parseSchemaFlag(args.schema)
|
|
85
|
+
* @throws {Error} exitCode=2 bila BUKAN tepat satu yang aktif
|
|
86
|
+
*/
|
|
87
|
+
function validateScopeFlags(args, schemaList) {
|
|
88
|
+
const hasTable = args.table != null && args.table !== '';
|
|
89
|
+
const hasSchema = Array.isArray(schemaList) && schemaList.length > 0;
|
|
90
|
+
const hasAll = args['all-schemas'] === true;
|
|
91
|
+
|
|
92
|
+
const count = (hasTable ? 1 : 0) + (hasSchema ? 1 : 0) + (hasAll ? 1 : 0);
|
|
93
|
+
if (count !== 1) {
|
|
94
|
+
throw failWith('Specify exactly one of --table, --schema, or --all-schemas.', 2);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolusi scope flag → daftar IR yang akan diproses (urutan deklarasi Map).
|
|
100
|
+
*
|
|
101
|
+
* Mengasumsikan validateScopeFlags() sudah lolos (tepat satu flag aktif).
|
|
102
|
+
* --table → [findTableOrThrow(models, args.table)] (exit 2 bila tak terdaftar/ambigu).
|
|
103
|
+
* --schema=<L> → subset IR ber-schemaName ∈ L; 0 match → exit 2 (sinyal typo).
|
|
104
|
+
* --all-schemas → seluruh IR (boleh kosong → caller perlakukan sebagai 0 tabel, exit 0).
|
|
105
|
+
*
|
|
106
|
+
* @param {Map<string, Object>} models - hasil loadSdf
|
|
107
|
+
* @param {Object} args - argumen ter-parse
|
|
108
|
+
* @param {string[]|null} schemaList - hasil parseSchemaFlag(args.schema)
|
|
109
|
+
* @returns {Object[]} array IR sesuai scope
|
|
110
|
+
* @throws {Error} exitCode=2 (table tak terdaftar/ambigu, atau --schema 0-match)
|
|
111
|
+
*/
|
|
112
|
+
function resolveScope(models, args, schemaList) {
|
|
113
|
+
const hasTable = args.table != null && args.table !== '';
|
|
114
|
+
if (hasTable) {
|
|
115
|
+
return [findTableOrThrow(models, args.table)];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (Array.isArray(schemaList) && schemaList.length > 0) {
|
|
119
|
+
const set = new Set(schemaList);
|
|
120
|
+
const subset = [...models.values()].filter((ir) => set.has(getNamespace(ir).schemaName));
|
|
121
|
+
if (subset.length === 0) {
|
|
122
|
+
throw failWith(`No tables found for schema(s): ${schemaList.join(', ')}.`, 2);
|
|
123
|
+
}
|
|
124
|
+
return subset;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --all-schemas: seluruh tabel (semua schema, termasuk schemaless). Boleh kosong.
|
|
128
|
+
return [...models.values()];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
parseSchemaFlag,
|
|
133
|
+
relDataFilePath,
|
|
134
|
+
validateScopeFlags,
|
|
135
|
+
resolveScope,
|
|
136
|
+
// Diekspor untuk reuse/test internal.
|
|
137
|
+
_internal: { failWith }
|
|
138
|
+
};
|