@restforgejs/platform 5.1.0 → 5.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) 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 +104 -0
  5. package/generators/cli/data/push.js +95 -0
  6. package/generators/cli/fast-track.js +12 -25
  7. package/generators/cli/init.js +347 -97
  8. package/generators/cli/schema/introspect.js +10 -10
  9. package/generators/lib/data/data-scope.js +138 -0
  10. package/generators/lib/data/db-executor.js +440 -0
  11. package/generators/lib/data/dialect-kit.js +56 -0
  12. package/generators/lib/data/envelope.js +236 -0
  13. package/generators/lib/data/pull-runner.js +414 -0
  14. package/generators/lib/data/push-runner.js +400 -0
  15. package/generators/lib/data/sdf-reader.js +132 -0
  16. package/generators/lib/data/table-order.js +126 -0
  17. package/generators/lib/data/value-codec.js +188 -0
  18. package/generators/lib/templates/dashboard-catalog.js +1 -1
  19. package/generators/lib/templates/db-connection-env.js +1 -1
  20. package/generators/lib/templates/dbschema-catalog.js +1 -1
  21. package/generators/lib/templates/field-validation-catalog.js +1 -1
  22. package/generators/lib/templates/mysql-template.js +1 -1
  23. package/generators/lib/templates/oracle-template.js +1 -1
  24. package/generators/lib/templates/postgres-template.js +1 -1
  25. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  26. package/generators/lib/templates/sqlite-template.js +1 -1
  27. package/integrity-manifest.json +18 -18
  28. package/package.json +1 -1
  29. package/scripts/verify-integrity.js +1 -1
  30. package/server.js +1 -1
  31. package/src/components/handlers/adjust_handler.js +1 -1
  32. package/src/components/handlers/audit_handler.js +1 -1
  33. package/src/components/handlers/delete_handler.js +1 -1
  34. package/src/components/handlers/export_handler.js +1 -1
  35. package/src/components/handlers/import_handler.js +1 -1
  36. package/src/components/handlers/insert_handler.js +1 -1
  37. package/src/components/handlers/update_handler.js +1 -1
  38. package/src/components/handlers/upload_handler.js +1 -1
  39. package/src/components/handlers/workflow_handler.js +1 -1
  40. package/src/components/integrations/webhook.js +1 -1
  41. package/src/consumers/baseConsumer.js +1 -1
  42. package/src/consumers/declarativeMapper.js +1 -1
  43. package/src/consumers/handlers/apiHandler.js +1 -1
  44. package/src/consumers/handlers/consoleHandler.js +1 -1
  45. package/src/consumers/handlers/databaseHandler.js +1 -1
  46. package/src/consumers/handlers/index.js +1 -1
  47. package/src/consumers/handlers/kafkaHandler.js +1 -1
  48. package/src/consumers/index.js +1 -1
  49. package/src/consumers/messageTransformer.js +1 -1
  50. package/src/consumers/validator.js +1 -1
  51. package/src/core/db/dialect/base-dialect.js +1 -1
  52. package/src/core/db/dialect/index.js +1 -1
  53. package/src/core/db/dialect/mysql-dialect.js +1 -1
  54. package/src/core/db/dialect/oracle-dialect.js +1 -1
  55. package/src/core/db/dialect/postgres-dialect.js +1 -1
  56. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  57. package/src/core/db/flatten-helper.js +1 -1
  58. package/src/core/db/query-builder-error.js +1 -1
  59. package/src/core/db/query-builder.js +1 -1
  60. package/src/core/db/relation-helper.js +1 -1
  61. package/src/core/handlers/delete_handler.js +1 -1
  62. package/src/core/handlers/insert_handler.js +1 -1
  63. package/src/core/handlers/update_handler.js +1 -1
  64. package/src/core/models/base-model.js +1 -1
  65. package/src/core/utils/cache-manager.js +1 -1
  66. package/src/core/utils/component-engine.js +1 -1
  67. package/src/core/utils/context-builder.js +1 -1
  68. package/src/core/utils/datetime-formatter.js +1 -1
  69. package/src/core/utils/datetime-parser.js +1 -1
  70. package/src/core/utils/db.js +1 -1
  71. package/src/core/utils/logger.js +1 -1
  72. package/src/core/utils/payload-loader.js +1 -1
  73. package/src/core/utils/security-checks.js +1 -1
  74. package/src/middleware/body-options.js +1 -1
  75. package/src/middleware/cors.js +1 -1
  76. package/src/middleware/idempotency.js +1 -1
  77. package/src/middleware/rate-limiter.js +1 -1
  78. package/src/middleware/request-logger.js +1 -1
  79. package/src/middleware/security-headers.js +1 -1
  80. package/src/models/base-model-mysql.js +1 -1
  81. package/src/models/base-model-oracle.js +1 -1
  82. package/src/models/base-model-sqlite.js +1 -1
  83. package/src/models/base-model.js +1 -1
  84. package/src/pro/caching/redis-client.js +1 -1
  85. package/src/pro/caching/redis-helper.js +1 -1
  86. package/src/pro/consumers/baseConsumer.js +1 -1
  87. package/src/pro/consumers/declarativeMapper.js +1 -1
  88. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  89. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  90. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  91. package/src/pro/consumers/handlers/index.js +1 -1
  92. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  93. package/src/pro/consumers/index.js +1 -1
  94. package/src/pro/consumers/messageTransformer.js +1 -1
  95. package/src/pro/consumers/validator.js +1 -1
  96. package/src/pro/database/base-model-mysql.js +1 -1
  97. package/src/pro/database/base-model-oracle.js +1 -1
  98. package/src/pro/database/base-model-sqlite.js +1 -1
  99. package/src/pro/database/db-mysql.js +1 -1
  100. package/src/pro/database/db-oracle.js +1 -1
  101. package/src/pro/database/db-sqlite.js +1 -1
  102. package/src/pro/excel/excel-generator.js +1 -1
  103. package/src/pro/excel/excel-parser.js +1 -1
  104. package/src/pro/excel/export-service.js +1 -1
  105. package/src/pro/excel/export_handler.js +1 -1
  106. package/src/pro/excel/import-service.js +1 -1
  107. package/src/pro/excel/import-validator.js +1 -1
  108. package/src/pro/excel/import_handler.js +1 -1
  109. package/src/pro/excel/upsert-builder.js +1 -1
  110. package/src/pro/idgen/idgen-routes.js +1 -1
  111. package/src/pro/integrations/lookup-resolver.js +1 -1
  112. package/src/pro/integrations/upload-handler-v2.js +1 -1
  113. package/src/pro/integrations/upload-handler.js +1 -1
  114. package/src/pro/integrations/webhook.js +1 -1
  115. package/src/pro/locking/lock-routes.js +1 -1
  116. package/src/pro/locking/resource-lock-manager.js +1 -1
  117. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  118. package/src/pro/messaging/kafkaService.js +1 -1
  119. package/src/pro/messaging/messagehubService.js +1 -1
  120. package/src/pro/messaging/rabbitmqService.js +1 -1
  121. package/src/pro/scheduler/job-manager.js +1 -1
  122. package/src/pro/scheduler/job-routes.js +1 -1
  123. package/src/pro/scheduler/job-validator.js +1 -1
  124. package/src/pro/storage/base-storage-provider.js +1 -1
  125. package/src/pro/storage/file-metadata-helper.js +1 -1
  126. package/src/pro/storage/index.js +1 -1
  127. package/src/pro/storage/local-storage-provider.js +1 -1
  128. package/src/pro/storage/s3-storage-provider.js +1 -1
  129. package/src/pro/storage/upload-cleanup-job.js +1 -1
  130. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  131. package/src/pro/storage/upload-pending-tracker.js +1 -1
  132. package/src/pro/websocket/broadcast-helper.js +1 -1
  133. package/src/pro/websocket/index.js +1 -1
  134. package/src/pro/websocket/livesync-server.js +1 -1
  135. package/src/pro/websocket/ws-broadcaster.js +1 -1
  136. package/src/services/export-service.js +1 -1
  137. package/src/services/import-service.js +1 -1
  138. package/src/services/kafkaConsumerService.js +1 -1
  139. package/src/services/kafkaService.js +1 -1
  140. package/src/services/messagehubService.js +1 -1
  141. package/src/services/rabbitmqService.js +1 -1
  142. package/src/utils/cache-invalidation-registry.js +1 -1
  143. package/src/utils/cache-manager.js +1 -1
  144. package/src/utils/component-engine.js +1 -1
  145. package/src/utils/config-extractor.js +1 -1
  146. package/src/utils/consumerLogger.js +1 -1
  147. package/src/utils/context-builder.js +1 -1
  148. package/src/utils/dashboard-helpers.js +1 -1
  149. package/src/utils/dateHelper.js +1 -1
  150. package/src/utils/datetime-formatter.js +1 -1
  151. package/src/utils/datetime-parser.js +1 -1
  152. package/src/utils/db-bootstrap.js +1 -1
  153. package/src/utils/db-mysql.js +1 -1
  154. package/src/utils/db-oracle.js +1 -1
  155. package/src/utils/db-sqlite.js +1 -1
  156. package/src/utils/db.js +1 -1
  157. package/src/utils/demo-generator.js +1 -1
  158. package/src/utils/excel-generator.js +1 -1
  159. package/src/utils/excel-parser.js +1 -1
  160. package/src/utils/file-watcher.js +1 -1
  161. package/src/utils/id-generator.js +1 -1
  162. package/src/utils/idempotency-manager.js +1 -1
  163. package/src/utils/import-validator.js +1 -1
  164. package/src/utils/license-client.js +1 -1
  165. package/src/utils/lock-manager.js +1 -1
  166. package/src/utils/logger.js +1 -1
  167. package/src/utils/lookup-resolver.js +1 -1
  168. package/src/utils/payload-loader.js +1 -1
  169. package/src/utils/processor-response.js +1 -1
  170. package/src/utils/rabbitmq.js +1 -1
  171. package/src/utils/redis-client.js +1 -1
  172. package/src/utils/redis-helper.js +1 -1
  173. package/src/utils/request-scope.js +1 -1
  174. package/src/utils/security-checks.js +1 -1
  175. package/src/utils/service-resolver.js +1 -1
  176. package/src/utils/shutdown-coordinator.js +1 -1
  177. package/src/utils/trusted-keys.js +1 -1
  178. package/src/utils/upload-handler.js +1 -1
  179. package/src/utils/upsert-builder.js +1 -1
  180. package/src/utils/workflow-hook-executor.js +1 -1
@@ -0,0 +1,400 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * push-runner — Business logic command `data push` (append-only, amandemen A6).
5
+ *
6
+ * Push bersifat append-only: tidak ada pilihan mode. Mode `upsert`/`replace`
7
+ * di-descope dari v1 (User konfirmasi 2026-06-03), sehingga flag `--mode`/`--force`
8
+ * dihapus dari contract dan runner tidak lagi membaca/memvalidasi keduanya.
9
+ *
10
+ * `run()` adalah dispatcher (single vs multi-schema); `runOne(ir, ctx)` mengeksekusi
11
+ * push satu tabel dan mengelola executor-nya sendiri (Phase 03, data-pull-push-v2).
12
+ *
13
+ * Alur dispatcher `run()`:
14
+ * 1. Resolve config DB tujuan (config-resolver) → loadConfig (dbschema-kit/connection); SEKALI.
15
+ * 2. parseSchemaFlag + validateScopeFlags (sebelum load SDF / koneksi DB): tepat satu
16
+ * dari --table / --schema / --all-schemas (selain itu → exit 2).
17
+ * 3. Load SDF SEKALI → Map<qualifiedName, IR> (sdf-reader).
18
+ * 4. resolveScope(models, args, schemaList) → daftar IR sesuai flag scope.
19
+ * 4a. Jalur --table: satu IR → runOne(ir, ctx) → ringkasan single (regresi nol).
20
+ * 4b. Jalur --schema/--all-schemas: topoSortByFk(SUBSET) → urutan parent→child (table-order);
21
+ * siklus → warning stderr + fallback urutan deklarasi. Loop FAIL-FAST (Q3); tabel tanpa
22
+ * file input di-SKIP (warning stderr, catat di skipped[], lanjut) → runOne per IR → agregat.
23
+ *
24
+ * Alur `runOne(ir, ctx)` (per tabel; metadata kolom/namespace/PK murni dari SDF IR):
25
+ * a. Derive nama file dari SDF (aturan IDENTIK pull, relDataFilePath): bersarang
26
+ * `<input>/<schema>/<table>.json` bila ber-schema, flat `<input>/<table>.json`.
27
+ * File tidak ada → exit 1. Baca + parseEnvelope + validateEnvelope (shape invalid → exit 1).
28
+ * b. Column match vs SDF (BUKAN introspeksi DB): tiap envelope.columns[].name harus ada
29
+ * di SDF dan tipe konsisten. Mismatch → exit 1. No-PK guard (exit 1).
30
+ * c. Connect (db-executor) + validasi identifier tabel/kolom (anti-injection).
31
+ * d. INSERT batch + commit per batch: tiap nilai di-decode (value-codec) per dialect tujuan,
32
+ * INSERT parameterized per baris dirakit via dialect helper (placeholder) dan dijalankan
33
+ * via executeTransaction (commit per batch). Append: konflik PK = error DB (exit 3).
34
+ * e. Disconnect (selalu, di finally).
35
+ *
36
+ * Exit code: 0 sukses; 1 operational (file tidak ada, envelope invalid, column mismatch
37
+ * vs SDF, no-PK anomali); 2 usage/precondition (--config wajib, mutual-exclusive, tabel
38
+ * tak terdaftar/ambigu di SDF); 3 database error (koneksi maupun INSERT, termasuk konflik
39
+ * PK pada append). Handler tidak memanggil process.exit() sendiri.
40
+ *
41
+ * @module generators/lib/data/push-runner
42
+ */
43
+
44
+ const fs = require('fs');
45
+ const path = require('path');
46
+
47
+ const { resolveConfig, printDefaultConfigWarning } = require('../utils/config-resolver');
48
+ const { loadConfig } = require('../dbschema-kit/connection');
49
+ const {
50
+ loadSdf,
51
+ irToColumns,
52
+ getNamespace,
53
+ getPrimaryKey
54
+ } = require('./sdf-reader');
55
+ const { parseSchemaFlag, validateScopeFlags, resolveScope, relDataFilePath } = require('./data-scope');
56
+ const { createDialect, validateIdentifier, placeholder } = require('./dialect-kit');
57
+ const { createExecutor } = require('./db-executor');
58
+ const { decodeValue } = require('./value-codec');
59
+ const { parseEnvelope, validateEnvelope } = require('./envelope');
60
+ const { topoSortByFk } = require('./table-order');
61
+
62
+ const DEFAULT_BATCH_SIZE = 1000;
63
+
64
+ /**
65
+ * Buat Error dengan exitCode terlampir (override default dispatcher).
66
+ * @param {string} message
67
+ * @param {number} exitCode
68
+ * @returns {Error}
69
+ */
70
+ function failWith(message, exitCode) {
71
+ const err = new Error(message);
72
+ err.exitCode = exitCode;
73
+ return err;
74
+ }
75
+
76
+ /**
77
+ * Petakan `config.dialect` internal ke bentuk DB_TYPE kanonik untuk output.
78
+ *
79
+ * `loadConfig` memetakan `DB_TYPE=postgresql` menjadi `dialect='postgres'`; output
80
+ * push (`target_dialect`) memakai bentuk kanonik `postgresql`, konsisten dengan
81
+ * `source_dialect` pull (pull-runner.canonicalSourceDialect). Dialect lain identik.
82
+ *
83
+ * @param {string} dialect - config.dialect ('postgres'|'mysql'|'oracle'|'sqlite')
84
+ * @returns {string} 'postgresql'|'mysql'|'oracle'|'sqlite'
85
+ */
86
+ function canonicalTargetDialect(dialect) {
87
+ return dialect === 'postgres' ? 'postgresql' : dialect;
88
+ }
89
+
90
+ /**
91
+ * Normalisasi nilai integer non-negatif dari flag.
92
+ * @param {*} value
93
+ * @param {number|null} fallback
94
+ * @returns {number|null}
95
+ */
96
+ function normalizeInt(value, fallback) {
97
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
98
+ return Math.trunc(value);
99
+ }
100
+ return fallback;
101
+ }
102
+
103
+ /**
104
+ * Validasi tiap kolom envelope terhadap metadata SDF (tanpa menyentuh DB).
105
+ *
106
+ * Setiap `envelope.columns[].name` wajib terdeklarasi di SDF dengan tipe canonical
107
+ * yang konsisten. Kolom SDF yang tidak muncul di envelope diperbolehkan (akan
108
+ * memakai default/NULL DB pada INSERT). Mismatch (kolom tak dikenal di SDF, atau
109
+ * tipe berbeda) → throw exitCode=1.
110
+ *
111
+ * @param {Array<{name: string, type: string}>} envelopeColumns
112
+ * @param {Array<{name: string, type: string}>} sdfColumns
113
+ * @param {string} tableName - untuk pesan error
114
+ * @returns {Array<{name: string, type: string}>} kolom yang akan di-INSERT (urutan envelope)
115
+ * @throws {Error} exitCode=1 bila ada kolom tak dikenal atau tipe berbeda
116
+ */
117
+ function matchColumns(envelopeColumns, sdfColumns, tableName) {
118
+ const sdfByName = new Map(sdfColumns.map((c) => [c.name, c]));
119
+ const insertColumns = [];
120
+ for (const ec of envelopeColumns) {
121
+ const sc = sdfByName.get(ec.name);
122
+ if (!sc) {
123
+ throw failWith(
124
+ `Column '${ec.name}' in envelope is not declared in SDF for table '${tableName}'.`,
125
+ 1
126
+ );
127
+ }
128
+ if (sc.type !== ec.type) {
129
+ throw failWith(
130
+ `Column '${ec.name}' type mismatch for table '${tableName}': envelope '${ec.type}' vs SDF '${sc.type}'.`,
131
+ 1
132
+ );
133
+ }
134
+ insertColumns.push({ name: ec.name, type: ec.type });
135
+ }
136
+ return insertColumns;
137
+ }
138
+
139
+ /**
140
+ * Push satu tabel SDF dari file envelope JSON ke DB. Path input via relDataFilePath
141
+ * (bersarang `<input>/<schema>/<table>.json` bila ber-schema, flat
142
+ * `<input>/<table>.json` bila tanpa schema).
143
+ *
144
+ * Mengelola executor-nya sendiri (buat → disconnect di finally) agar isolasi
145
+ * per-tabel jelas dan jalur multi-schema dapat memanggilnya berulang tanpa kebocoran
146
+ * koneksi. Metadata kolom, namespace, dan PK murni dari IR.
147
+ *
148
+ * @param {Object} ir - IR model SDF satu tabel
149
+ * @param {Object} ctx - konteks resolusi sekali-pakai
150
+ * @param {Object} ctx.config - config DB hasil loadConfig
151
+ * @param {string} ctx.targetDialect - dialect kanonik (untuk output, dirakit caller)
152
+ * @param {Object} ctx.args - argumen ter-parse (storage-path, batch-size)
153
+ * @param {string} ctx.cwd - working directory
154
+ * @returns {Promise<{schema: (string|null), table: string, rows: number, committed_batches: number}>}
155
+ */
156
+ async function runOne(ir, ctx) {
157
+ const { config, args, cwd } = ctx;
158
+
159
+ const sdfColumns = irToColumns(ir);
160
+ const { schemaName, qualifiedName } = getNamespace(ir);
161
+ const rawPk = getPrimaryKey(ir);
162
+ const pk = Array.isArray(rawPk) ? rawPk : [];
163
+
164
+ // ── a. Derive nama file dari SDF (aturan IDENTIK pull, relDataFilePath) ────────
165
+ // Path = {input}/relDataFilePath(ir), turunan MURNI dari SDF (bukan --table mentah
166
+ // maupun envelope.schema). Tabel ber-schema → bersarang `<schema>/<table>.json`;
167
+ // tanpa schema → flat `<table>.json`. Single-table: file-not-found = exit 1 (tak
168
+ // berubah). Jalur --schema/--all-schemas menyaring file-missing SEBELUM memanggil
169
+ // runOne (skip-missing), jadi guard ini hanya tercapai pada single.
170
+ const inputDir = path.resolve(cwd, args['storage-path']);
171
+ const inFile = path.join(inputDir, relDataFilePath(ir));
172
+ if (!fs.existsSync(inFile)) {
173
+ const rel = path.relative(cwd, inFile).split(path.sep).join('/');
174
+ throw failWith(`Input file not found: ${rel}`, 1);
175
+ }
176
+
177
+ let envelope;
178
+ try {
179
+ const raw = fs.readFileSync(inFile, 'utf8');
180
+ envelope = validateEnvelope(parseEnvelope(raw));
181
+ } catch (e) {
182
+ // parseEnvelope/validateEnvelope sudah ber-exitCode=1; baca file gagal lain → 1.
183
+ if (Number.isInteger(e.exitCode)) throw e;
184
+ throw failWith(`Failed to read envelope '${path.relative(cwd, inFile).split(path.sep).join('/')}': ${e.message}`, 1);
185
+ }
186
+
187
+ // Cross-check OPSIONAL: path diturunkan dari SDF, bukan envelope. Bila envelope 1.1
188
+ // membawa `schema` yang berbeda dari schema SDF (mis. file dipindah/diedit manual),
189
+ // cetak warning informatif (bukan error) — konsisten perlakuan envelope.table.
190
+ // File 1.0 (tanpa `schema`, undefined) dilewati.
191
+ if (envelope.schema !== undefined && envelope.schema !== schemaName) {
192
+ process.stderr.write(
193
+ `Warning: envelope schema '${envelope.schema}' differs from SDF schema ` +
194
+ `'${schemaName === null ? '(none)' : schemaName}' for table '${ir.tableName}'\n`
195
+ );
196
+ }
197
+
198
+ // ── b. Column match vs SDF (tanpa introspeksi DB) + no-PK guard ──────────────
199
+ const insertColumns = matchColumns(envelope.columns, sdfColumns, ir.tableName);
200
+
201
+ // No-PK = anomali: schema-validator MEWAJIBKAN primary key, jadi IR yang lolos load
202
+ // SDF pasti punya PK. Bila tetap kosong (mis. IR diinjeksi), gagal jelas lebih aman.
203
+ // Selaras pull-runner (phase-02b Keputusan K3).
204
+ if (pk.length === 0) {
205
+ throw failWith(`Table '${ir.tableName}' has no primary key in SDF (unexpected)`, 1);
206
+ }
207
+
208
+ // ── c. Validasi identifier (anti-injection) + siapkan executor ───────────────
209
+ const dialect = createDialect(config.dialect);
210
+ validateIdentifier(dialect, qualifiedName);
211
+ for (const col of insertColumns) validateIdentifier(dialect, col.name);
212
+
213
+ const batchSize = normalizeInt(args['batch-size'], DEFAULT_BATCH_SIZE) || DEFAULT_BATCH_SIZE;
214
+ const executor = createExecutor(config);
215
+
216
+ // ── d. INSERT batch + commit per batch ───────────────────────────────────────
217
+ // INSERT parameterized per-baris (dirakit via dialect.placeholder), dijalankan
218
+ // per batch dalam SATU executeTransaction → commit per batch. Pilihan per-baris
219
+ // (bukan multi-row VALUES) menjaga paritas lintas-dialect: Oracle tidak mendukung
220
+ // `VALUES (..),(..)` multi-row. Kegagalan satu batch me-rollback batch tsb.;
221
+ // batch sebelumnya tetap ter-commit (risiko partial, didokumentasikan Poin 5/8).
222
+ const colList = insertColumns.map((c) => c.name).join(', ');
223
+ const valuesClause = insertColumns.map((_, i) => placeholder(dialect, i + 1)).join(', ');
224
+ const insertSql = `INSERT INTO ${qualifiedName} (${colList}) VALUES (${valuesClause})`;
225
+
226
+ let rowCount = 0;
227
+ let committedBatches = 0;
228
+ try {
229
+ const rows = envelope.rows;
230
+ for (let start = 0; start < rows.length; start += batchSize) {
231
+ const slice = rows.slice(start, start + batchSize);
232
+ const queries = slice.map((row) => ({
233
+ sql: insertSql,
234
+ params: insertColumns.map((c) => decodeValue(row[c.name], c.type, config.dialect))
235
+ }));
236
+ await executor.executeTransaction(queries);
237
+ committedBatches += 1;
238
+ rowCount += slice.length;
239
+ }
240
+ } catch (e) {
241
+ // exitCode yang sudah di-set dipertahankan; selain itu error lapisan DB
242
+ // (connect/INSERT, termasuk konflik PK pada append) dipetakan ke exit 3.
243
+ if (Number.isInteger(e.exitCode)) throw e;
244
+ throw failWith(`Database error during push: ${e.message}`, 3);
245
+ } finally {
246
+ try { await executor.disconnect(); } catch (_e) { /* ignore */ }
247
+ }
248
+
249
+ // ── e. Ringkasan per-tabel (tanpa command/target_dialect; dirakit caller) ─────
250
+ return {
251
+ schema: schemaName,
252
+ table: ir.tableName,
253
+ rows: rowCount,
254
+ committed_batches: committedBatches
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Jalankan command `data push` (dispatcher: single-table vs multi-schema, append-only).
260
+ * @param {Object} args - argumen ter-parse dari contract
261
+ * @returns {Promise<Object>} ringkasan single { command, schema, table, rows, target_dialect, committed_batches }
262
+ * atau agregat { command, scope, table_count, total_rows, tables: [...], skipped: [...] }
263
+ */
264
+ async function run(args) {
265
+ const cwd = process.cwd();
266
+
267
+ // ── 1. Resolve config DB tujuan (SEKALI) ─────────────────────────────────────
268
+ const resolved = resolveConfig(args.config, cwd);
269
+ if (!resolved) {
270
+ process.stderr.write(
271
+ "Tip: set a default with 'npx restforge config set-default --config=<file>' to omit --config in future runs\n"
272
+ );
273
+ throw failWith('--config=<file> is required.', 2);
274
+ }
275
+ if (resolved.source === 'default') {
276
+ printDefaultConfigWarning(resolved.defaultName);
277
+ }
278
+
279
+ let config;
280
+ try {
281
+ config = loadConfig(resolved.path);
282
+ } catch (e) {
283
+ throw failWith(e.message, 2);
284
+ }
285
+
286
+ // ── 2. Validasi scope flags (SEBELUM SDF/DB) ─────────────────────────────────
287
+ // Tepat satu dari --table / --schema / --all-schemas (selain itu → exit 2).
288
+ // Helper bersama dengan pull (data-scope) agar perilaku identik.
289
+ const schemaList = parseSchemaFlag(args.schema);
290
+ validateScopeFlags(args, schemaList);
291
+ const hasTable = args.table != null && args.table !== '';
292
+
293
+ // ── 3. Load SDF (SEKALI) ─────────────────────────────────────────────────────
294
+ const schemaPath = args['schema-path'];
295
+ const absSchemaPath = path.resolve(cwd, schemaPath);
296
+ let models;
297
+ try {
298
+ models = loadSdf(absSchemaPath);
299
+ } catch (e) {
300
+ // Error baca SDF (path tidak ada, parse gagal) = operational.
301
+ throw failWith(e.message, 1);
302
+ }
303
+
304
+ const ctx = {
305
+ config,
306
+ targetDialect: canonicalTargetDialect(config.dialect),
307
+ args,
308
+ cwd
309
+ };
310
+
311
+ // ── 4. Resolusi scope → daftar IR ────────────────────────────────────────────
312
+ // --table → [ir] (exit 2 bila tak terdaftar/ambigu); --schema → subset (exit 2
313
+ // bila 0-match); --all-schemas → seluruh IR (boleh kosong → agregat 0 tabel).
314
+ const subset = resolveScope(models, args, schemaList);
315
+
316
+ // ── 4a. Jalur --table (single; regresi nol) ──────────────────────────────────
317
+ if (hasTable) {
318
+ const per = await runOne(subset[0], ctx);
319
+ const summary = {
320
+ command: 'push',
321
+ schema: per.schema,
322
+ table: per.table,
323
+ rows: per.rows,
324
+ target_dialect: ctx.targetDialect,
325
+ committed_batches: per.committed_batches
326
+ };
327
+
328
+ if (args.json === true) {
329
+ process.stdout.write(JSON.stringify(summary) + '\n');
330
+ } else {
331
+ process.stdout.write(
332
+ `Pushed ${per.rows} rows to ${per.table} (dialect: ${ctx.targetDialect}, batches: ${per.committed_batches})\n`
333
+ );
334
+ }
335
+ return summary;
336
+ }
337
+
338
+ // ── 4b. Jalur --schema/--all-schemas (topo-sort FK parent→child, fail-fast Q3, skip-missing) ──
339
+ // Urutan push diturunkan dari FK SDF SUBSET (bukan seluruh models): parent (tabel
340
+ // direferensikan) SEBELUM child agar INSERT child tidak melanggar FK constraint.
341
+ // Siklus → fallback urutan deklarasi. Catatan (Phase 03): untuk --schema subset, FK
342
+ // ke parent di luar subset → edge diabaikan table-order; child bisa di-push tanpa
343
+ // parent → potensi FK error runtime (exit 3), tanpa pre-warning.
344
+ const scope = schemaList ? 'schema' : 'all-schemas';
345
+ const subsetMap = new Map(subset.map((ir) => [getNamespace(ir).qualifiedName, ir]));
346
+ const { ordered, cycle } = topoSortByFk(subsetMap);
347
+ if (cycle) {
348
+ process.stderr.write('FK cycle detected; falling back to declaration order\n');
349
+ }
350
+
351
+ // Skip-missing HANYA di jalur multi: tabel tanpa file input dilewati (warning +
352
+ // catat), bukan error. Path input via relDataFilePath (aturan IDENTIK pull).
353
+ const inputDir = path.resolve(cwd, args['storage-path']);
354
+ const tables = [];
355
+ const skipped = [];
356
+ let totalRows = 0;
357
+
358
+ for (const ir of ordered) {
359
+ const inFile = path.join(inputDir, relDataFilePath(ir));
360
+ if (!fs.existsSync(inFile)) {
361
+ process.stderr.write(`Skipped ${ir.tableName}: input file not found\n`);
362
+ skipped.push(ir.tableName);
363
+ continue;
364
+ }
365
+
366
+ const per = await runOne(ir, ctx); // fail-fast (Q3)
367
+ tables.push(per);
368
+ totalRows += per.rows;
369
+ if (args.json !== true) {
370
+ process.stdout.write(
371
+ `Pushed ${per.rows} rows to ${per.table} (dialect: ${ctx.targetDialect}, batches: ${per.committed_batches})\n`
372
+ );
373
+ }
374
+ }
375
+
376
+ const aggregate = {
377
+ command: 'push',
378
+ scope,
379
+ table_count: tables.length,
380
+ total_rows: totalRows,
381
+ tables,
382
+ skipped
383
+ };
384
+
385
+ if (args.json === true) {
386
+ process.stdout.write(JSON.stringify(aggregate) + '\n');
387
+ } else {
388
+ process.stdout.write(
389
+ `Done: ${tables.length} tables, ${totalRows} rows total (skipped: ${skipped.length})\n`
390
+ );
391
+ }
392
+
393
+ return aggregate;
394
+ }
395
+
396
+ module.exports = {
397
+ run,
398
+ // Diekspor untuk reuse/test internal.
399
+ _internal: { runOne, matchColumns, normalizeInt, canonicalTargetDialect }
400
+ };
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * sdf-reader — Sumber metadata tabel untuk `data pull` / `data push`.
5
+ *
6
+ * Seluruh metadata (daftar kolom, tipe canonical, nullable, primary key composite)
7
+ * dibaca murni dari SDF IR (Schema Definition File), TANPA introspeksi DB. Modul ini
8
+ * mereuse loader existing `dbschema-kit/loader` dan tidak membuka koneksi database.
9
+ *
10
+ * Keputusan arah: amandemen A2 (SDF sebagai sumber kebenaran tabel) dan A3 (deteksi
11
+ * binary by-omission — SDF hanya bisa menyimpan 10 tipe canonical, jadi kolom binary
12
+ * mustahil muncul). Lihat docs/plan/data-pull-push/data-pull-push-09-amendments.md.
13
+ *
14
+ * @module generators/lib/data/sdf-reader
15
+ */
16
+
17
+ const { loadSchemaPath } = require('../dbschema-kit/loader');
18
+
19
+ /**
20
+ * Muat SDF dari file atau folder menjadi Map<qualifiedName, IR>.
21
+ *
22
+ * Delegasi langsung ke loader existing — tidak ada introspeksi DB. `schemaPath`
23
+ * boleh menunjuk satu file `.js` atau folder berisi banyak file SDF.
24
+ *
25
+ * @param {string} schemaPath - Path ke file atau folder SDF
26
+ * @returns {Map<string, Object>} Map qualifiedName → IR model
27
+ */
28
+ function loadSdf(schemaPath) {
29
+ return loadSchemaPath(schemaPath);
30
+ }
31
+
32
+ /**
33
+ * Cari satu IR dari Map hasil loadSdf berdasarkan argumen tabel.
34
+ *
35
+ * Urutan pencocokan:
36
+ * 1. Match eksak via qualifiedName (key Map) → selalu satu entry, tidak ambigu.
37
+ * 2. Match via `ir.tableName` (arg tanpa schema). Bila cocok dengan LEBIH DARI SATU
38
+ * entry (nama tabel sama di schema berbeda), arg dianggap ambigu → throw exitCode=2
39
+ * yang meminta bentuk qualified `schema.table`.
40
+ *
41
+ * Mereuse pola table-guard di cli/schema/apply.js:303-314. Bila tidak ditemukan,
42
+ * throw Error dengan `err.exitCode = 2` (precondition) — penolakan terjadi SEBELUM
43
+ * koneksi DB dibuka oleh caller.
44
+ *
45
+ * @param {Map<string, Object>} models - Hasil loadSdf
46
+ * @param {string} tableArg - Nilai flag --table
47
+ * @returns {Object} IR model yang cocok
48
+ * @throws {Error} exitCode=2 bila tabel tidak terdaftar ATAU ambigu lintas-schema
49
+ */
50
+ function findTableOrThrow(models, tableArg) {
51
+ // 1. Match eksak via qualifiedName (key Map) — kunci Map unik, selalu satu entry.
52
+ if (models.has(tableArg)) {
53
+ return models.get(tableArg);
54
+ }
55
+
56
+ // 2. Match via tableName tanpa schema — bisa lebih dari satu bila nama tabel sama
57
+ // di schema berbeda.
58
+ const byTableName = [];
59
+ for (const ir of models.values()) {
60
+ if (ir.tableName === tableArg) {
61
+ byTableName.push(ir);
62
+ }
63
+ }
64
+
65
+ if (byTableName.length === 1) {
66
+ return byTableName[0];
67
+ }
68
+ if (byTableName.length > 1) {
69
+ const err = new Error(
70
+ `Table '${tableArg}' is ambiguous across schemas; specify 'schema.table'.`
71
+ );
72
+ err.exitCode = 2;
73
+ throw err;
74
+ }
75
+
76
+ const err = new Error(`Table '${tableArg}' not found in SDF.`);
77
+ err.exitCode = 2;
78
+ throw err;
79
+ }
80
+
81
+ /**
82
+ * Transformasi IR menjadi array kolom envelope.
83
+ *
84
+ * Murni in-memory, tanpa I/O. Urutan kolom mengikuti urutan deklarasi field di SDF
85
+ * (`Object.keys(ir.fields)`).
86
+ *
87
+ * @param {Object} ir - IR model dari SDF
88
+ * @returns {Array<{name: string, type: string, nullable: boolean, primary_key: boolean}>}
89
+ */
90
+ function irToColumns(ir) {
91
+ const primaryKey = Array.isArray(ir.primaryKey) ? ir.primaryKey : [];
92
+ return Object.keys(ir.fields).map((name) => {
93
+ const field = ir.fields[name];
94
+ return {
95
+ name,
96
+ type: field.type,
97
+ nullable: field.notnull !== true,
98
+ primary_key: primaryKey.includes(name)
99
+ };
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Ambil namespace (schema DB) dari IR.
105
+ *
106
+ * @param {Object} ir - IR model dari SDF
107
+ * @returns {{schemaName: (string|null), qualifiedName: string}}
108
+ */
109
+ function getNamespace(ir) {
110
+ return {
111
+ schemaName: ir.schemaName || null,
112
+ qualifiedName: ir.qualifiedName || ir.tableName
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Ambil daftar primary key (mendukung composite).
118
+ *
119
+ * @param {Object} ir - IR model dari SDF
120
+ * @returns {string[]} Array nama kolom primary key
121
+ */
122
+ function getPrimaryKey(ir) {
123
+ return ir.primaryKey;
124
+ }
125
+
126
+ module.exports = {
127
+ loadSdf,
128
+ findTableOrThrow,
129
+ irToColumns,
130
+ getNamespace,
131
+ getPrimaryKey
132
+ };
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * table-order — Topological sort tabel SDF berdasarkan foreign key (parent→child).
5
+ *
6
+ * `data push` bersifat append (INSERT), sehingga tabel CHILD (pemilik kolom FK)
7
+ * wajib di-push SETELAH tabel PARENT (tabel yang direferensikan), agar INSERT child
8
+ * tidak melanggar FK constraint. Helper ini menurunkan urutan tersebut MURNI dari
9
+ * IR SDF, tanpa I/O maupun koneksi DB.
10
+ *
11
+ * Edge dipungut dari `ir.fields[<col>].fk.table` (arah: parent SEBELUM child). FK
12
+ * target selalu nama tabel polos (shorthand `fk:` tidak mendukung lintas-schema —
13
+ * lihat phase-00-discovery Deliverable 1), jadi pemetaan `fk.table → IR` dilakukan
14
+ * via index `tableName → IR`.
15
+ *
16
+ * Penanganan kasus tepi:
17
+ * - Self-FK (`fk.table === ir.tableName`) → edge diabaikan (bukan siklus).
18
+ * - Target tak dikenal (tak ada di `models`, R2) → edge diabaikan (jangan crash).
19
+ * - `tableName` duplikat antar-schema (R1) → ambiguous; edge yang menunjuk
20
+ * nama itu diabaikan (no-edge). v2 berasumsi single-namespace.
21
+ * - Siklus FK (R5) → `cycle:true`; `ordered` =
22
+ * urutan deklarasi (insertion order Map). Runner mencetak warning + fallback.
23
+ *
24
+ * @module generators/lib/data/table-order
25
+ */
26
+
27
+ /**
28
+ * Identitas node = qualifiedName (kunci unik Map). `tableName` bisa duplikat antar
29
+ * schema, sehingga tidak aman dipakai sebagai identitas node.
30
+ * @param {Object} ir
31
+ * @returns {string}
32
+ */
33
+ function nodeKeyOf(ir) {
34
+ return ir.qualifiedName || ir.tableName;
35
+ }
36
+
37
+ /**
38
+ * Urutkan tabel secara topologis berdasarkan FK (Kahn's algorithm).
39
+ *
40
+ * @param {Map<string, Object>} models - Map<qualifiedName, IR> hasil loadSdf.
41
+ * @returns {{ ordered: Object[], cycle: boolean }} `ordered` = IR parent→child
42
+ * (atau urutan deklarasi bila siklus); `cycle` = true bila siklus terdeteksi.
43
+ */
44
+ function topoSortByFk(models) {
45
+ const irs = [...models.values()];
46
+
47
+ // Index tableName → IR. Bila >1 IR berbagi tableName (duplikat antar-schema, R1),
48
+ // nama itu ambiguous: edge yang menargetkannya akan diabaikan (no-edge).
49
+ const byTableName = new Map();
50
+ const ambiguous = new Set();
51
+ for (const ir of irs) {
52
+ const tn = ir.tableName;
53
+ if (byTableName.has(tn)) {
54
+ ambiguous.add(tn);
55
+ } else {
56
+ byTableName.set(tn, ir);
57
+ }
58
+ }
59
+
60
+ // Graph: node = nodeKey (qualifiedName). `indegree` = jumlah parent; `children`
61
+ // = parentKey → Set<childKey>. Inisialisasi semua node dengan indegree 0.
62
+ const indegree = new Map();
63
+ const children = new Map();
64
+ for (const ir of irs) {
65
+ indegree.set(nodeKeyOf(ir), 0);
66
+ children.set(nodeKeyOf(ir), new Set());
67
+ }
68
+
69
+ // Pungut edge unik (parent → child) dari fk.table tiap field.
70
+ for (const childIr of irs) {
71
+ const childKey = nodeKeyOf(childIr);
72
+ for (const fieldName of Object.keys(childIr.fields)) {
73
+ const field = childIr.fields[fieldName];
74
+ const fk = field && field.fk;
75
+ if (!fk || !fk.table) continue;
76
+
77
+ const parentName = fk.table; // nama tabel polos
78
+
79
+ // Self-FK → bukan siklus, abaikan edge.
80
+ if (parentName === childIr.tableName) continue;
81
+ // Target tak dikenal (SDF parsial, R2) → abaikan edge.
82
+ if (!byTableName.has(parentName)) continue;
83
+ // Duplikat antar-schema (R1) → ambiguous, perlakukan sebagai no-edge.
84
+ if (ambiguous.has(parentName)) continue;
85
+
86
+ const parentKey = nodeKeyOf(byTableName.get(parentName));
87
+ // Self lewat qualifiedName (defensif) → abaikan.
88
+ if (parentKey === childKey) continue;
89
+
90
+ // Edge ganda (mis. dua kolom FK ke tabel sama) dihitung sekali.
91
+ if (!children.get(parentKey).has(childKey)) {
92
+ children.get(parentKey).add(childKey);
93
+ indegree.set(childKey, indegree.get(childKey) + 1);
94
+ }
95
+ }
96
+ }
97
+
98
+ // Kahn: antrekan node indegree 0 dalam URUTAN DEKLARASI (insertion order Map),
99
+ // proses children juga dalam urutan penyisipan (= urutan deklarasi child), agar
100
+ // hasil deterministik dan stabil untuk tabel tanpa FK.
101
+ const irByKey = new Map(irs.map((ir) => [nodeKeyOf(ir), ir]));
102
+ const queue = [];
103
+ for (const ir of irs) {
104
+ if (indegree.get(nodeKeyOf(ir)) === 0) queue.push(nodeKeyOf(ir));
105
+ }
106
+
107
+ const orderedKeys = [];
108
+ while (queue.length > 0) {
109
+ const key = queue.shift();
110
+ orderedKeys.push(key);
111
+ for (const childKey of children.get(key)) {
112
+ const next = indegree.get(childKey) - 1;
113
+ indegree.set(childKey, next);
114
+ if (next === 0) queue.push(childKey);
115
+ }
116
+ }
117
+
118
+ // Bila tidak semua node ter-emit, ada siklus (R5): fallback urutan deklarasi.
119
+ if (orderedKeys.length !== irs.length) {
120
+ return { ordered: irs.slice(), cycle: true };
121
+ }
122
+
123
+ return { ordered: orderedKeys.map((key) => irByKey.get(key)), cycle: false };
124
+ }
125
+
126
+ module.exports = { topoSortByFk };