@restforgejs/platform 4.1.1 → 4.2.8

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 (178) hide show
  1. package/bin/sdf-tools.exe +0 -0
  2. package/build-info.json +2 -2
  3. package/cli/consumer-deploy.js +1 -1
  4. package/cli/consumer.js +1 -1
  5. package/generators/cli/endpoint/create.js +42 -3
  6. package/generators/cli/schema/apply.js +525 -0
  7. package/generators/cli/schema/diff.js +321 -0
  8. package/generators/cli/schema/generate-ddl.js +7 -10
  9. package/generators/cli/schema/init.js +95 -172
  10. package/generators/cli/schema/migrate.js +10 -16
  11. package/generators/cli/schema/models.js +8 -12
  12. package/generators/cli/schema/template.js +222 -0
  13. package/generators/cli/schema/validate.js +8 -12
  14. package/generators/cli-entry.js +17 -2
  15. package/generators/lib/dbschema-kit/apply-engine.js +582 -0
  16. package/generators/lib/dbschema-kit/diff-engine.js +703 -0
  17. package/generators/lib/dbschema-kit/diff-reporter.js +272 -0
  18. package/generators/lib/dbschema-kit/emitters/alter-table.js +275 -0
  19. package/generators/lib/payload/endpoint-schema-validator.js +171 -0
  20. package/generators/lib/payload/payload-runner.js +137 -220
  21. package/generators/lib/payload/schema-diff.js +277 -0
  22. package/generators/lib/utils/audit-columns.js +181 -0
  23. package/generators/lib/utils/cli-output.js +17 -0
  24. package/generators/lib/utils/database-introspector.js +16 -13
  25. package/integrity-manifest.json +8 -8
  26. package/package.json +1 -1
  27. package/scripts/verify-integrity.js +1 -1
  28. package/server.js +1 -1
  29. package/src/components/handlers/adjust_handler.js +1 -1
  30. package/src/components/handlers/audit_handler.js +1 -1
  31. package/src/components/handlers/delete_handler.js +1 -1
  32. package/src/components/handlers/export_handler.js +1 -1
  33. package/src/components/handlers/import_handler.js +1 -1
  34. package/src/components/handlers/insert_handler.js +1 -1
  35. package/src/components/handlers/update_handler.js +1 -1
  36. package/src/components/handlers/upload_handler.js +1 -1
  37. package/src/components/handlers/workflow_handler.js +1 -1
  38. package/src/components/integrations/webhook.js +1 -1
  39. package/src/consumers/baseConsumer.js +1 -1
  40. package/src/consumers/declarativeMapper.js +1 -1
  41. package/src/consumers/handlers/apiHandler.js +1 -1
  42. package/src/consumers/handlers/consoleHandler.js +1 -1
  43. package/src/consumers/handlers/databaseHandler.js +1 -1
  44. package/src/consumers/handlers/index.js +1 -1
  45. package/src/consumers/handlers/kafkaHandler.js +1 -1
  46. package/src/consumers/index.js +1 -1
  47. package/src/consumers/messageTransformer.js +1 -1
  48. package/src/consumers/validator.js +1 -1
  49. package/src/core/db/dialect/base-dialect.js +1 -1
  50. package/src/core/db/dialect/index.js +1 -1
  51. package/src/core/db/dialect/mysql-dialect.js +1 -1
  52. package/src/core/db/dialect/oracle-dialect.js +1 -1
  53. package/src/core/db/dialect/postgres-dialect.js +1 -1
  54. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  55. package/src/core/db/flatten-helper.js +1 -1
  56. package/src/core/db/query-builder-error.js +1 -1
  57. package/src/core/db/query-builder.js +1 -1
  58. package/src/core/db/relation-helper.js +1 -1
  59. package/src/core/handlers/delete_handler.js +1 -1
  60. package/src/core/handlers/insert_handler.js +1 -1
  61. package/src/core/handlers/update_handler.js +1 -1
  62. package/src/core/models/base-model.js +1 -1
  63. package/src/core/utils/cache-manager.js +1 -1
  64. package/src/core/utils/component-engine.js +1 -1
  65. package/src/core/utils/context-builder.js +1 -1
  66. package/src/core/utils/datetime-formatter.js +1 -1
  67. package/src/core/utils/datetime-parser.js +1 -1
  68. package/src/core/utils/db.js +1 -1
  69. package/src/core/utils/logger.js +1 -1
  70. package/src/core/utils/payload-loader.js +1 -1
  71. package/src/core/utils/security-checks.js +1 -1
  72. package/src/middleware/body-options.js +1 -1
  73. package/src/middleware/cors.js +1 -1
  74. package/src/middleware/idempotency.js +1 -1
  75. package/src/middleware/rate-limiter.js +1 -1
  76. package/src/middleware/request-logger.js +1 -1
  77. package/src/middleware/security-headers.js +1 -1
  78. package/src/models/base-model-mysql.js +1 -1
  79. package/src/models/base-model-oracle.js +1 -1
  80. package/src/models/base-model-sqlite.js +1 -1
  81. package/src/models/base-model.js +1 -1
  82. package/src/pro/caching/redis-client.js +1 -1
  83. package/src/pro/caching/redis-helper.js +1 -1
  84. package/src/pro/consumers/baseConsumer.js +1 -1
  85. package/src/pro/consumers/declarativeMapper.js +1 -1
  86. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  87. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  88. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  89. package/src/pro/consumers/handlers/index.js +1 -1
  90. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  91. package/src/pro/consumers/index.js +1 -1
  92. package/src/pro/consumers/messageTransformer.js +1 -1
  93. package/src/pro/consumers/validator.js +1 -1
  94. package/src/pro/database/base-model-mysql.js +1 -1
  95. package/src/pro/database/base-model-oracle.js +1 -1
  96. package/src/pro/database/base-model-sqlite.js +1 -1
  97. package/src/pro/database/db-mysql.js +1 -1
  98. package/src/pro/database/db-oracle.js +1 -1
  99. package/src/pro/database/db-sqlite.js +1 -1
  100. package/src/pro/excel/excel-generator.js +1 -1
  101. package/src/pro/excel/excel-parser.js +1 -1
  102. package/src/pro/excel/export-service.js +1 -1
  103. package/src/pro/excel/export_handler.js +1 -1
  104. package/src/pro/excel/import-service.js +1 -1
  105. package/src/pro/excel/import-validator.js +1 -1
  106. package/src/pro/excel/import_handler.js +1 -1
  107. package/src/pro/excel/upsert-builder.js +1 -1
  108. package/src/pro/idgen/idgen-routes.js +1 -1
  109. package/src/pro/integrations/lookup-resolver.js +1 -1
  110. package/src/pro/integrations/upload-handler-v2.js +1 -1
  111. package/src/pro/integrations/upload-handler.js +1 -1
  112. package/src/pro/integrations/webhook.js +1 -1
  113. package/src/pro/locking/lock-routes.js +1 -1
  114. package/src/pro/locking/resource-lock-manager.js +1 -1
  115. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  116. package/src/pro/messaging/kafkaService.js +1 -1
  117. package/src/pro/messaging/messagehubService.js +1 -1
  118. package/src/pro/messaging/rabbitmqService.js +1 -1
  119. package/src/pro/scheduler/job-manager.js +1 -1
  120. package/src/pro/scheduler/job-routes.js +1 -1
  121. package/src/pro/scheduler/job-validator.js +1 -1
  122. package/src/pro/storage/base-storage-provider.js +1 -1
  123. package/src/pro/storage/file-metadata-helper.js +1 -1
  124. package/src/pro/storage/index.js +1 -1
  125. package/src/pro/storage/local-storage-provider.js +1 -1
  126. package/src/pro/storage/s3-storage-provider.js +1 -1
  127. package/src/pro/storage/upload-cleanup-job.js +1 -1
  128. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  129. package/src/pro/storage/upload-pending-tracker.js +1 -1
  130. package/src/pro/websocket/broadcast-helper.js +1 -1
  131. package/src/pro/websocket/index.js +1 -1
  132. package/src/pro/websocket/livesync-server.js +1 -1
  133. package/src/pro/websocket/ws-broadcaster.js +1 -1
  134. package/src/services/export-service.js +1 -1
  135. package/src/services/import-service.js +1 -1
  136. package/src/services/kafkaConsumerService.js +1 -1
  137. package/src/services/kafkaService.js +1 -1
  138. package/src/services/messagehubService.js +1 -1
  139. package/src/services/rabbitmqService.js +1 -1
  140. package/src/utils/cache-invalidation-registry.js +1 -1
  141. package/src/utils/cache-manager.js +1 -1
  142. package/src/utils/component-engine.js +1 -1
  143. package/src/utils/config-extractor.js +1 -1
  144. package/src/utils/consumerLogger.js +1 -1
  145. package/src/utils/context-builder.js +1 -1
  146. package/src/utils/dashboard-helpers.js +1 -1
  147. package/src/utils/dateHelper.js +1 -1
  148. package/src/utils/datetime-formatter.js +1 -1
  149. package/src/utils/datetime-parser.js +1 -1
  150. package/src/utils/db-bootstrap.js +1 -1
  151. package/src/utils/db-mysql.js +1 -1
  152. package/src/utils/db-oracle.js +1 -1
  153. package/src/utils/db-sqlite.js +1 -1
  154. package/src/utils/db.js +1 -1
  155. package/src/utils/demo-generator.js +1 -1
  156. package/src/utils/excel-generator.js +1 -1
  157. package/src/utils/excel-parser.js +1 -1
  158. package/src/utils/file-watcher.js +1 -1
  159. package/src/utils/id-generator.js +1 -1
  160. package/src/utils/idempotency-manager.js +1 -1
  161. package/src/utils/import-validator.js +1 -1
  162. package/src/utils/license-client.js +1 -1
  163. package/src/utils/lock-manager.js +1 -1
  164. package/src/utils/logger.js +1 -1
  165. package/src/utils/lookup-resolver.js +1 -1
  166. package/src/utils/payload-loader.js +1 -1
  167. package/src/utils/processor-response.js +1 -1
  168. package/src/utils/rabbitmq.js +1 -1
  169. package/src/utils/redis-client.js +1 -1
  170. package/src/utils/redis-helper.js +1 -1
  171. package/src/utils/request-scope.js +1 -1
  172. package/src/utils/security-checks.js +1 -1
  173. package/src/utils/service-resolver.js +1 -1
  174. package/src/utils/shutdown-coordinator.js +1 -1
  175. package/src/utils/trusted-keys.js +1 -1
  176. package/src/utils/upload-handler.js +1 -1
  177. package/src/utils/upsert-builder.js +1 -1
  178. package/src/utils/workflow-hook-executor.js +1 -1
@@ -0,0 +1,321 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Contract: schema diff
5
+ *
6
+ * Drift detection antara Schema Definition File (SDF) dan struktur tabel
7
+ * actual di database. Read-only. Tidak melakukan modifikasi pada filesystem
8
+ * maupun database.
9
+ *
10
+ * Algoritma bidirectional: tampilkan delta dari kedua sisi (only-in-SDF,
11
+ * only-in-DB, mismatched) di level IR (Intermediate Representation),
12
+ * bypass formatting noise.
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const path = require('path');
18
+
19
+ const { loadSchemaPath } = require('../../lib/dbschema-kit/loader');
20
+ const { mapTableMetaToIR } = require('../../lib/dbschema-kit/introspect-mapper');
21
+ const { loadConfig } = require('../../lib/dbschema-kit/connection');
22
+ const { diffModels } = require('../../lib/dbschema-kit/diff-engine');
23
+ const { reportHuman, reportJson, getExitCode } = require('../../lib/dbschema-kit/diff-reporter');
24
+ const { resolveConfig, printDefaultConfigWarning } = require('../../lib/utils/config-resolver');
25
+
26
+ function loadIntrospector() {
27
+ const stubPath = process.env.DBSCHEMA_KIT_TEST_INTROSPECT_STUB;
28
+ if (stubPath) {
29
+ return require(path.resolve(stubPath));
30
+ }
31
+ return defaultIntrospector;
32
+ }
33
+
34
+ function mapDialectToDbType(dialect) {
35
+ switch (dialect) {
36
+ case 'postgres': return 'postgresql';
37
+ case 'mysql': return 'mysql';
38
+ case 'oracle': return 'oracle';
39
+ case 'sqlite': return 'sqlite';
40
+ default: return dialect;
41
+ }
42
+ }
43
+
44
+ function buildEphemeralEnvFile(config, dbType) {
45
+ const lines = [`DB_TYPE=${dbType}`];
46
+ if (config.host !== undefined) lines.push(`DB_HOST=${config.host}`);
47
+ if (config.port !== undefined) lines.push(`DB_PORT=${config.port}`);
48
+ if (dbType === 'oracle') {
49
+ lines.push(`DB_NAME=${config.serviceName || ''}`);
50
+ } else if (config.database !== undefined) {
51
+ lines.push(`DB_NAME=${config.database}`);
52
+ }
53
+ if (config.user !== undefined) lines.push(`DB_USER=${config.user}`);
54
+ if (config.password !== undefined) lines.push(`DB_PASSWORD=${config.password}`);
55
+ return lines.join('\n') + '\n';
56
+ }
57
+
58
+ function createTmpEnvFile(content) {
59
+ const tmpDir = os.tmpdir();
60
+ const tmpName = `restforge-diff-${process.pid}-${Date.now()}.env`;
61
+ const fullPath = path.join(tmpDir, tmpName);
62
+ fs.writeFileSync(fullPath, content, 'utf8');
63
+ return fullPath;
64
+ }
65
+
66
+ async function collectTableMeta(introspector, target) {
67
+ const ref = target.schemaName ? `${target.schemaName}.${target.tableName}` : target.tableName;
68
+ const detailedColumns = await introspector.getDetailedColumnInfo(ref);
69
+ const constraints = await introspector.getConstraints(ref);
70
+ const foreignKeys = await introspector.getForeignKeys(ref);
71
+ const indexes = await introspector.getIndexes(ref);
72
+
73
+ const pkConstraint = constraints.find((c) => c.type === 'PRIMARY KEY');
74
+ const uniqueConstraints = constraints
75
+ .filter((c) => c.type === 'UNIQUE')
76
+ .map((c) => ({ name: c.name, columns: c.columns }));
77
+
78
+ return {
79
+ tableName: target.tableName,
80
+ schemaName: target.schemaName || null,
81
+ columns: detailedColumns,
82
+ primaryKey: pkConstraint ? pkConstraint.columns : [],
83
+ uniques: uniqueConstraints,
84
+ foreignKeys,
85
+ indexes,
86
+ checks: []
87
+ };
88
+ }
89
+
90
+ const defaultIntrospector = {
91
+ introspect: async ({ config, tables }) => {
92
+ const dbType = mapDialectToDbType(config.dialect);
93
+ if (dbType === 'sqlite') {
94
+ throw new Error('schema:diff does not yet support sqlite. Use postgres, mysql, or oracle.');
95
+ }
96
+
97
+ const { DatabaseIntrospector } = require('../../lib/utils/database-introspector');
98
+ const introspector = new DatabaseIntrospector({ quiet: true });
99
+
100
+ const ephemeralEnv = buildEphemeralEnvFile(config, dbType);
101
+ const tmpPath = createTmpEnvFile(ephemeralEnv);
102
+
103
+ try {
104
+ const ok = introspector.loadConfig(tmpPath);
105
+ if (!ok) throw new Error('Failed to load config into DatabaseIntrospector');
106
+ const connected = await introspector.connect();
107
+ if (!connected) throw new Error('Failed to connect to database');
108
+
109
+ const out = [];
110
+ for (const target of tables) {
111
+ try {
112
+ out.push(await collectTableMeta(introspector, target));
113
+ } catch (err) {
114
+ // Tabel tidak ada di DB: catat dengan struktur kosong agar
115
+ // diff engine memperlakukannya sebagai "only-in-SDF" di sisi DB.
116
+ out.push({
117
+ tableName: target.tableName,
118
+ schemaName: target.schemaName || null,
119
+ columns: [],
120
+ primaryKey: [],
121
+ uniques: [],
122
+ foreignKeys: [],
123
+ indexes: [],
124
+ checks: [],
125
+ _missing: true,
126
+ _error: err && err.message ? err.message : String(err)
127
+ });
128
+ }
129
+ }
130
+ return { tables: out };
131
+ } finally {
132
+ try { await introspector.disconnect(); } catch (e) { /* ignore */ }
133
+ try { fs.unlinkSync(tmpPath); } catch (e) { /* ignore */ }
134
+ }
135
+ }
136
+ };
137
+
138
+ module.exports = {
139
+ resource: 'schema',
140
+ verb: 'diff',
141
+ description: 'Drift detection antara SDF dan struktur tabel database (read-only, bidirectional)',
142
+ category: 'introspection',
143
+ flags: {
144
+ path: {
145
+ type: 'string',
146
+ required: true,
147
+ description: 'Path file atau folder schema (mis. ./schema atau ./schema/users.js)'
148
+ },
149
+ config: {
150
+ type: 'string',
151
+ required: true,
152
+ description: 'File config database (.env)'
153
+ },
154
+ table: {
155
+ type: 'string',
156
+ required: false,
157
+ default: null,
158
+ description: 'Diff hanya satu tabel spesifik (default: semua model di SDF)'
159
+ },
160
+ json: {
161
+ type: 'boolean',
162
+ required: false,
163
+ default: false,
164
+ description: 'Output format JSON (default: human-readable plain)'
165
+ }
166
+ },
167
+ examples: [
168
+ 'npx restforge schema diff --path=./schema --config=db.env',
169
+ 'npx restforge schema diff --path=./schema/visitors.js --config=db.env --table=visitors',
170
+ 'npx restforge schema diff --path=./schema --config=db.env --json'
171
+ ],
172
+ async handler(args) {
173
+ // 1. Resolve config
174
+ const resolvedConfigResult = resolveConfig(args.config, process.cwd());
175
+ if (!resolvedConfigResult) {
176
+ console.error('Error: --config=<file> is required.');
177
+ console.error("Tip: set a default with 'npx restforge config set-default --config=<file>' to omit --config in future runs");
178
+ const err = new Error('--config=<file> is required');
179
+ err.exitCode = 2;
180
+ throw err;
181
+ }
182
+
183
+ if (resolvedConfigResult.source === 'default') {
184
+ printDefaultConfigWarning(resolvedConfigResult.defaultName);
185
+ }
186
+
187
+ let config;
188
+ try {
189
+ config = loadConfig(resolvedConfigResult.path);
190
+ } catch (err) {
191
+ console.error(`Error: ${err.message}`);
192
+ const e = new Error(err.message);
193
+ e.exitCode = 2;
194
+ throw e;
195
+ }
196
+
197
+ // 2. Load SDF
198
+ const schemaPath = args.path;
199
+ const absPath = path.resolve(process.cwd(), schemaPath);
200
+
201
+ let sdfModels;
202
+ try {
203
+ sdfModels = loadSchemaPath(absPath);
204
+ } catch (err) {
205
+ console.error(`Error: ${err.message}`);
206
+ const e = new Error(err.message);
207
+ e.exitCode = 2;
208
+ throw e;
209
+ }
210
+
211
+ if (sdfModels.size === 0) {
212
+ console.error(`Error: No schema models found at '${schemaPath}'.`);
213
+ const e = new Error(`No schema models found at '${schemaPath}'`);
214
+ e.exitCode = 2;
215
+ throw e;
216
+ }
217
+
218
+ // 3. Resolve target tables
219
+ let targetEntries = Array.from(sdfModels.entries());
220
+ if (args.table) {
221
+ targetEntries = targetEntries.filter(([qualified, ir]) => {
222
+ return ir.tableName === args.table || qualified === args.table;
223
+ });
224
+ if (targetEntries.length === 0) {
225
+ console.error(`Error: Table '${args.table}' not found in SDF.`);
226
+ const e = new Error(`Table '${args.table}' not found in SDF`);
227
+ e.exitCode = 2;
228
+ throw e;
229
+ }
230
+ }
231
+
232
+ const targetsForDb = targetEntries.map(([_, ir]) => ({
233
+ tableName: ir.tableName,
234
+ schemaName: ir.schemaName || null
235
+ }));
236
+
237
+ // 4. Connect database, introspect setiap tabel
238
+ let introspector;
239
+ try {
240
+ introspector = loadIntrospector();
241
+ } catch (err) {
242
+ console.error(`Error: Failed to load introspector: ${err.message}`);
243
+ const e = new Error(err.message);
244
+ e.exitCode = 2;
245
+ throw e;
246
+ }
247
+
248
+ let result;
249
+ try {
250
+ result = await introspector.introspect({ config, tables: targetsForDb });
251
+ } catch (err) {
252
+ const message = err && err.message ? err.message : String(err);
253
+ console.error(`Error: ${message}`);
254
+ const e = new Error(message);
255
+ e.exitCode = 2;
256
+ throw e;
257
+ }
258
+
259
+ if (!result || !Array.isArray(result.tables)) {
260
+ console.error('Error: Introspector did not return a tables array.');
261
+ const e = new Error('Introspector did not return a tables array');
262
+ e.exitCode = 2;
263
+ throw e;
264
+ }
265
+
266
+ // 5. Map DB results to IR (keyed by tableName supaya match dengan SDF)
267
+ const dialect = config.dialect;
268
+ const dbModels = new Map();
269
+ for (const tableMeta of result.tables) {
270
+ if (tableMeta._missing) {
271
+ // Tabel hilang di DB: register empty IR agar diff engine
272
+ // memperlakukan sebagai onlyInSdf untuk seluruh content.
273
+ const qualified = tableMeta.schemaName
274
+ ? `${tableMeta.schemaName}.${tableMeta.tableName}`
275
+ : tableMeta.tableName;
276
+ dbModels.set(qualified, {
277
+ tableName: tableMeta.tableName,
278
+ schemaName: tableMeta.schemaName || null,
279
+ qualifiedName: qualified,
280
+ fields: {},
281
+ primaryKey: [],
282
+ indexes: [],
283
+ uniques: [],
284
+ relations: {},
285
+ checks: []
286
+ });
287
+ continue;
288
+ }
289
+ const ir = mapTableMetaToIR(tableMeta, dialect);
290
+ dbModels.set(ir.qualifiedName || ir.tableName, ir);
291
+ }
292
+
293
+ // 6. Build SDF map matching same key convention as dbModels
294
+ const sdfMap = new Map();
295
+ for (const [_, ir] of targetEntries) {
296
+ sdfMap.set(ir.qualifiedName || ir.tableName, ir);
297
+ }
298
+
299
+ // 7. Compute diff
300
+ const deltas = diffModels(sdfMap, dbModels);
301
+
302
+ // 8. Report
303
+ if (args.json) {
304
+ const obj = reportJson(deltas);
305
+ process.stdout.write(JSON.stringify(obj, null, 2));
306
+ process.stdout.write('\n');
307
+ } else {
308
+ const text = reportHuman(deltas);
309
+ process.stdout.write(text);
310
+ process.stdout.write('\n');
311
+ }
312
+
313
+ // 9. Exit code
314
+ const exitCode = getExitCode(deltas);
315
+ if (exitCode !== 0) {
316
+ const err = new Error(`schema diff detected drift in ${deltas.filter((d) => d.hasDrift).length} table(s)`);
317
+ err.exitCode = exitCode;
318
+ throw err;
319
+ }
320
+ }
321
+ };
@@ -27,6 +27,11 @@ module.exports = {
27
27
  description: 'Generate DDL SQL (CREATE TABLE, CREATE INDEX, opsional DROP) untuk seluruh schema models',
28
28
  category: 'generation',
29
29
  flags: {
30
+ path: {
31
+ type: 'string',
32
+ required: true,
33
+ description: 'Path file atau folder schema (mis. ./schema atau ./schema/users.js)'
34
+ },
30
35
  dialect: {
31
36
  type: 'string',
32
37
  required: true,
@@ -45,17 +50,9 @@ module.exports = {
45
50
  description: 'Sertakan statement DROP TABLE sebelum CREATE'
46
51
  }
47
52
  },
48
- positional: [
49
- {
50
- name: 'path',
51
- type: 'string',
52
- required: true,
53
- description: 'Path file atau folder schema'
54
- }
55
- ],
56
53
  examples: [
57
- 'npx restforge schema generate-ddl ./schema --dialect=postgres --out=migration.sql',
58
- 'npx restforge schema generate-ddl ./schema --dialect=mysql --drop=true'
54
+ 'npx restforge schema generate-ddl --path=./schema --dialect=postgres --out=migration.sql',
55
+ 'npx restforge schema generate-ddl --path=./schema --dialect=mysql --drop=true'
59
56
  ],
60
57
  async handler(args) {
61
58
  if (!args.dialect) {
@@ -1,172 +1,95 @@
1
- 'use strict';
2
-
3
- /**
4
- * Contract: schema init
5
- *
6
- * Create a skeleton schema definition file. The table name is derived from the
7
- * target filename (without `.js` extension). The skeleton is a complete example
8
- * including fields, indexes, uniques, and relations.
9
- *
10
- * Inline migration v4.0.0: business logic dari dbschema-init.js
11
- * dipindah ke handler.
12
- */
13
-
14
- const fs = require('fs');
15
- const path = require('path');
16
-
17
- function buildSkeleton(tableName) {
18
- const omitFkExample = tableName === 'table_parent';
19
-
20
- const fkField = omitFkExample
21
- ? ''
22
- : `
23
- // Foreign key placeholder (rename to actual parent column; relation declared below)
24
- table_parent_id: 'string:36 notnull',
25
- `;
26
-
27
- const uniquesEntry = omitFkExample
28
- ? ` ['${tableName}_code', '${tableName}_name']`
29
- : ` ['${tableName}_code', 'table_parent_id']`;
30
-
31
- const indexesEntry = omitFkExample
32
- ? ` '${tableName}_name',
33
- ['${tableName}_code', 'is_active']`
34
- : ` '${tableName}_name',
35
- ['table_parent_id', 'is_active']`;
36
-
37
- const relationsBlock = omitFkExample
38
- ? ''
39
- : `,
40
-
41
- // Relations to parent tables (belongsTo only at this layer)
42
- relations: {
43
- parent: {
44
- type: 'belongsTo',
45
- target: 'table_parent',
46
- localKey: 'table_parent_id',
47
- references: 'table_parent_id',
48
- onDelete: 'restrict'
49
- }
50
- }`;
51
-
52
- return `'use strict';
53
-
54
- /**
55
- * Schema definition for \`${tableName}\`
56
- *
57
- * Generated by: npx restforge schema init
58
- * Adjust fields, indexes, uniques, and relations to match the actual table.
59
- */
60
- module.exports = ({ defineModel }) => defineModel('${tableName}', {
61
- fields: {
62
- // Primary key (string:36 fits a UUID; backend defaults to uuidv7 at insert)
63
- ${tableName}_id: 'string:36 pk',
64
-
65
- // Required fields with simple constraints
66
- ${tableName}_code: 'string:20 unique notnull',
67
- ${tableName}_name: 'string:100 notnull',
68
-
69
- // Optional field with default
70
- is_active: 'boolean default:true',
71
-
72
- // Numeric: decimal:precision,scale
73
- credit_limit: 'decimal:18,2 default:0',
74
-
75
- // String column constrained by CHECK (defined in checks: section below)
76
- status: "string:20 notnull default:'active'",
77
- ${fkField}
78
- // Audit columns (RESTForge 4-column convention)
79
- // See dbschema:catalog --section=auditColumns for the full spec.
80
- // The same 4 columns are referenced by field-validation:catalog
81
- // auditColumns on the RDF/backend side, so SDF and RDF stay aligned.
82
- created_at: 'timestamp default:now()',
83
- created_by: 'string:100',
84
- updated_at: 'timestamp autoUpdate',
85
- updated_by: 'string:100'
86
- },
87
-
88
- // Composite uniques (single-column unique uses shorthand on field above)
89
- uniques: [
90
- ${uniquesEntry}
91
- ],
92
-
93
- // Indexes for query performance (single column or composite)
94
- indexes: [
95
- ${indexesEntry}
96
- ],
97
-
98
- // CHECK constraints (operators: in, gt, gte, lt, lte, eq, neq)
99
- checks: [
100
- { field: 'status', in: ['active', 'inactive', 'suspended'] },
101
- { field: 'credit_limit', gte: 0 }
102
- ]${relationsBlock}
103
- });
104
- `;
105
- }
106
-
107
- module.exports = {
108
- resource: 'schema',
109
- verb: 'init',
110
- description: 'Membuat skeleton file schema definition (fields, indexes, uniques, relations)',
111
- category: 'utility',
112
- flags: {},
113
- positional: [
114
- {
115
- name: 'path',
116
- type: 'string',
117
- required: true,
118
- description: 'Path file schema yang akan dibuat (mis. schema/users.js)'
119
- }
120
- ],
121
- examples: [
122
- 'npx restforge schema init schema/users.js',
123
- 'npx restforge schema init ./schema/item_product.js'
124
- ],
125
- async handler(args) {
126
- if (!args.path) {
127
- console.error('Error: Target file path is required.');
128
- console.error("Run 'npx restforge schema init --help' for usage information");
129
- throw new Error('Target file path is required');
130
- }
131
-
132
- const absPath = path.resolve(process.cwd(), args.path);
133
-
134
- if (path.extname(absPath) !== '.js') {
135
- console.error(`Error: Schema file must have .js extension: ${absPath}`);
136
- throw new Error(`Schema file must have .js extension: ${absPath}`);
137
- }
138
-
139
- if (fs.existsSync(absPath)) {
140
- console.error(`Error: File already exists: ${absPath}`);
141
- throw new Error(`File already exists: ${absPath}`);
142
- }
143
-
144
- const tableName = path.basename(absPath, '.js');
145
- if (!/^[a-z][a-z0-9_]*$/i.test(tableName)) {
146
- console.error(
147
- `Error: Invalid table name '${tableName}' (derived from filename). ` +
148
- `Must start with a letter and contain only letters, digits, or underscore.`
149
- );
150
- throw new Error(`Invalid table name '${tableName}' (derived from filename)`);
151
- }
152
-
153
- const parentDir = path.dirname(absPath);
154
- try {
155
- fs.mkdirSync(parentDir, { recursive: true });
156
- } catch (err) {
157
- console.error(`Error: Failed to create directory '${parentDir}': ${err.message}`);
158
- throw new Error(`Failed to create directory '${parentDir}': ${err.message}`);
159
- }
160
-
161
- try {
162
- fs.writeFileSync(absPath, buildSkeleton(tableName), 'utf8');
163
- } catch (err) {
164
- console.error(`Error: Failed to write file '${absPath}': ${err.message}`);
165
- throw new Error(`Failed to write file '${absPath}': ${err.message}`);
166
- }
167
-
168
- console.log(`✓ Created schema skeleton: ${absPath}`);
169
- console.log(` Table name: ${tableName}`);
170
- console.log(' Edit fields, indexes, uniques, and relations to match the actual table.');
171
- }
172
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Contract: schema init
5
+ *
6
+ * Thin wrapper untuk scaffold file schema definition. Implementasi sekarang
7
+ * mendelegasikan ke binary native sdf-tools.exe dengan template `dummy`,
8
+ * menggantikan generator skeleton custom yang sebelumnya inline.
9
+ *
10
+ * Setara dengan: sdf-tools.exe --table=dummy --generate --lang=sdf --path=<USER_PATH>
11
+ *
12
+ * Kontrak CLI dipertahankan (verb, category, flag --path) agar workflow user
13
+ * tidak breaking. File yang dihasilkan berisi struktur template `dummy` dari
14
+ * koleksi RestForge Schema Reference, yang dapat di-rename dan diadaptasi
15
+ * sesuai kebutuhan.
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const os = require('os');
20
+ const path = require('path');
21
+ const { spawnSync } = require('child_process');
22
+
23
+ function resolveBinaryPath() {
24
+ if (os.platform() !== 'win32') return null;
25
+ return path.resolve(__dirname, '..', '..', '..', 'bin', 'sdf-tools.exe');
26
+ }
27
+
28
+ module.exports = {
29
+ resource: 'schema',
30
+ verb: 'init',
31
+ description: 'Membuat skeleton file schema definition (delegate ke sdf-tools dengan template dummy)',
32
+ category: 'utility',
33
+ flags: {
34
+ path: {
35
+ type: 'string',
36
+ required: true,
37
+ description: 'Path file schema yang akan dibuat (mis. schema/users.js)'
38
+ }
39
+ },
40
+ examples: [
41
+ 'npx restforge schema init --path=schema/users.js',
42
+ 'npx restforge schema init --path=./schema/item_product.js'
43
+ ],
44
+ async handler(args) {
45
+ if (!args.path) {
46
+ console.error('Error: Target file path is required.');
47
+ console.error("Run 'npx restforge schema init --help' for usage information");
48
+ throw new Error('Target file path is required');
49
+ }
50
+
51
+ const binaryPath = resolveBinaryPath();
52
+ if (!binaryPath) {
53
+ const err = new Error(
54
+ `schema init hanya tersedia di Windows (sdf-tools.exe). Platform saat ini: ${os.platform()}`
55
+ );
56
+ err.exitCode = 3;
57
+ throw err;
58
+ }
59
+
60
+ if (!fs.existsSync(binaryPath)) {
61
+ const err = new Error(
62
+ `sdf-tools.exe tidak ditemukan di ${binaryPath}. ` +
63
+ 'Pastikan binary sudah di-build dan tersedia di folder bin/ package.'
64
+ );
65
+ err.exitCode = 3;
66
+ throw err;
67
+ }
68
+
69
+ const binaryArgs = [
70
+ '--table=dummy',
71
+ '--generate',
72
+ '--lang=sdf',
73
+ `--path=${args.path}`
74
+ ];
75
+
76
+ const result = spawnSync(binaryPath, binaryArgs, {
77
+ stdio: 'inherit',
78
+ windowsHide: true
79
+ });
80
+
81
+ if (result.error) {
82
+ const err = new Error(`Gagal menjalankan sdf-tools.exe: ${result.error.message}`);
83
+ err.exitCode = 1;
84
+ throw err;
85
+ }
86
+
87
+ const status = typeof result.status === 'number' ? result.status : 1;
88
+ if (status !== 0) {
89
+ const err = new Error(`sdf-tools.exe exit code ${status}`);
90
+ err.exitCode = status;
91
+ err.silent = true;
92
+ throw err;
93
+ }
94
+ }
95
+ };