@restforgejs/platform 5.1.4 → 5.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build-info.json +2 -2
- package/cli/consumer-deploy.js +1 -1
- package/cli/consumer.js +1 -1
- package/generators/cli/data/pull.js +16 -7
- package/generators/cli/data/push.js +17 -7
- package/generators/cli/init.js +347 -97
- package/generators/lib/data/data-scope.js +138 -0
- package/generators/lib/data/envelope.js +25 -9
- package/generators/lib/data/pull-runner.js +44 -37
- package/generators/lib/data/push-runner.js +64 -46
- package/generators/lib/templates/dashboard-catalog.js +1 -1
- package/generators/lib/templates/db-connection-env.js +1 -1
- package/generators/lib/templates/dbschema-catalog.js +1 -1
- package/generators/lib/templates/field-validation-catalog.js +1 -1
- package/generators/lib/templates/mysql-template.js +1 -1
- package/generators/lib/templates/oracle-template.js +1 -1
- package/generators/lib/templates/postgres-template.js +1 -1
- package/generators/lib/templates/query-declarative-catalog.js +1 -1
- package/generators/lib/templates/sqlite-template.js +1 -1
- package/integrity-manifest.json +18 -18
- package/package.json +1 -1
- package/scripts/verify-integrity.js +1 -1
- package/server.js +1 -1
- package/src/components/handlers/adjust_handler.js +1 -1
- package/src/components/handlers/audit_handler.js +1 -1
- package/src/components/handlers/delete_handler.js +1 -1
- package/src/components/handlers/export_handler.js +1 -1
- package/src/components/handlers/import_handler.js +1 -1
- package/src/components/handlers/insert_handler.js +1 -1
- package/src/components/handlers/update_handler.js +1 -1
- package/src/components/handlers/upload_handler.js +1 -1
- package/src/components/handlers/workflow_handler.js +1 -1
- package/src/components/integrations/webhook.js +1 -1
- package/src/consumers/baseConsumer.js +1 -1
- package/src/consumers/declarativeMapper.js +1 -1
- package/src/consumers/handlers/apiHandler.js +1 -1
- package/src/consumers/handlers/consoleHandler.js +1 -1
- package/src/consumers/handlers/databaseHandler.js +1 -1
- package/src/consumers/handlers/index.js +1 -1
- package/src/consumers/handlers/kafkaHandler.js +1 -1
- package/src/consumers/index.js +1 -1
- package/src/consumers/messageTransformer.js +1 -1
- package/src/consumers/validator.js +1 -1
- package/src/core/db/dialect/base-dialect.js +1 -1
- package/src/core/db/dialect/index.js +1 -1
- package/src/core/db/dialect/mysql-dialect.js +1 -1
- package/src/core/db/dialect/oracle-dialect.js +1 -1
- package/src/core/db/dialect/postgres-dialect.js +1 -1
- package/src/core/db/dialect/sqlite-dialect.js +1 -1
- package/src/core/db/flatten-helper.js +1 -1
- package/src/core/db/query-builder-error.js +1 -1
- package/src/core/db/query-builder.js +1 -1
- package/src/core/db/relation-helper.js +1 -1
- package/src/core/handlers/delete_handler.js +1 -1
- package/src/core/handlers/insert_handler.js +1 -1
- package/src/core/handlers/update_handler.js +1 -1
- package/src/core/models/base-model.js +1 -1
- package/src/core/utils/cache-manager.js +1 -1
- package/src/core/utils/component-engine.js +1 -1
- package/src/core/utils/context-builder.js +1 -1
- package/src/core/utils/datetime-formatter.js +1 -1
- package/src/core/utils/datetime-parser.js +1 -1
- package/src/core/utils/db.js +1 -1
- package/src/core/utils/logger.js +1 -1
- package/src/core/utils/payload-loader.js +1 -1
- package/src/core/utils/security-checks.js +1 -1
- package/src/middleware/body-options.js +1 -1
- package/src/middleware/cors.js +1 -1
- package/src/middleware/idempotency.js +1 -1
- package/src/middleware/rate-limiter.js +1 -1
- package/src/middleware/request-logger.js +1 -1
- package/src/middleware/security-headers.js +1 -1
- package/src/models/base-model-mysql.js +1 -1
- package/src/models/base-model-oracle.js +1 -1
- package/src/models/base-model-sqlite.js +1 -1
- package/src/models/base-model.js +1 -1
- package/src/pro/caching/redis-client.js +1 -1
- package/src/pro/caching/redis-helper.js +1 -1
- package/src/pro/consumers/baseConsumer.js +1 -1
- package/src/pro/consumers/declarativeMapper.js +1 -1
- package/src/pro/consumers/handlers/apiHandler.js +1 -1
- package/src/pro/consumers/handlers/consoleHandler.js +1 -1
- package/src/pro/consumers/handlers/databaseHandler.js +1 -1
- package/src/pro/consumers/handlers/index.js +1 -1
- package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
- package/src/pro/consumers/index.js +1 -1
- package/src/pro/consumers/messageTransformer.js +1 -1
- package/src/pro/consumers/validator.js +1 -1
- package/src/pro/database/base-model-mysql.js +1 -1
- package/src/pro/database/base-model-oracle.js +1 -1
- package/src/pro/database/base-model-sqlite.js +1 -1
- package/src/pro/database/db-mysql.js +1 -1
- package/src/pro/database/db-oracle.js +1 -1
- package/src/pro/database/db-sqlite.js +1 -1
- package/src/pro/excel/excel-generator.js +1 -1
- package/src/pro/excel/excel-parser.js +1 -1
- package/src/pro/excel/export-service.js +1 -1
- package/src/pro/excel/export_handler.js +1 -1
- package/src/pro/excel/import-service.js +1 -1
- package/src/pro/excel/import-validator.js +1 -1
- package/src/pro/excel/import_handler.js +1 -1
- package/src/pro/excel/upsert-builder.js +1 -1
- package/src/pro/idgen/idgen-routes.js +1 -1
- package/src/pro/integrations/lookup-resolver.js +1 -1
- package/src/pro/integrations/upload-handler-v2.js +1 -1
- package/src/pro/integrations/upload-handler.js +1 -1
- package/src/pro/integrations/webhook.js +1 -1
- package/src/pro/locking/lock-routes.js +1 -1
- package/src/pro/locking/resource-lock-manager.js +1 -1
- package/src/pro/messaging/kafkaConsumerService.js +1 -1
- package/src/pro/messaging/kafkaService.js +1 -1
- package/src/pro/messaging/messagehubService.js +1 -1
- package/src/pro/messaging/rabbitmqService.js +1 -1
- package/src/pro/scheduler/job-manager.js +1 -1
- package/src/pro/scheduler/job-routes.js +1 -1
- package/src/pro/scheduler/job-validator.js +1 -1
- package/src/pro/storage/base-storage-provider.js +1 -1
- package/src/pro/storage/file-metadata-helper.js +1 -1
- package/src/pro/storage/index.js +1 -1
- package/src/pro/storage/local-storage-provider.js +1 -1
- package/src/pro/storage/s3-storage-provider.js +1 -1
- package/src/pro/storage/upload-cleanup-job.js +1 -1
- package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
- package/src/pro/storage/upload-pending-tracker.js +1 -1
- package/src/pro/websocket/broadcast-helper.js +1 -1
- package/src/pro/websocket/index.js +1 -1
- package/src/pro/websocket/livesync-server.js +1 -1
- package/src/pro/websocket/ws-broadcaster.js +1 -1
- package/src/services/export-service.js +1 -1
- package/src/services/import-service.js +1 -1
- package/src/services/kafkaConsumerService.js +1 -1
- package/src/services/kafkaService.js +1 -1
- package/src/services/messagehubService.js +1 -1
- package/src/services/rabbitmqService.js +1 -1
- package/src/utils/cache-invalidation-registry.js +1 -1
- package/src/utils/cache-manager.js +1 -1
- package/src/utils/component-engine.js +1 -1
- package/src/utils/config-extractor.js +1 -1
- package/src/utils/consumerLogger.js +1 -1
- package/src/utils/context-builder.js +1 -1
- package/src/utils/dashboard-helpers.js +1 -1
- package/src/utils/dateHelper.js +1 -1
- package/src/utils/datetime-formatter.js +1 -1
- package/src/utils/datetime-parser.js +1 -1
- package/src/utils/db-bootstrap.js +1 -1
- package/src/utils/db-mysql.js +1 -1
- package/src/utils/db-oracle.js +1 -1
- package/src/utils/db-sqlite.js +1 -1
- package/src/utils/db.js +1 -1
- package/src/utils/demo-generator.js +1 -1
- package/src/utils/excel-generator.js +1 -1
- package/src/utils/excel-parser.js +1 -1
- package/src/utils/file-watcher.js +1 -1
- package/src/utils/id-generator.js +1 -1
- package/src/utils/idempotency-manager.js +1 -1
- package/src/utils/import-validator.js +1 -1
- package/src/utils/license-client.js +1 -1
- package/src/utils/lock-manager.js +1 -1
- package/src/utils/logger.js +1 -1
- package/src/utils/lookup-resolver.js +1 -1
- package/src/utils/payload-loader.js +1 -1
- package/src/utils/processor-response.js +1 -1
- package/src/utils/rabbitmq.js +1 -1
- package/src/utils/redis-client.js +1 -1
- package/src/utils/redis-helper.js +1 -1
- package/src/utils/request-scope.js +1 -1
- package/src/utils/security-checks.js +1 -1
- package/src/utils/service-resolver.js +1 -1
- package/src/utils/shutdown-coordinator.js +1 -1
- package/src/utils/trusted-keys.js +1 -1
- package/src/utils/upload-handler.js +1 -1
- package/src/utils/upsert-builder.js +1 -1
- package/src/utils/workflow-hook-executor.js +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* data-scope — Helper bersama untuk scope flags & derivasi path file data.
|
|
5
|
+
*
|
|
6
|
+
* Dipakai oleh `data pull` dan `data push` agar kedua command berbagi satu-satunya
|
|
7
|
+
* sumber kebenaran untuk: (a) parsing `--schema` comma-separated, (b) validasi
|
|
8
|
+
* "tepat satu" dari `{--table, --schema, --all-schemas}`, (c) derivasi path file
|
|
9
|
+
* relatif (bersarang per schema vs flat), dan (d) resolusi scope flag → daftar IR.
|
|
10
|
+
*
|
|
11
|
+
* Model scope (mirror `schema introspect`, namun "tepat satu" alih-alih kombinasi):
|
|
12
|
+
* --table=<name> → satu IR via findTableOrThrow (qualified/unqualified).
|
|
13
|
+
* --schema=<LIST> → semua IR ber-schemaName ∈ LIST; 0 match = exit 2 (anti-typo).
|
|
14
|
+
* --all-schemas → seluruh IR (boleh kosong → caller tangani sebagai 0 tabel).
|
|
15
|
+
*
|
|
16
|
+
* Aturan path seragam (Q2): layout ditentukan MURNI oleh schema tabel di IR.
|
|
17
|
+
* ber-schema → `<schemaName>/<tableName>.json`
|
|
18
|
+
* tanpa schema → `<tableName>.json` (flat root)
|
|
19
|
+
*
|
|
20
|
+
* Murni in-memory, tanpa I/O maupun koneksi DB.
|
|
21
|
+
*
|
|
22
|
+
* @module generators/lib/data/data-scope
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const { findTableOrThrow, getNamespace } = require('./sdf-reader');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Buat Error dengan exitCode terlampir (precondition/usage = 2).
|
|
30
|
+
* @param {string} message
|
|
31
|
+
* @param {number} exitCode
|
|
32
|
+
* @returns {Error}
|
|
33
|
+
*/
|
|
34
|
+
function failWith(message, exitCode) {
|
|
35
|
+
const err = new Error(message);
|
|
36
|
+
err.exitCode = exitCode;
|
|
37
|
+
return err;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse flag `--schema` (single atau comma-separated) → array nama schema.
|
|
42
|
+
*
|
|
43
|
+
* Selaras pola `schema introspect` (introspect.js:175-180): split koma, trim tiap
|
|
44
|
+
* elemen, buang elemen kosong. Mengembalikan `null` bila bukan string atau hasil
|
|
45
|
+
* akhir kosong (mis. `--schema=` atau `--schema=,,`), sehingga caller dapat
|
|
46
|
+
* memperlakukan "schema tidak disebut" secara seragam.
|
|
47
|
+
*
|
|
48
|
+
* @param {*} value - Nilai flag --schema
|
|
49
|
+
* @returns {string[]|null}
|
|
50
|
+
*/
|
|
51
|
+
function parseSchemaFlag(value) {
|
|
52
|
+
if (typeof value !== 'string') return null;
|
|
53
|
+
const parts = value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
54
|
+
if (parts.length === 0) return null;
|
|
55
|
+
return parts;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Path file data relatif terhadap storage dir, diturunkan MURNI dari schema IR.
|
|
60
|
+
*
|
|
61
|
+
* Satu-satunya titik derivasi nama file untuk pull & push (menghindari drift tiga
|
|
62
|
+
* titik edit lama). Tabel ber-schema → bersarang `<schemaName>/<tableName>.json`;
|
|
63
|
+
* tabel tanpa schema → flat `<tableName>.json`. Pemanggil cukup
|
|
64
|
+
* `path.join(storageDir, relDataFilePath(ir))`.
|
|
65
|
+
*
|
|
66
|
+
* @param {Object} ir - IR model SDF satu tabel
|
|
67
|
+
* @returns {string} Path relatif (memakai separator OS via path.join)
|
|
68
|
+
*/
|
|
69
|
+
function relDataFilePath(ir) {
|
|
70
|
+
const { schemaName } = getNamespace(ir);
|
|
71
|
+
return schemaName
|
|
72
|
+
? path.join(schemaName, `${ir.tableName}.json`)
|
|
73
|
+
: `${ir.tableName}.json`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Tegakkan "tepat satu" dari `{--table, --schema, --all-schemas}`.
|
|
78
|
+
*
|
|
79
|
+
* Arg-parser tidak punya mekanisme XOR lintas-flag (hanya `required` per-flag),
|
|
80
|
+
* sehingga validasi tiga-arah ditegakkan di sini. Dipanggil SEBELUM load SDF /
|
|
81
|
+
* koneksi DB. Keduanya/lebih maupun tidak ada satupun → exit 2 dengan pesan sama.
|
|
82
|
+
*
|
|
83
|
+
* @param {Object} args - argumen ter-parse (args.table, args['all-schemas'])
|
|
84
|
+
* @param {string[]|null} schemaList - hasil parseSchemaFlag(args.schema)
|
|
85
|
+
* @throws {Error} exitCode=2 bila BUKAN tepat satu yang aktif
|
|
86
|
+
*/
|
|
87
|
+
function validateScopeFlags(args, schemaList) {
|
|
88
|
+
const hasTable = args.table != null && args.table !== '';
|
|
89
|
+
const hasSchema = Array.isArray(schemaList) && schemaList.length > 0;
|
|
90
|
+
const hasAll = args['all-schemas'] === true;
|
|
91
|
+
|
|
92
|
+
const count = (hasTable ? 1 : 0) + (hasSchema ? 1 : 0) + (hasAll ? 1 : 0);
|
|
93
|
+
if (count !== 1) {
|
|
94
|
+
throw failWith('Specify exactly one of --table, --schema, or --all-schemas.', 2);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolusi scope flag → daftar IR yang akan diproses (urutan deklarasi Map).
|
|
100
|
+
*
|
|
101
|
+
* Mengasumsikan validateScopeFlags() sudah lolos (tepat satu flag aktif).
|
|
102
|
+
* --table → [findTableOrThrow(models, args.table)] (exit 2 bila tak terdaftar/ambigu).
|
|
103
|
+
* --schema=<L> → subset IR ber-schemaName ∈ L; 0 match → exit 2 (sinyal typo).
|
|
104
|
+
* --all-schemas → seluruh IR (boleh kosong → caller perlakukan sebagai 0 tabel, exit 0).
|
|
105
|
+
*
|
|
106
|
+
* @param {Map<string, Object>} models - hasil loadSdf
|
|
107
|
+
* @param {Object} args - argumen ter-parse
|
|
108
|
+
* @param {string[]|null} schemaList - hasil parseSchemaFlag(args.schema)
|
|
109
|
+
* @returns {Object[]} array IR sesuai scope
|
|
110
|
+
* @throws {Error} exitCode=2 (table tak terdaftar/ambigu, atau --schema 0-match)
|
|
111
|
+
*/
|
|
112
|
+
function resolveScope(models, args, schemaList) {
|
|
113
|
+
const hasTable = args.table != null && args.table !== '';
|
|
114
|
+
if (hasTable) {
|
|
115
|
+
return [findTableOrThrow(models, args.table)];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (Array.isArray(schemaList) && schemaList.length > 0) {
|
|
119
|
+
const set = new Set(schemaList);
|
|
120
|
+
const subset = [...models.values()].filter((ir) => set.has(getNamespace(ir).schemaName));
|
|
121
|
+
if (subset.length === 0) {
|
|
122
|
+
throw failWith(`No tables found for schema(s): ${schemaList.join(', ')}.`, 2);
|
|
123
|
+
}
|
|
124
|
+
return subset;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --all-schemas: seluruh tabel (semua schema, termasuk schemaless). Boleh kosong.
|
|
128
|
+
return [...models.values()];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
parseSchemaFlag,
|
|
133
|
+
relDataFilePath,
|
|
134
|
+
validateScopeFlags,
|
|
135
|
+
resolveScope,
|
|
136
|
+
// Diekspor untuk reuse/test internal.
|
|
137
|
+
_internal: { failWith }
|
|
138
|
+
};
|
|
@@ -4,27 +4,33 @@
|
|
|
4
4
|
* envelope — Bentuk + I/O envelope file data.
|
|
5
5
|
*
|
|
6
6
|
* Struktur envelope (urutan key wajib dipertahankan):
|
|
7
|
-
* { format_version, table, source_dialect, columns, rows }
|
|
7
|
+
* { format_version, schema, table, source_dialect, columns, rows }
|
|
8
|
+
*
|
|
9
|
+
* `schema` (= `ir.schemaName`, `null` bila tanpa schema) ditambahkan di format
|
|
10
|
+
* version 1.1. File 1.0 (tanpa `schema`) tetap diterima oleh `validateEnvelope`
|
|
11
|
+
* untuk back-compat baca `data push`.
|
|
8
12
|
*
|
|
9
13
|
* Mengacu pada docs/plan/data-pull-push/data-pull-push-03-data-format.md.
|
|
10
14
|
*
|
|
11
15
|
* @module generators/lib/data/envelope
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
|
-
const FORMAT_VERSION = '1.
|
|
18
|
+
const FORMAT_VERSION = '1.1';
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* Bangun header envelope (tanpa rows).
|
|
18
22
|
*
|
|
19
23
|
* @param {Object} args
|
|
24
|
+
* @param {string|null} [args.schema] - Nama schema (= ir.schemaName; null bila tanpa schema)
|
|
20
25
|
* @param {string} args.table - Nama tabel
|
|
21
26
|
* @param {string} args.sourceDialect - Dialect sumber (mis. 'postgresql')
|
|
22
27
|
* @param {Array<Object>} args.columns - Daftar kolom { name, type, nullable, primary_key }
|
|
23
|
-
* @returns {{format_version: string, table: string, source_dialect: string, columns: Array}}
|
|
28
|
+
* @returns {{format_version: string, schema: (string|null), table: string, source_dialect: string, columns: Array}}
|
|
24
29
|
*/
|
|
25
|
-
function buildHeader({ table, sourceDialect, columns }) {
|
|
30
|
+
function buildHeader({ schema = null, table, sourceDialect, columns }) {
|
|
26
31
|
return {
|
|
27
32
|
format_version: FORMAT_VERSION,
|
|
33
|
+
schema,
|
|
28
34
|
table,
|
|
29
35
|
source_dialect: sourceDialect,
|
|
30
36
|
columns
|
|
@@ -58,11 +64,11 @@ function resolveSink(sink) {
|
|
|
58
64
|
* Menulis header, meng-append rows per batch, lalu menutup array `rows` dan envelope.
|
|
59
65
|
* Cocok untuk tabel besar karena rows tidak perlu ditahan seluruhnya di memori.
|
|
60
66
|
* Output akhir adalah JSON valid dengan urutan key:
|
|
61
|
-
* format_version, table, source_dialect, columns, rows
|
|
67
|
+
* format_version, schema, table, source_dialect, columns, rows
|
|
62
68
|
*
|
|
63
69
|
* Penggunaan:
|
|
64
70
|
* const w = new EnvelopeWriter(fs.createWriteStream(path));
|
|
65
|
-
* w.writeHeader({ table, sourceDialect, columns });
|
|
71
|
+
* w.writeHeader({ schema, table, sourceDialect, columns });
|
|
66
72
|
* w.appendRows(batch1);
|
|
67
73
|
* w.appendRows(batch2);
|
|
68
74
|
* w.end();
|
|
@@ -83,16 +89,17 @@ class EnvelopeWriter {
|
|
|
83
89
|
|
|
84
90
|
/**
|
|
85
91
|
* Tulis prefix header dan buka array `rows`.
|
|
86
|
-
* @param {Object} header - { table, sourceDialect, columns }
|
|
92
|
+
* @param {Object} header - { schema, table, sourceDialect, columns }
|
|
87
93
|
* @returns {EnvelopeWriter} this
|
|
88
94
|
*/
|
|
89
|
-
writeHeader({ table, sourceDialect, columns }) {
|
|
95
|
+
writeHeader({ schema, table, sourceDialect, columns }) {
|
|
90
96
|
if (this._state !== 'init') {
|
|
91
97
|
throw new Error('EnvelopeWriter: writeHeader() hanya boleh dipanggil sekali di awal');
|
|
92
98
|
}
|
|
93
99
|
const prefix =
|
|
94
100
|
'{' +
|
|
95
101
|
`"format_version":${JSON.stringify(FORMAT_VERSION)},` +
|
|
102
|
+
`"schema":${JSON.stringify(schema ?? null)},` +
|
|
96
103
|
`"table":${JSON.stringify(table)},` +
|
|
97
104
|
`"source_dialect":${JSON.stringify(sourceDialect)},` +
|
|
98
105
|
`"columns":${JSON.stringify(columns)},` +
|
|
@@ -163,7 +170,11 @@ function parseEnvelope(jsonString) {
|
|
|
163
170
|
* Validasi shape envelope. Throw (exitCode=1) bila tidak valid.
|
|
164
171
|
*
|
|
165
172
|
* Wajib: format_version (ada), table (string), columns (array of {name,type}),
|
|
166
|
-
* rows (array of object).
|
|
173
|
+
* rows (array of object). Opsional: `schema` (string atau null bila ada).
|
|
174
|
+
*
|
|
175
|
+
* `format_version` tetap LENIENT (cukup ada/non-null) agar file 1.0 maupun 1.1
|
|
176
|
+
* sama-sama diterima tanpa penolakan versi. `schema` opsional untuk back-compat:
|
|
177
|
+
* file 1.0 (tanpa key `schema`) diterima; bila ada, wajib string atau null.
|
|
167
178
|
*
|
|
168
179
|
* @param {Object} obj
|
|
169
180
|
* @returns {Object} obj yang sama (untuk chaining)
|
|
@@ -182,6 +193,11 @@ function validateEnvelope(obj) {
|
|
|
182
193
|
if (obj.format_version === undefined || obj.format_version === null) {
|
|
183
194
|
fail('field "format_version" wajib ada');
|
|
184
195
|
}
|
|
196
|
+
// `schema` opsional (back-compat 1.0). Bila absen → diterima; bila ada → wajib
|
|
197
|
+
// string atau null. `null` eksplisit (1.1 tabel schemaless) tetap valid.
|
|
198
|
+
if (obj.schema !== undefined && obj.schema !== null && typeof obj.schema !== 'string') {
|
|
199
|
+
fail('field "schema" wajib berupa string atau null bila ada');
|
|
200
|
+
}
|
|
185
201
|
if (typeof obj.table !== 'string' || obj.table === '') {
|
|
186
202
|
fail('field "table" wajib berupa string non-kosong');
|
|
187
203
|
}
|
|
@@ -3,17 +3,18 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* pull-runner — Business logic command `data pull`.
|
|
5
5
|
*
|
|
6
|
-
* `run()` adalah dispatcher (single vs
|
|
6
|
+
* `run()` adalah dispatcher (single vs multi-schema); `runOne(ir, ctx)` mengeksekusi
|
|
7
7
|
* pull satu tabel dan mengelola executor-nya sendiri (Phase 02, data-pull-push-v2).
|
|
8
8
|
*
|
|
9
9
|
* Alur dispatcher `run()`:
|
|
10
10
|
* 1. Resolve config DB (config-resolver) → loadConfig (dbschema-kit/connection); SEKALI.
|
|
11
|
-
* 2.
|
|
12
|
-
* --table / --all-
|
|
11
|
+
* 2. parseSchemaFlag + validateScopeFlags (sebelum load SDF / koneksi DB): tepat satu
|
|
12
|
+
* dari --table / --schema / --all-schemas (selain itu → exit 2).
|
|
13
13
|
* 3. Validasi --format (hanya 'json').
|
|
14
14
|
* 4. Load SDF SEKALI → Map<qualifiedName, IR> (sdf-reader).
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* 5. resolveScope(models, args, schemaList) → daftar IR sesuai flag scope.
|
|
16
|
+
* 5a. Jalur --table: satu IR → runOne(ir, ctx) → ringkasan single.
|
|
17
|
+
* 5b. Jalur --schema/--all-schemas: iterasi subset FAIL-FAST (Q3) → runOne per IR →
|
|
17
18
|
* ringkasan agregat. Tabel yang sudah selesai sebelum error tetap tertulis.
|
|
18
19
|
*
|
|
19
20
|
* Alur `runOne(ir, ctx)` (per tabel; metadata kolom/namespace/PK murni dari SDF IR):
|
|
@@ -37,11 +38,11 @@ const { resolveConfig, printDefaultConfigWarning } = require('../utils/config-re
|
|
|
37
38
|
const { loadConfig } = require('../dbschema-kit/connection');
|
|
38
39
|
const {
|
|
39
40
|
loadSdf,
|
|
40
|
-
findTableOrThrow,
|
|
41
41
|
irToColumns,
|
|
42
42
|
getNamespace,
|
|
43
43
|
getPrimaryKey
|
|
44
44
|
} = require('./sdf-reader');
|
|
45
|
+
const { parseSchemaFlag, validateScopeFlags, resolveScope, relDataFilePath } = require('./data-scope');
|
|
45
46
|
const { createDialect, validateIdentifier } = require('./dialect-kit');
|
|
46
47
|
const { createExecutor } = require('./db-executor');
|
|
47
48
|
const { encodeValue } = require('./value-codec');
|
|
@@ -176,10 +177,12 @@ async function pullInBatches({ executor, dialect, queryTable, columns, pk, batch
|
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
/**
|
|
179
|
-
* Pull satu tabel SDF ke file envelope JSON
|
|
180
|
+
* Pull satu tabel SDF ke file envelope JSON (path via relDataFilePath: bersarang
|
|
181
|
+
* `<output>/<schema>/<table>.json` bila ber-schema, flat `<output>/<table>.json`
|
|
182
|
+
* bila tanpa schema).
|
|
180
183
|
*
|
|
181
184
|
* Mengelola executor-nya sendiri (buat → disconnect, termasuk cleanup file partial
|
|
182
|
-
* saat error) agar isolasi per-tabel jelas dan jalur
|
|
185
|
+
* saat error) agar isolasi per-tabel jelas dan jalur multi-schema dapat memanggilnya
|
|
183
186
|
* berulang tanpa kebocoran koneksi. Metadata kolom, namespace, dan PK murni dari IR.
|
|
184
187
|
*
|
|
185
188
|
* @param {Object} ir - IR model SDF satu tabel
|
|
@@ -188,24 +191,24 @@ async function pullInBatches({ executor, dialect, queryTable, columns, pk, batch
|
|
|
188
191
|
* @param {string} ctx.sourceDialect - dialect kanonik untuk header envelope
|
|
189
192
|
* @param {Object} ctx.args - argumen ter-parse (storage-path, force, limit, batch-size)
|
|
190
193
|
* @param {string} ctx.cwd - working directory
|
|
191
|
-
* @returns {Promise<{table: string, rows: number, file: string, source_dialect: string}>}
|
|
194
|
+
* @returns {Promise<{schema: (string|null), table: string, rows: number, file: string, source_dialect: string}>}
|
|
192
195
|
*/
|
|
193
196
|
async function runOne(ir, ctx) {
|
|
194
197
|
const { config, sourceDialect, args, cwd } = ctx;
|
|
195
198
|
|
|
196
199
|
const columns = irToColumns(ir);
|
|
197
|
-
const { qualifiedName } = getNamespace(ir);
|
|
200
|
+
const { schemaName, qualifiedName } = getNamespace(ir);
|
|
198
201
|
const rawPk = getPrimaryKey(ir);
|
|
199
202
|
const pk = Array.isArray(rawPk) ? rawPk : [];
|
|
200
203
|
|
|
201
204
|
// ── a. Cek file output (SEBELUM menulis/konek) ───────────────────────────────
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
// schema
|
|
205
|
-
//
|
|
206
|
-
//
|
|
205
|
+
// Path diturunkan dari relDataFilePath (aturan seragam Q2): tabel ber-schema →
|
|
206
|
+
// bersarang `<schema>/<table>.json` (mis. `sales/order_item.json`); tabel tanpa
|
|
207
|
+
// schema → flat `<table>.json` (mis. `visitors.json`). Helper bersama dengan push
|
|
208
|
+
// (data-scope) agar derivasi tidak drift. queryTable SELECT tetap memakai
|
|
209
|
+
// qualifiedName (identitas DB).
|
|
207
210
|
const outputDir = path.resolve(cwd, args['storage-path']);
|
|
208
|
-
const outFile = path.join(outputDir,
|
|
211
|
+
const outFile = path.join(outputDir, relDataFilePath(ir));
|
|
209
212
|
if (fs.existsSync(outFile) && args.force !== true) {
|
|
210
213
|
const rel = path.relative(cwd, outFile).split(path.sep).join('/');
|
|
211
214
|
throw failWith(
|
|
@@ -238,11 +241,14 @@ async function runOne(ir, ctx) {
|
|
|
238
241
|
let rowCount = 0;
|
|
239
242
|
let fd = null;
|
|
240
243
|
try {
|
|
241
|
-
|
|
244
|
+
// Buat direktori parent dari outFile (bukan sekadar outputDir): pada layout
|
|
245
|
+
// bersarang, outFile berada di subfolder schema (mis. `<storage>/main/`) yang
|
|
246
|
+
// belum tentu ada. path.dirname mencakup subfolder schema maupun flat root.
|
|
247
|
+
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
242
248
|
fd = fs.openSync(outFile, 'w');
|
|
243
249
|
|
|
244
250
|
const writer = new EnvelopeWriter((chunk) => fs.writeSync(fd, chunk));
|
|
245
|
-
writer.writeHeader({ table: ir.tableName, sourceDialect, columns });
|
|
251
|
+
writer.writeHeader({ schema: schemaName, table: ir.tableName, sourceDialect, columns });
|
|
246
252
|
|
|
247
253
|
rowCount = await pullInBatches({
|
|
248
254
|
executor,
|
|
@@ -277,6 +283,7 @@ async function runOne(ir, ctx) {
|
|
|
277
283
|
// ── e. Ringkasan per-tabel (tanpa `command`; dirakit caller) ─────────────────
|
|
278
284
|
const relFile = path.relative(cwd, outFile).split(path.sep).join('/');
|
|
279
285
|
return {
|
|
286
|
+
schema: schemaName,
|
|
280
287
|
table: ir.tableName,
|
|
281
288
|
rows: rowCount,
|
|
282
289
|
file: relFile,
|
|
@@ -285,10 +292,10 @@ async function runOne(ir, ctx) {
|
|
|
285
292
|
}
|
|
286
293
|
|
|
287
294
|
/**
|
|
288
|
-
* Jalankan command `data pull` (dispatcher: single-table vs
|
|
295
|
+
* Jalankan command `data pull` (dispatcher: single-table vs multi-schema).
|
|
289
296
|
* @param {Object} args - argumen ter-parse dari contract
|
|
290
|
-
* @returns {Promise<Object>} ringkasan single { command, table, rows, file, source_dialect }
|
|
291
|
-
* atau agregat { command,
|
|
297
|
+
* @returns {Promise<Object>} ringkasan single { command, schema, table, rows, file, source_dialect }
|
|
298
|
+
* atau agregat { command, scope, table_count, total_rows, tables: [...] }
|
|
292
299
|
*/
|
|
293
300
|
async function run(args) {
|
|
294
301
|
const cwd = process.cwd();
|
|
@@ -312,17 +319,12 @@ async function run(args) {
|
|
|
312
319
|
throw failWith(e.message, 2);
|
|
313
320
|
}
|
|
314
321
|
|
|
315
|
-
// ── 2. Validasi
|
|
316
|
-
//
|
|
317
|
-
//
|
|
322
|
+
// ── 2. Validasi scope flags (SEBELUM SDF/DB) ─────────────────────────────────
|
|
323
|
+
// Tepat satu dari --table / --schema / --all-schemas (selain itu → exit 2).
|
|
324
|
+
// Helper bersama dengan push (data-scope) agar perilaku identik.
|
|
325
|
+
const schemaList = parseSchemaFlag(args.schema);
|
|
326
|
+
validateScopeFlags(args, schemaList);
|
|
318
327
|
const hasTable = args.table != null && args.table !== '';
|
|
319
|
-
const hasAll = args['all-tables'] === true;
|
|
320
|
-
if (hasTable && hasAll) {
|
|
321
|
-
throw failWith('Specify exactly one of --table or --all-tables.', 2);
|
|
322
|
-
}
|
|
323
|
-
if (!hasTable && !hasAll) {
|
|
324
|
-
throw failWith('Specify either --table=<name> or --all-tables.', 2);
|
|
325
|
-
}
|
|
326
328
|
|
|
327
329
|
// ── 3. Validasi --format ─────────────────────────────────────────────────────
|
|
328
330
|
if (args.format !== 'json') {
|
|
@@ -350,10 +352,14 @@ async function run(args) {
|
|
|
350
352
|
cwd
|
|
351
353
|
};
|
|
352
354
|
|
|
355
|
+
// ── 5. Resolusi scope → daftar IR ────────────────────────────────────────────
|
|
356
|
+
// --table → [ir] (exit 2 bila tak terdaftar/ambigu); --schema → subset (exit 2
|
|
357
|
+
// bila 0-match); --all-schemas → seluruh IR (boleh kosong → agregat 0 tabel).
|
|
358
|
+
const subset = resolveScope(models, args, schemaList);
|
|
359
|
+
|
|
353
360
|
// ── 5a. Jalur --table (single; regresi nol) ──────────────────────────────────
|
|
354
361
|
if (hasTable) {
|
|
355
|
-
const
|
|
356
|
-
const per = await runOne(ir, ctx);
|
|
362
|
+
const per = await runOne(subset[0], ctx);
|
|
357
363
|
const summary = { command: 'pull', ...per };
|
|
358
364
|
|
|
359
365
|
if (args.json === true) {
|
|
@@ -366,13 +372,14 @@ async function run(args) {
|
|
|
366
372
|
return summary;
|
|
367
373
|
}
|
|
368
374
|
|
|
369
|
-
// ── 5b. Jalur --all-
|
|
375
|
+
// ── 5b. Jalur --schema/--all-schemas (fail-fast Q3; agregat) ─────────────────
|
|
370
376
|
// Iterasi mengikuti urutan insertion Map (file SDF ter-sort alfabetis). Error
|
|
371
377
|
// pada satu tabel menghentikan loop (fail-fast); tabel yang sudah selesai
|
|
372
|
-
// sebelum error tetap tertulis (konsekuensi Q3/R4, didokumentasikan Phase
|
|
378
|
+
// sebelum error tetap tertulis (konsekuensi Q3/R4, didokumentasikan Phase 03).
|
|
379
|
+
const scope = schemaList ? 'schema' : 'all-schemas';
|
|
373
380
|
const tables = [];
|
|
374
381
|
let totalRows = 0;
|
|
375
|
-
for (const ir of
|
|
382
|
+
for (const ir of subset) {
|
|
376
383
|
const per = await runOne(ir, ctx);
|
|
377
384
|
tables.push(per);
|
|
378
385
|
totalRows += per.rows;
|
|
@@ -385,7 +392,7 @@ async function run(args) {
|
|
|
385
392
|
|
|
386
393
|
const aggregate = {
|
|
387
394
|
command: 'pull',
|
|
388
|
-
|
|
395
|
+
scope,
|
|
389
396
|
table_count: tables.length,
|
|
390
397
|
total_rows: totalRows,
|
|
391
398
|
tables
|
|
@@ -7,21 +7,23 @@
|
|
|
7
7
|
* di-descope dari v1 (User konfirmasi 2026-06-03), sehingga flag `--mode`/`--force`
|
|
8
8
|
* dihapus dari contract dan runner tidak lagi membaca/memvalidasi keduanya.
|
|
9
9
|
*
|
|
10
|
-
* `run()` adalah dispatcher (single vs
|
|
10
|
+
* `run()` adalah dispatcher (single vs multi-schema); `runOne(ir, ctx)` mengeksekusi
|
|
11
11
|
* push satu tabel dan mengelola executor-nya sendiri (Phase 03, data-pull-push-v2).
|
|
12
12
|
*
|
|
13
13
|
* Alur dispatcher `run()`:
|
|
14
14
|
* 1. Resolve config DB tujuan (config-resolver) → loadConfig (dbschema-kit/connection); SEKALI.
|
|
15
|
-
* 2.
|
|
16
|
-
* --table / --all-
|
|
15
|
+
* 2. parseSchemaFlag + validateScopeFlags (sebelum load SDF / koneksi DB): tepat satu
|
|
16
|
+
* dari --table / --schema / --all-schemas (selain itu → exit 2).
|
|
17
17
|
* 3. Load SDF SEKALI → Map<qualifiedName, IR> (sdf-reader).
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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.
|
|
22
23
|
*
|
|
23
24
|
* Alur `runOne(ir, ctx)` (per tabel; metadata kolom/namespace/PK murni dari SDF IR):
|
|
24
|
-
* a. Derive nama file dari SDF (aturan IDENTIK pull):
|
|
25
|
+
* a. Derive nama file dari SDF (aturan IDENTIK pull, relDataFilePath): bersarang
|
|
26
|
+
* `<input>/<schema>/<table>.json` bila ber-schema, flat `<input>/<table>.json`.
|
|
25
27
|
* File tidak ada → exit 1. Baca + parseEnvelope + validateEnvelope (shape invalid → exit 1).
|
|
26
28
|
* b. Column match vs SDF (BUKAN introspeksi DB): tiap envelope.columns[].name harus ada
|
|
27
29
|
* di SDF dan tipe konsisten. Mismatch → exit 1. No-PK guard (exit 1).
|
|
@@ -46,11 +48,11 @@ const { resolveConfig, printDefaultConfigWarning } = require('../utils/config-re
|
|
|
46
48
|
const { loadConfig } = require('../dbschema-kit/connection');
|
|
47
49
|
const {
|
|
48
50
|
loadSdf,
|
|
49
|
-
findTableOrThrow,
|
|
50
51
|
irToColumns,
|
|
51
52
|
getNamespace,
|
|
52
53
|
getPrimaryKey
|
|
53
54
|
} = require('./sdf-reader');
|
|
55
|
+
const { parseSchemaFlag, validateScopeFlags, resolveScope, relDataFilePath } = require('./data-scope');
|
|
54
56
|
const { createDialect, validateIdentifier, placeholder } = require('./dialect-kit');
|
|
55
57
|
const { createExecutor } = require('./db-executor');
|
|
56
58
|
const { decodeValue } = require('./value-codec');
|
|
@@ -135,10 +137,12 @@ function matchColumns(envelopeColumns, sdfColumns, tableName) {
|
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
/**
|
|
138
|
-
* Push satu tabel SDF dari file envelope JSON
|
|
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).
|
|
139
143
|
*
|
|
140
144
|
* Mengelola executor-nya sendiri (buat → disconnect di finally) agar isolasi
|
|
141
|
-
* per-tabel jelas dan jalur
|
|
145
|
+
* per-tabel jelas dan jalur multi-schema dapat memanggilnya berulang tanpa kebocoran
|
|
142
146
|
* koneksi. Metadata kolom, namespace, dan PK murni dari IR.
|
|
143
147
|
*
|
|
144
148
|
* @param {Object} ir - IR model SDF satu tabel
|
|
@@ -147,24 +151,24 @@ function matchColumns(envelopeColumns, sdfColumns, tableName) {
|
|
|
147
151
|
* @param {string} ctx.targetDialect - dialect kanonik (untuk output, dirakit caller)
|
|
148
152
|
* @param {Object} ctx.args - argumen ter-parse (storage-path, batch-size)
|
|
149
153
|
* @param {string} ctx.cwd - working directory
|
|
150
|
-
* @returns {Promise<{table: string, rows: number, committed_batches: number}>}
|
|
154
|
+
* @returns {Promise<{schema: (string|null), table: string, rows: number, committed_batches: number}>}
|
|
151
155
|
*/
|
|
152
156
|
async function runOne(ir, ctx) {
|
|
153
157
|
const { config, args, cwd } = ctx;
|
|
154
158
|
|
|
155
159
|
const sdfColumns = irToColumns(ir);
|
|
156
|
-
const { qualifiedName } = getNamespace(ir);
|
|
160
|
+
const { schemaName, qualifiedName } = getNamespace(ir);
|
|
157
161
|
const rawPk = getPrimaryKey(ir);
|
|
158
162
|
const pk = Array.isArray(rawPk) ? rawPk : [];
|
|
159
163
|
|
|
160
|
-
// ── a. Derive nama file dari SDF (aturan IDENTIK pull)
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
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.
|
|
166
170
|
const inputDir = path.resolve(cwd, args['storage-path']);
|
|
167
|
-
const inFile = path.join(inputDir,
|
|
171
|
+
const inFile = path.join(inputDir, relDataFilePath(ir));
|
|
168
172
|
if (!fs.existsSync(inFile)) {
|
|
169
173
|
const rel = path.relative(cwd, inFile).split(path.sep).join('/');
|
|
170
174
|
throw failWith(`Input file not found: ${rel}`, 1);
|
|
@@ -180,6 +184,17 @@ async function runOne(ir, ctx) {
|
|
|
180
184
|
throw failWith(`Failed to read envelope '${path.relative(cwd, inFile).split(path.sep).join('/')}': ${e.message}`, 1);
|
|
181
185
|
}
|
|
182
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
|
+
|
|
183
198
|
// ── b. Column match vs SDF (tanpa introspeksi DB) + no-PK guard ──────────────
|
|
184
199
|
const insertColumns = matchColumns(envelope.columns, sdfColumns, ir.tableName);
|
|
185
200
|
|
|
@@ -233,6 +248,7 @@ async function runOne(ir, ctx) {
|
|
|
233
248
|
|
|
234
249
|
// ── e. Ringkasan per-tabel (tanpa command/target_dialect; dirakit caller) ─────
|
|
235
250
|
return {
|
|
251
|
+
schema: schemaName,
|
|
236
252
|
table: ir.tableName,
|
|
237
253
|
rows: rowCount,
|
|
238
254
|
committed_batches: committedBatches
|
|
@@ -240,10 +256,10 @@ async function runOne(ir, ctx) {
|
|
|
240
256
|
}
|
|
241
257
|
|
|
242
258
|
/**
|
|
243
|
-
* Jalankan command `data push` (dispatcher: single-table vs
|
|
259
|
+
* Jalankan command `data push` (dispatcher: single-table vs multi-schema, append-only).
|
|
244
260
|
* @param {Object} args - argumen ter-parse dari contract
|
|
245
|
-
* @returns {Promise<Object>} ringkasan single { command, table, rows, target_dialect, committed_batches }
|
|
246
|
-
* atau agregat { command,
|
|
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: [...] }
|
|
247
263
|
*/
|
|
248
264
|
async function run(args) {
|
|
249
265
|
const cwd = process.cwd();
|
|
@@ -267,18 +283,12 @@ async function run(args) {
|
|
|
267
283
|
throw failWith(e.message, 2);
|
|
268
284
|
}
|
|
269
285
|
|
|
270
|
-
// ── 2. Validasi
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
|
|
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);
|
|
274
291
|
const hasTable = args.table != null && args.table !== '';
|
|
275
|
-
const hasAll = args['all-tables'] === true;
|
|
276
|
-
if (hasTable && hasAll) {
|
|
277
|
-
throw failWith('Specify exactly one of --table or --all-tables.', 2);
|
|
278
|
-
}
|
|
279
|
-
if (!hasTable && !hasAll) {
|
|
280
|
-
throw failWith('Specify either --table=<name> or --all-tables.', 2);
|
|
281
|
-
}
|
|
282
292
|
|
|
283
293
|
// ── 3. Load SDF (SEKALI) ─────────────────────────────────────────────────────
|
|
284
294
|
const schemaPath = args['schema-path'];
|
|
@@ -298,12 +308,17 @@ async function run(args) {
|
|
|
298
308
|
cwd
|
|
299
309
|
};
|
|
300
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
|
+
|
|
301
316
|
// ── 4a. Jalur --table (single; regresi nol) ──────────────────────────────────
|
|
302
317
|
if (hasTable) {
|
|
303
|
-
const
|
|
304
|
-
const per = await runOne(ir, ctx);
|
|
318
|
+
const per = await runOne(subset[0], ctx);
|
|
305
319
|
const summary = {
|
|
306
320
|
command: 'push',
|
|
321
|
+
schema: per.schema,
|
|
307
322
|
table: per.table,
|
|
308
323
|
rows: per.rows,
|
|
309
324
|
target_dialect: ctx.targetDialect,
|
|
@@ -320,25 +335,28 @@ async function run(args) {
|
|
|
320
335
|
return summary;
|
|
321
336
|
}
|
|
322
337
|
|
|
323
|
-
// ── 4b. Jalur --all-
|
|
324
|
-
// Urutan push diturunkan dari FK SDF
|
|
325
|
-
// agar INSERT child tidak melanggar FK constraint.
|
|
326
|
-
|
|
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);
|
|
327
347
|
if (cycle) {
|
|
328
348
|
process.stderr.write('FK cycle detected; falling back to declaration order\n');
|
|
329
349
|
}
|
|
330
350
|
|
|
331
|
-
// Skip-missing HANYA di
|
|
332
|
-
// catat), bukan error.
|
|
333
|
-
// INSERT child dapat melanggar FK → muncul sebagai DB error exit 3 (fail-fast).
|
|
351
|
+
// Skip-missing HANYA di jalur multi: tabel tanpa file input dilewati (warning +
|
|
352
|
+
// catat), bukan error. Path input via relDataFilePath (aturan IDENTIK pull).
|
|
334
353
|
const inputDir = path.resolve(cwd, args['storage-path']);
|
|
335
354
|
const tables = [];
|
|
336
355
|
const skipped = [];
|
|
337
356
|
let totalRows = 0;
|
|
338
357
|
|
|
339
358
|
for (const ir of ordered) {
|
|
340
|
-
const
|
|
341
|
-
const inFile = path.join(inputDir, `${qualifiedName}.json`);
|
|
359
|
+
const inFile = path.join(inputDir, relDataFilePath(ir));
|
|
342
360
|
if (!fs.existsSync(inFile)) {
|
|
343
361
|
process.stderr.write(`Skipped ${ir.tableName}: input file not found\n`);
|
|
344
362
|
skipped.push(ir.tableName);
|
|
@@ -357,7 +375,7 @@ async function run(args) {
|
|
|
357
375
|
|
|
358
376
|
const aggregate = {
|
|
359
377
|
command: 'push',
|
|
360
|
-
|
|
378
|
+
scope,
|
|
361
379
|
table_count: tables.length,
|
|
362
380
|
total_rows: totalRows,
|
|
363
381
|
tables,
|