@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
@@ -33,6 +33,7 @@ const MetadataManager = require('../../lib/utils/metadata-manager');
33
33
  const DemoGenerator = require('../../src/utils/demo-generator');
34
34
  const projectRegistry = require('../../lib/utils/project-registry');
35
35
  const cliOutput = require('../../lib/utils/cli-output');
36
+ const endpointSchemaValidator = require('../../lib/payload/endpoint-schema-validator');
36
37
 
37
38
  function hasAuditRequired(payload) {
38
39
  if (!payload || !payload.fieldPolicy) return false;
@@ -91,6 +92,18 @@ module.exports = {
91
92
  default: null,
92
93
  description: 'Tipe database (postgres|mysql|oracle|sqlite). Default: auto-detect dari config'
93
94
  },
95
+ config: {
96
+ type: 'string',
97
+ required: false,
98
+ default: null,
99
+ description: 'File config database (.env) untuk validasi schema payload-vs-database. Wajib kecuali `--skip-schema-check` aktif atau default config sudah di-set via `config set-default`. Fallback ke `.restforge/defaults.json` bila tidak disediakan eksplisit'
100
+ },
101
+ 'skip-schema-check': {
102
+ type: 'boolean',
103
+ required: false,
104
+ default: false,
105
+ description: 'Lewati validasi schema database (escape hatch untuk DB offline atau maintenance). Shape RDF tetap divalidasi'
106
+ },
94
107
  force: {
95
108
  type: 'boolean',
96
109
  required: false,
@@ -123,8 +136,9 @@ module.exports = {
123
136
  }
124
137
  },
125
138
  examples: [
126
- 'npx restforge endpoint create --project=my-app --name=users --payload=users.json',
127
- 'npx restforge endpoint create --project=my-app --name=orders --payload=orders.json --force'
139
+ 'npx restforge endpoint create --project=my-app --name=users --payload=users.json --config=db.env',
140
+ 'npx restforge endpoint create --project=my-app --name=orders --payload=orders.json --config=db.env --force=true',
141
+ 'npx restforge endpoint create --project=my-app --name=visitors --payload=visitors.json --skip-schema-check'
128
142
  ],
129
143
  async handler(args) {
130
144
  const startTime = Date.now();
@@ -142,6 +156,10 @@ module.exports = {
142
156
  const skipSqlValidation = !!args['skip-sql-validation'];
143
157
  const noAuditMigration = !!args['no-audit-migration'];
144
158
  const verbose = !!args.verbose;
159
+ const skipSchemaCheck = !!args['skip-schema-check'];
160
+ const configArg = typeof args.config === 'string' && args.config.trim().length > 0
161
+ ? args.config.trim()
162
+ : null;
145
163
 
146
164
  if (!verbose) {
147
165
  cliOutput.mute();
@@ -178,6 +196,26 @@ module.exports = {
178
196
  warnings: cliOutput.drainWarnings()
179
197
  };
180
198
 
199
+ // Schema validation pre-codegen: cross-check payload terhadap struktur
200
+ // tabel database aktual. Wajib unless --skip-schema-check di-set.
201
+ // Drift -> throw (exitCode=1). Connection fail -> throw (exitCode=3).
202
+ // Validasi dilakukan SEBELUM conflict-checker + archive supaya gagal
203
+ // tanpa rotate file existing.
204
+ if (muted) cliOutput.unmute();
205
+ const schemaResult = await endpointSchemaValidator.validateEndpointSchema({
206
+ payload,
207
+ payloadFileName: path.basename(payloadFile),
208
+ configArg,
209
+ skipSchemaCheck,
210
+ workingDir: cwd
211
+ });
212
+ if (!verbose) {
213
+ cliOutput.mute();
214
+ muted = true;
215
+ }
216
+ summary.schemaValidation = schemaResult;
217
+ summary.config.config = configArg || null;
218
+
181
219
  const registry = projectRegistry.loadProjectRegistry();
182
220
  if (registry.projects[project]) {
183
221
  const existing = registry.projects[project];
@@ -271,8 +309,9 @@ module.exports = {
271
309
  summary.duration = ((Date.now() - startTime) / 1000).toFixed(2);
272
310
  cliOutput.printCreateSummary(summary);
273
311
  } catch (error) {
312
+ // cli-entry.js men-print `Error: <message>` ke stderr saat handler
313
+ // re-throw (lihat handler dispatch). Jangan double-print di sini.
274
314
  if (muted) cliOutput.unmute();
275
- console.error(`Error: ${error.message}`);
276
315
  throw error;
277
316
  }
278
317
  }
@@ -0,0 +1,525 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Contract: schema apply
5
+ *
6
+ * Apply drift dari SDF ke database via ALTER TABLE incremental.
7
+ * Komplemen schema diff (detect only) dan schema migrate (full create/drop).
8
+ *
9
+ * Default: additive only (ADD COLUMN/INDEX/UNIQUE). Drift onlyInDb atau
10
+ * mismatched di-skip dengan warning kecuali opt-in flag aktif.
11
+ *
12
+ * Exit code:
13
+ * 0 - semua drift applicable ter-apply (atau no drift)
14
+ * 1 - ada drift yang di-skip karena butuh opt-in (--allow-drop / --allow-modify)
15
+ * 2 - error
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const os = require('os');
20
+ const path = require('path');
21
+
22
+ const { loadSchemaPath } = require('../../lib/dbschema-kit/loader');
23
+ const { mapTableMetaToIR } = require('../../lib/dbschema-kit/introspect-mapper');
24
+ const { loadConfig } = require('../../lib/dbschema-kit/connection');
25
+ const { diffModels } = require('../../lib/dbschema-kit/diff-engine');
26
+ const { generateAlterStatements } = require('../../lib/dbschema-kit/apply-engine');
27
+ const { resolveConfig, printDefaultConfigWarning } = require('../../lib/utils/config-resolver');
28
+
29
+ function loadIntrospector() {
30
+ const stubPath = process.env.DBSCHEMA_KIT_TEST_INTROSPECT_STUB;
31
+ if (stubPath) {
32
+ return require(path.resolve(stubPath));
33
+ }
34
+ return defaultIntrospector;
35
+ }
36
+
37
+ function loadApplyExecutor() {
38
+ const stubPath = process.env.DBSCHEMA_KIT_TEST_APPLY_STUB;
39
+ if (stubPath) {
40
+ return require(path.resolve(stubPath));
41
+ }
42
+ return require('../../lib/dbschema-kit/apply-executor');
43
+ }
44
+
45
+ function mapDialectToDbType(dialect) {
46
+ switch (dialect) {
47
+ case 'postgres': return 'postgresql';
48
+ case 'mysql': return 'mysql';
49
+ case 'oracle': return 'oracle';
50
+ case 'sqlite': return 'sqlite';
51
+ default: return dialect;
52
+ }
53
+ }
54
+
55
+ function buildEphemeralEnvFile(config, dbType) {
56
+ const lines = [`DB_TYPE=${dbType}`];
57
+ if (config.host !== undefined) lines.push(`DB_HOST=${config.host}`);
58
+ if (config.port !== undefined) lines.push(`DB_PORT=${config.port}`);
59
+ if (dbType === 'oracle') {
60
+ lines.push(`DB_NAME=${config.serviceName || ''}`);
61
+ } else if (config.database !== undefined) {
62
+ lines.push(`DB_NAME=${config.database}`);
63
+ }
64
+ if (config.user !== undefined) lines.push(`DB_USER=${config.user}`);
65
+ if (config.password !== undefined) lines.push(`DB_PASSWORD=${config.password}`);
66
+ return lines.join('\n') + '\n';
67
+ }
68
+
69
+ function createTmpEnvFile(content) {
70
+ const tmpDir = os.tmpdir();
71
+ const tmpName = `restforge-apply-${process.pid}-${Date.now()}.env`;
72
+ const fullPath = path.join(tmpDir, tmpName);
73
+ fs.writeFileSync(fullPath, content, 'utf8');
74
+ return fullPath;
75
+ }
76
+
77
+ async function collectTableMeta(introspector, target) {
78
+ const ref = target.schemaName ? `${target.schemaName}.${target.tableName}` : target.tableName;
79
+ const detailedColumns = await introspector.getDetailedColumnInfo(ref);
80
+ const constraints = await introspector.getConstraints(ref);
81
+ const foreignKeys = await introspector.getForeignKeys(ref);
82
+ const indexes = await introspector.getIndexes(ref);
83
+
84
+ const pkConstraint = constraints.find((c) => c.type === 'PRIMARY KEY');
85
+ const uniqueConstraints = constraints
86
+ .filter((c) => c.type === 'UNIQUE')
87
+ .map((c) => ({ name: c.name, columns: c.columns }));
88
+
89
+ return {
90
+ tableName: target.tableName,
91
+ schemaName: target.schemaName || null,
92
+ columns: detailedColumns,
93
+ primaryKey: pkConstraint ? pkConstraint.columns : [],
94
+ uniques: uniqueConstraints,
95
+ foreignKeys,
96
+ indexes,
97
+ checks: []
98
+ };
99
+ }
100
+
101
+ const defaultIntrospector = {
102
+ introspect: async ({ config, tables }) => {
103
+ const dbType = mapDialectToDbType(config.dialect);
104
+ if (dbType === 'sqlite') {
105
+ throw new Error('schema:apply does not yet support sqlite. Use postgres, mysql, or oracle.');
106
+ }
107
+
108
+ const { DatabaseIntrospector } = require('../../lib/utils/database-introspector');
109
+ const introspector = new DatabaseIntrospector({ quiet: true });
110
+
111
+ const ephemeralEnv = buildEphemeralEnvFile(config, dbType);
112
+ const tmpPath = createTmpEnvFile(ephemeralEnv);
113
+
114
+ try {
115
+ const ok = introspector.loadConfig(tmpPath);
116
+ if (!ok) throw new Error('Failed to load config into DatabaseIntrospector');
117
+ const connected = await introspector.connect();
118
+ if (!connected) throw new Error('Failed to connect to database');
119
+
120
+ const out = [];
121
+ for (const target of tables) {
122
+ try {
123
+ out.push(await collectTableMeta(introspector, target));
124
+ } catch (err) {
125
+ out.push({
126
+ tableName: target.tableName,
127
+ schemaName: target.schemaName || null,
128
+ columns: [],
129
+ primaryKey: [],
130
+ uniques: [],
131
+ foreignKeys: [],
132
+ indexes: [],
133
+ checks: [],
134
+ _missing: true,
135
+ _error: err && err.message ? err.message : String(err)
136
+ });
137
+ }
138
+ }
139
+ return { tables: out };
140
+ } finally {
141
+ try { await introspector.disconnect(); } catch (e) { /* ignore */ }
142
+ try { fs.unlinkSync(tmpPath); } catch (e) { /* ignore */ }
143
+ }
144
+ }
145
+ };
146
+
147
+ function buildConnectionSummary(config) {
148
+ if (config.dialect === 'sqlite') {
149
+ return `sqlite @ ${config.file}`;
150
+ }
151
+ if (config.dialect === 'oracle') {
152
+ return `oracle @ ${config.host}:${config.port}/${config.serviceName}`;
153
+ }
154
+ return `${config.dialect} @ ${config.host}:${config.port}/${config.database}`;
155
+ }
156
+
157
+ function formatStatementForDisplay(stmt, dialect) {
158
+ if (dialect === 'oracle' && /^\s*BEGIN\b/i.test(stmt)) {
159
+ return stmt + '\n/';
160
+ }
161
+ return stmt + ';';
162
+ }
163
+
164
+ function printDryRun(result, dialect, outStream) {
165
+ const out = outStream || process.stdout;
166
+ out.write('-- DDL Preview (schema apply, dry-run) --\n');
167
+ out.write('\n');
168
+
169
+ const tables = Object.keys(result.statementsByTable);
170
+ for (const table of tables) {
171
+ out.write(`[${table}]\n`);
172
+ for (const stmt of result.statementsByTable[table]) {
173
+ out.write(formatStatementForDisplay(stmt, dialect));
174
+ out.write('\n');
175
+ }
176
+ out.write('\n');
177
+ }
178
+
179
+ if (tables.length === 0) {
180
+ out.write('No applicable ALTER statements.\n');
181
+ out.write('\n');
182
+ }
183
+
184
+ if (result.skipped.length > 0) {
185
+ out.write('Warnings:\n');
186
+ for (const sk of result.skipped) {
187
+ out.write(` ${sk.description} (${sk.reason})\n`);
188
+ }
189
+ out.write('\n');
190
+ }
191
+
192
+ out.write('Summary:\n');
193
+ out.write(` Tables affected: ${result.summary.tablesAffected}\n`);
194
+ out.write(` Additions: ${result.summary.totalAdditions}\n`);
195
+ out.write(` Modifications: ${result.summary.totalModifications}\n`);
196
+ out.write(` Deletions: ${result.summary.totalDeletions}\n`);
197
+ out.write(` Skipped (opt-in): ${result.summary.totalSkipped}\n`);
198
+ out.write('\n');
199
+ out.write('Dry-run complete. No changes applied.\n');
200
+ }
201
+
202
+ function buildExitCode(result) {
203
+ const hasOptInSkip = result.skipped.some((s) =>
204
+ s.reason === 'requires --allow-drop' || s.reason === 'requires --allow-modify'
205
+ );
206
+ return hasOptInSkip ? 1 : 0;
207
+ }
208
+
209
+ module.exports = {
210
+ resource: 'schema',
211
+ verb: 'apply',
212
+ description: 'Apply drift dari SDF ke database via ALTER TABLE incremental (komplemen schema diff)',
213
+ category: 'management',
214
+ flags: {
215
+ path: {
216
+ type: 'string',
217
+ required: true,
218
+ description: 'Path file atau folder schema (mis. ./schema atau ./schema/users.js)'
219
+ },
220
+ config: {
221
+ type: 'string',
222
+ required: true,
223
+ description: 'File config database (.env)'
224
+ },
225
+ table: {
226
+ type: 'string',
227
+ required: false,
228
+ default: null,
229
+ description: 'Apply hanya satu tabel spesifik (default: semua model di SDF)'
230
+ },
231
+ 'dry-run': {
232
+ type: 'boolean',
233
+ required: false,
234
+ default: false,
235
+ description: 'Preview ALTER tanpa apply ke database'
236
+ },
237
+ 'allow-drop': {
238
+ type: 'boolean',
239
+ required: false,
240
+ default: false,
241
+ description: 'Opt-in: izinkan DROP COLUMN/INDEX/UNIQUE constraint (destruktif data)'
242
+ },
243
+ 'allow-modify': {
244
+ type: 'boolean',
245
+ required: false,
246
+ default: false,
247
+ description: 'Opt-in: izinkan ALTER COLUMN length/nullable (potential data loss)'
248
+ }
249
+ },
250
+ examples: [
251
+ 'npx restforge schema apply --path=./schema --config=db.env --dry-run',
252
+ 'npx restforge schema apply --path=./schema --config=db.env',
253
+ 'npx restforge schema apply --path=./schema --config=db.env --allow-drop',
254
+ 'npx restforge schema apply --path=./schema/visitors.js --config=db.env --table=visitors --dry-run'
255
+ ],
256
+ async handler(args) {
257
+ // 1. Resolve config
258
+ const resolvedConfigResult = resolveConfig(args.config, process.cwd());
259
+ if (!resolvedConfigResult) {
260
+ console.error('Error: --config=<file> is required.');
261
+ console.error("Tip: set a default with 'npx restforge config set-default --config=<file>' to omit --config in future runs");
262
+ const err = new Error('--config=<file> is required');
263
+ err.exitCode = 2;
264
+ throw err;
265
+ }
266
+ if (resolvedConfigResult.source === 'default') {
267
+ printDefaultConfigWarning(resolvedConfigResult.defaultName);
268
+ }
269
+
270
+ let config;
271
+ try {
272
+ config = loadConfig(resolvedConfigResult.path);
273
+ } catch (err) {
274
+ console.error(`Error: ${err.message}`);
275
+ const e = new Error(err.message);
276
+ e.exitCode = 2;
277
+ throw e;
278
+ }
279
+
280
+ // 2. Load SDF
281
+ const schemaPath = args.path;
282
+ const absPath = path.resolve(process.cwd(), schemaPath);
283
+
284
+ let sdfModels;
285
+ try {
286
+ sdfModels = loadSchemaPath(absPath);
287
+ } catch (err) {
288
+ console.error(`Error: ${err.message}`);
289
+ const e = new Error(err.message);
290
+ e.exitCode = 2;
291
+ throw e;
292
+ }
293
+
294
+ if (sdfModels.size === 0) {
295
+ console.error(`Error: No schema models found at '${schemaPath}'.`);
296
+ const e = new Error(`No schema models found at '${schemaPath}'`);
297
+ e.exitCode = 2;
298
+ throw e;
299
+ }
300
+
301
+ // 3. Filter target tables
302
+ let targetEntries = Array.from(sdfModels.entries());
303
+ if (args.table) {
304
+ targetEntries = targetEntries.filter(([qualified, ir]) => {
305
+ return ir.tableName === args.table || qualified === args.table;
306
+ });
307
+ if (targetEntries.length === 0) {
308
+ console.error(`Error: Table '${args.table}' not found in SDF.`);
309
+ const e = new Error(`Table '${args.table}' not found in SDF`);
310
+ e.exitCode = 2;
311
+ throw e;
312
+ }
313
+ }
314
+
315
+ const targetsForDb = targetEntries.map(([_, ir]) => ({
316
+ tableName: ir.tableName,
317
+ schemaName: ir.schemaName || null
318
+ }));
319
+
320
+ // 4. Introspect database
321
+ let introspector;
322
+ try {
323
+ introspector = loadIntrospector();
324
+ } catch (err) {
325
+ console.error(`Error: Failed to load introspector: ${err.message}`);
326
+ const e = new Error(err.message);
327
+ e.exitCode = 2;
328
+ throw e;
329
+ }
330
+
331
+ let introspectResult;
332
+ try {
333
+ introspectResult = await introspector.introspect({ config, tables: targetsForDb });
334
+ } catch (err) {
335
+ const message = err && err.message ? err.message : String(err);
336
+ console.error(`Error: ${message}`);
337
+ const e = new Error(message);
338
+ e.exitCode = 2;
339
+ throw e;
340
+ }
341
+
342
+ if (!introspectResult || !Array.isArray(introspectResult.tables)) {
343
+ console.error('Error: Introspector did not return a tables array.');
344
+ const e = new Error('Introspector did not return a tables array');
345
+ e.exitCode = 2;
346
+ throw e;
347
+ }
348
+
349
+ // 5. Map DB meta to IR
350
+ const dialect = config.dialect;
351
+ const dbModels = new Map();
352
+ for (const tableMeta of introspectResult.tables) {
353
+ if (tableMeta._missing) {
354
+ const qualified = tableMeta.schemaName
355
+ ? `${tableMeta.schemaName}.${tableMeta.tableName}`
356
+ : tableMeta.tableName;
357
+ dbModels.set(qualified, {
358
+ tableName: tableMeta.tableName,
359
+ schemaName: tableMeta.schemaName || null,
360
+ qualifiedName: qualified,
361
+ fields: {},
362
+ primaryKey: [],
363
+ indexes: [],
364
+ uniques: [],
365
+ relations: {},
366
+ checks: []
367
+ });
368
+ continue;
369
+ }
370
+ const ir = mapTableMetaToIR(tableMeta, dialect);
371
+ dbModels.set(ir.qualifiedName || ir.tableName, ir);
372
+ }
373
+
374
+ // 6. Build SDF map matching key convention
375
+ const sdfMap = new Map();
376
+ for (const [_, ir] of targetEntries) {
377
+ sdfMap.set(ir.qualifiedName || ir.tableName, ir);
378
+ }
379
+
380
+ // 7. Compute deltas via diff-engine
381
+ const deltas = diffModels(sdfMap, dbModels);
382
+
383
+ // 8. Generate ALTER statements
384
+ let alterResult;
385
+ try {
386
+ alterResult = generateAlterStatements(deltas, {
387
+ dialect,
388
+ allowDrop: args['allow-drop'] === true,
389
+ allowModify: args['allow-modify'] === true,
390
+ sdfModels: sdfMap
391
+ });
392
+ } catch (err) {
393
+ console.error(`Error: ${err.message}`);
394
+ const e = new Error(err.message);
395
+ e.exitCode = 2;
396
+ throw e;
397
+ }
398
+
399
+ // 9. Dry-run path
400
+ if (args['dry-run']) {
401
+ printDryRun(alterResult, dialect);
402
+ const exitCode = buildExitCode(alterResult);
403
+ process.stdout.write(`Exit code: ${exitCode}\n`);
404
+ if (exitCode !== 0) {
405
+ const err = new Error(`schema apply dry-run: ${alterResult.summary.totalSkipped} drift entries require opt-in`);
406
+ err.exitCode = exitCode;
407
+ throw err;
408
+ }
409
+ return;
410
+ }
411
+
412
+ // 10. No-op path
413
+ if (alterResult.statements.length === 0) {
414
+ const exitCode = buildExitCode(alterResult);
415
+ if (alterResult.skipped.length === 0) {
416
+ console.log('No drift to apply.');
417
+ } else {
418
+ console.log('No applicable ALTER statements.');
419
+ console.log('Warnings:');
420
+ for (const sk of alterResult.skipped) {
421
+ console.log(` ${sk.description} (${sk.reason})`);
422
+ }
423
+ }
424
+ if (exitCode !== 0) {
425
+ const err = new Error(`schema apply: ${alterResult.summary.totalSkipped} drift entries require opt-in`);
426
+ err.exitCode = exitCode;
427
+ throw err;
428
+ }
429
+ return;
430
+ }
431
+
432
+ // 11. Apply path
433
+ console.log(`Connecting to database (${buildConnectionSummary(config)})...`);
434
+ console.log(`Applying ${alterResult.statements.length} ALTER statement(s)...`);
435
+ console.log('');
436
+
437
+ const tables = Object.keys(alterResult.statementsByTable);
438
+ const tableHeaderPrinted = new Set();
439
+
440
+ function progressHandler({ index, total, statement, status, error }) {
441
+ if (status === 'running') return;
442
+ const entry = alterResult.statements[index - 1];
443
+ const tableLabel = entry ? entry.table : '?';
444
+ if (!tableHeaderPrinted.has(tableLabel)) {
445
+ console.log(`[${tableLabel}]`);
446
+ tableHeaderPrinted.add(tableLabel);
447
+ }
448
+ if (status === 'success') {
449
+ console.log(` ✓ ${statement};`);
450
+ return;
451
+ }
452
+ if (status === 'error') {
453
+ console.error(` ✗ ${statement};`);
454
+ const message = error && error.message ? error.message : String(error);
455
+ console.error(` Error: ${message}`);
456
+ }
457
+ }
458
+
459
+ let executor;
460
+ try {
461
+ executor = loadApplyExecutor();
462
+ } catch (err) {
463
+ console.error(`Error: Failed to load apply executor: ${err.message}`);
464
+ const e = new Error(err.message);
465
+ e.exitCode = 2;
466
+ throw e;
467
+ }
468
+
469
+ const sqlStatements = alterResult.statements.map((s) => s.sql);
470
+
471
+ let applyResult;
472
+ try {
473
+ applyResult = await executor.applyStatements({
474
+ statements: sqlStatements,
475
+ dialect,
476
+ config,
477
+ onProgress: progressHandler
478
+ });
479
+ } catch (err) {
480
+ if (err && err.status === 'ROLLBACK') {
481
+ console.error('');
482
+ console.error('ROLLBACK applied. Database state unchanged.');
483
+ console.error(` Statements applied before rollback: ${err.applied || 0}`);
484
+ console.error(` Failed statement index: ${err.failedIndex}`);
485
+ const e = new Error(err.message || 'ROLLBACK applied');
486
+ e.exitCode = 2;
487
+ throw e;
488
+ }
489
+ const message = err && err.message ? err.message : String(err);
490
+ console.error(`Error: ${message}`);
491
+ const e = new Error(message);
492
+ e.exitCode = 2;
493
+ throw e;
494
+ }
495
+
496
+ console.log('');
497
+ if (applyResult && applyResult.status === 'SUCCESS') {
498
+ console.log('Summary:');
499
+ console.log(` ${applyResult.applied} statement(s) applied successfully in ${applyResult.durationMs}ms.`);
500
+
501
+ for (const sk of alterResult.skipped) {
502
+ console.log(` ⚠ Skipped: ${sk.target} (${sk.reason})`);
503
+ }
504
+
505
+ const exitCode = buildExitCode(alterResult);
506
+ if (exitCode !== 0) {
507
+ const err = new Error(`schema apply: ${alterResult.summary.totalSkipped} drift entries require opt-in`);
508
+ err.exitCode = exitCode;
509
+ throw err;
510
+ }
511
+ return;
512
+ }
513
+
514
+ if (applyResult && applyResult.status === 'PARTIAL') {
515
+ console.error('Partial apply detected:');
516
+ console.error(` ${applyResult.applied || 0} applied`);
517
+ console.error(` ${applyResult.failed || 1} failed`);
518
+ const err = new Error('Partial apply detected. Manual cleanup may be required.');
519
+ err.exitCode = 2;
520
+ throw err;
521
+ }
522
+
523
+ console.log(`Status: ${applyResult ? applyResult.status : 'UNKNOWN'}`);
524
+ }
525
+ };