@restforgejs/platform 5.1.4 → 5.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/build-info.json +2 -2
  2. package/cli/consumer-deploy.js +1 -1
  3. package/cli/consumer.js +1 -1
  4. package/generators/cli/data/pull.js +16 -7
  5. package/generators/cli/data/push.js +17 -7
  6. package/generators/cli/fast-track.js +63 -43
  7. package/generators/cli/init.js +347 -97
  8. package/generators/lib/data/data-scope.js +138 -0
  9. package/generators/lib/data/envelope.js +25 -9
  10. package/generators/lib/data/pull-runner.js +44 -37
  11. package/generators/lib/data/push-runner.js +64 -46
  12. package/generators/lib/templates/dashboard-catalog.js +1 -1
  13. package/generators/lib/templates/db-connection-env.js +1 -1
  14. package/generators/lib/templates/dbschema-catalog.js +1 -1
  15. package/generators/lib/templates/field-validation-catalog.js +1 -1
  16. package/generators/lib/templates/mysql-template.js +1 -1
  17. package/generators/lib/templates/oracle-template.js +1 -1
  18. package/generators/lib/templates/postgres-template.js +1 -1
  19. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  20. package/generators/lib/templates/sqlite-template.js +1 -1
  21. package/integrity-manifest.json +18 -18
  22. package/package.json +1 -1
  23. package/scripts/verify-integrity.js +1 -1
  24. package/server.js +1 -1
  25. package/src/components/handlers/adjust_handler.js +1 -1
  26. package/src/components/handlers/audit_handler.js +1 -1
  27. package/src/components/handlers/delete_handler.js +1 -1
  28. package/src/components/handlers/export_handler.js +1 -1
  29. package/src/components/handlers/import_handler.js +1 -1
  30. package/src/components/handlers/insert_handler.js +1 -1
  31. package/src/components/handlers/update_handler.js +1 -1
  32. package/src/components/handlers/upload_handler.js +1 -1
  33. package/src/components/handlers/workflow_handler.js +1 -1
  34. package/src/components/integrations/webhook.js +1 -1
  35. package/src/consumers/baseConsumer.js +1 -1
  36. package/src/consumers/declarativeMapper.js +1 -1
  37. package/src/consumers/handlers/apiHandler.js +1 -1
  38. package/src/consumers/handlers/consoleHandler.js +1 -1
  39. package/src/consumers/handlers/databaseHandler.js +1 -1
  40. package/src/consumers/handlers/index.js +1 -1
  41. package/src/consumers/handlers/kafkaHandler.js +1 -1
  42. package/src/consumers/index.js +1 -1
  43. package/src/consumers/messageTransformer.js +1 -1
  44. package/src/consumers/validator.js +1 -1
  45. package/src/core/db/dialect/base-dialect.js +1 -1
  46. package/src/core/db/dialect/index.js +1 -1
  47. package/src/core/db/dialect/mysql-dialect.js +1 -1
  48. package/src/core/db/dialect/oracle-dialect.js +1 -1
  49. package/src/core/db/dialect/postgres-dialect.js +1 -1
  50. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  51. package/src/core/db/flatten-helper.js +1 -1
  52. package/src/core/db/query-builder-error.js +1 -1
  53. package/src/core/db/query-builder.js +1 -1
  54. package/src/core/db/relation-helper.js +1 -1
  55. package/src/core/handlers/delete_handler.js +1 -1
  56. package/src/core/handlers/insert_handler.js +1 -1
  57. package/src/core/handlers/update_handler.js +1 -1
  58. package/src/core/models/base-model.js +1 -1
  59. package/src/core/utils/cache-manager.js +1 -1
  60. package/src/core/utils/component-engine.js +1 -1
  61. package/src/core/utils/context-builder.js +1 -1
  62. package/src/core/utils/datetime-formatter.js +1 -1
  63. package/src/core/utils/datetime-parser.js +1 -1
  64. package/src/core/utils/db.js +1 -1
  65. package/src/core/utils/logger.js +1 -1
  66. package/src/core/utils/payload-loader.js +1 -1
  67. package/src/core/utils/security-checks.js +1 -1
  68. package/src/middleware/body-options.js +1 -1
  69. package/src/middleware/cors.js +1 -1
  70. package/src/middleware/idempotency.js +1 -1
  71. package/src/middleware/rate-limiter.js +1 -1
  72. package/src/middleware/request-logger.js +1 -1
  73. package/src/middleware/security-headers.js +1 -1
  74. package/src/models/base-model-mysql.js +1 -1
  75. package/src/models/base-model-oracle.js +1 -1
  76. package/src/models/base-model-sqlite.js +1 -1
  77. package/src/models/base-model.js +1 -1
  78. package/src/pro/caching/redis-client.js +1 -1
  79. package/src/pro/caching/redis-helper.js +1 -1
  80. package/src/pro/consumers/baseConsumer.js +1 -1
  81. package/src/pro/consumers/declarativeMapper.js +1 -1
  82. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  83. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  84. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  85. package/src/pro/consumers/handlers/index.js +1 -1
  86. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  87. package/src/pro/consumers/index.js +1 -1
  88. package/src/pro/consumers/messageTransformer.js +1 -1
  89. package/src/pro/consumers/validator.js +1 -1
  90. package/src/pro/database/base-model-mysql.js +1 -1
  91. package/src/pro/database/base-model-oracle.js +1 -1
  92. package/src/pro/database/base-model-sqlite.js +1 -1
  93. package/src/pro/database/db-mysql.js +1 -1
  94. package/src/pro/database/db-oracle.js +1 -1
  95. package/src/pro/database/db-sqlite.js +1 -1
  96. package/src/pro/excel/excel-generator.js +1 -1
  97. package/src/pro/excel/excel-parser.js +1 -1
  98. package/src/pro/excel/export-service.js +1 -1
  99. package/src/pro/excel/export_handler.js +1 -1
  100. package/src/pro/excel/import-service.js +1 -1
  101. package/src/pro/excel/import-validator.js +1 -1
  102. package/src/pro/excel/import_handler.js +1 -1
  103. package/src/pro/excel/upsert-builder.js +1 -1
  104. package/src/pro/idgen/idgen-routes.js +1 -1
  105. package/src/pro/integrations/lookup-resolver.js +1 -1
  106. package/src/pro/integrations/upload-handler-v2.js +1 -1
  107. package/src/pro/integrations/upload-handler.js +1 -1
  108. package/src/pro/integrations/webhook.js +1 -1
  109. package/src/pro/locking/lock-routes.js +1 -1
  110. package/src/pro/locking/resource-lock-manager.js +1 -1
  111. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  112. package/src/pro/messaging/kafkaService.js +1 -1
  113. package/src/pro/messaging/messagehubService.js +1 -1
  114. package/src/pro/messaging/rabbitmqService.js +1 -1
  115. package/src/pro/scheduler/job-manager.js +1 -1
  116. package/src/pro/scheduler/job-routes.js +1 -1
  117. package/src/pro/scheduler/job-validator.js +1 -1
  118. package/src/pro/storage/base-storage-provider.js +1 -1
  119. package/src/pro/storage/file-metadata-helper.js +1 -1
  120. package/src/pro/storage/index.js +1 -1
  121. package/src/pro/storage/local-storage-provider.js +1 -1
  122. package/src/pro/storage/s3-storage-provider.js +1 -1
  123. package/src/pro/storage/upload-cleanup-job.js +1 -1
  124. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  125. package/src/pro/storage/upload-pending-tracker.js +1 -1
  126. package/src/pro/websocket/broadcast-helper.js +1 -1
  127. package/src/pro/websocket/index.js +1 -1
  128. package/src/pro/websocket/livesync-server.js +1 -1
  129. package/src/pro/websocket/ws-broadcaster.js +1 -1
  130. package/src/services/export-service.js +1 -1
  131. package/src/services/import-service.js +1 -1
  132. package/src/services/kafkaConsumerService.js +1 -1
  133. package/src/services/kafkaService.js +1 -1
  134. package/src/services/messagehubService.js +1 -1
  135. package/src/services/rabbitmqService.js +1 -1
  136. package/src/utils/cache-invalidation-registry.js +1 -1
  137. package/src/utils/cache-manager.js +1 -1
  138. package/src/utils/component-engine.js +1 -1
  139. package/src/utils/config-extractor.js +1 -1
  140. package/src/utils/consumerLogger.js +1 -1
  141. package/src/utils/context-builder.js +1 -1
  142. package/src/utils/dashboard-helpers.js +1 -1
  143. package/src/utils/dateHelper.js +1 -1
  144. package/src/utils/datetime-formatter.js +1 -1
  145. package/src/utils/datetime-parser.js +1 -1
  146. package/src/utils/db-bootstrap.js +1 -1
  147. package/src/utils/db-mysql.js +1 -1
  148. package/src/utils/db-oracle.js +1 -1
  149. package/src/utils/db-sqlite.js +1 -1
  150. package/src/utils/db.js +1 -1
  151. package/src/utils/demo-generator.js +1 -1
  152. package/src/utils/excel-generator.js +1 -1
  153. package/src/utils/excel-parser.js +1 -1
  154. package/src/utils/file-watcher.js +1 -1
  155. package/src/utils/id-generator.js +1 -1
  156. package/src/utils/idempotency-manager.js +1 -1
  157. package/src/utils/import-validator.js +1 -1
  158. package/src/utils/license-client.js +1 -1
  159. package/src/utils/lock-manager.js +1 -1
  160. package/src/utils/logger.js +1 -1
  161. package/src/utils/lookup-resolver.js +1 -1
  162. package/src/utils/payload-loader.js +1 -1
  163. package/src/utils/processor-response.js +1 -1
  164. package/src/utils/rabbitmq.js +1 -1
  165. package/src/utils/redis-client.js +1 -1
  166. package/src/utils/redis-helper.js +1 -1
  167. package/src/utils/request-scope.js +1 -1
  168. package/src/utils/security-checks.js +1 -1
  169. package/src/utils/service-resolver.js +1 -1
  170. package/src/utils/shutdown-coordinator.js +1 -1
  171. package/src/utils/trusted-keys.js +1 -1
  172. package/src/utils/upload-handler.js +1 -1
  173. package/src/utils/upsert-builder.js +1 -1
  174. package/src/utils/workflow-hook-executor.js +1 -1
@@ -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/db-connection.env` sebagai template starting point.
8
- *
9
- * Inline migration v4.0.0: business logic asli dari `init.js` di-inline
10
- * langsung ke handler. Adapter `process.argv` override dihapus. `process.exit()`
11
- * di legacy dikonversi menjadi `throw new Error(...)` agar `cli-entry.js` yang
12
- * handle exit code.
13
- *
14
- * Catatan: contract ini global verb (snapshot key = `init`, bukan
15
- * `<resource>/init`). Struktur file di `generators/cli/init.js` (root cli/
16
- * directory, bukan subdirectory resource).
17
- */
18
-
19
- const path = require('node:path');
20
- const fs = require('node:fs');
21
- const { DB_CONNECTION_ENV_TEMPLATE: TEMPLATE_ENV } = require('../lib/templates/db-connection-env');
22
-
23
- module.exports = {
24
- verb: 'init',
25
- description: 'Generate starter config file (config/db-connection.env) di working directory',
26
- category: 'utility',
27
- flags: {
28
- force: {
29
- type: 'boolean',
30
- required: false,
31
- default: false,
32
- description: 'Timpa file existing tanpa konfirmasi'
33
- }
34
- },
35
- examples: [
36
- 'npx restforge init',
37
- 'npx restforge init --force'
38
- ],
39
- async handler(args) {
40
- const workingDir = process.cwd();
41
-
42
- console.log('');
43
- console.log('RESTForge - Init');
44
- console.log('====================');
45
- console.log('');
46
-
47
- const files = [
48
- {
49
- relativePath: path.join('config', 'db-connection.env'),
50
- content: TEMPLATE_ENV
51
- }
52
- ];
53
-
54
- for (const file of files) {
55
- file.absolutePath = path.resolve(workingDir, file.relativePath);
56
- }
57
-
58
- const existingFiles = files.filter(f => fs.existsSync(f.absolutePath));
59
-
60
- if (existingFiles.length > 0 && !args.force) {
61
- console.log('WARNING: The following files already exist:');
62
- console.log('');
63
- for (const file of existingFiles) {
64
- console.log(` ${file.relativePath}`);
65
- }
66
- console.log('');
67
- console.log('Use --force to overwrite:');
68
- console.log(' npx restforge init --force');
69
- console.log('');
70
- throw new Error(`${existingFiles.length} file(s) already exist. Use --force to overwrite.`);
71
- }
72
-
73
- const dirs = [
74
- path.resolve(workingDir, 'config')
75
- ];
76
-
77
- for (const dir of dirs) {
78
- if (!fs.existsSync(dir)) {
79
- fs.mkdirSync(dir, { recursive: true });
80
- }
81
- }
82
-
83
- for (const file of files) {
84
- fs.writeFileSync(file.absolutePath, file.content, 'utf8');
85
- console.log(` Created: ${file.relativePath}`);
86
- }
87
-
88
- console.log('');
89
- console.log('Done! Sample files generated successfully.');
90
- console.log('');
91
- console.log('Next Steps:');
92
- console.log('');
93
- console.log(' 1. Edit config/db-connection.env');
94
- console.log(' Update database credentials (DB_HOST, DB_USER, DB_PASSWORD, DB_NAME)');
95
- console.log('');
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
+ }
@@ -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
+ };