frg-data-diff 2.0.0
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/README.md +628 -0
- package/dist/apply/apply-diff.d.ts +24 -0
- package/dist/apply/apply-diff.js +205 -0
- package/dist/apply/conflict.d.ts +20 -0
- package/dist/apply/conflict.js +14 -0
- package/dist/apply/plan.d.ts +13 -0
- package/dist/apply/plan.js +37 -0
- package/dist/cli/apply.d.ts +8 -0
- package/dist/cli/apply.js +270 -0
- package/dist/cli/generator.d.ts +9 -0
- package/dist/cli/generator.js +804 -0
- package/dist/cli/pg-triggers.d.ts +2 -0
- package/dist/cli/pg-triggers.js +185 -0
- package/dist/cli/root.d.ts +8 -0
- package/dist/cli/root.js +231 -0
- package/dist/cli/sql.d.ts +9 -0
- package/dist/cli/sql.js +158 -0
- package/dist/config/config-schema.d.ts +380 -0
- package/dist/config/config-schema.js +95 -0
- package/dist/config/load-config.d.ts +12 -0
- package/dist/config/load-config.js +87 -0
- package/dist/config/resolve-options.d.ts +95 -0
- package/dist/config/resolve-options.js +183 -0
- package/dist/config/write-config.d.ts +18 -0
- package/dist/config/write-config.js +103 -0
- package/dist/db/connection.d.ts +28 -0
- package/dist/db/connection.js +34 -0
- package/dist/db/metadata.d.ts +70 -0
- package/dist/db/metadata.js +238 -0
- package/dist/db/pg-triggers.d.ts +27 -0
- package/dist/db/pg-triggers.js +59 -0
- package/dist/db/sql.d.ts +72 -0
- package/dist/db/sql.js +250 -0
- package/dist/diff/diff-schema.d.ts +380 -0
- package/dist/diff/diff-schema.js +67 -0
- package/dist/diff/generate-diff.d.ts +20 -0
- package/dist/diff/generate-diff.js +224 -0
- package/dist/diff/pg-triggers-diff.d.ts +11 -0
- package/dist/diff/pg-triggers-diff.js +161 -0
- package/dist/diff/serialize-value.d.ts +35 -0
- package/dist/diff/serialize-value.js +242 -0
- package/dist/diff/write-diff-yaml.d.ts +3 -0
- package/dist/diff/write-diff-yaml.js +48 -0
- package/dist/schema-diff/generate-schema-diff.d.ts +12 -0
- package/dist/schema-diff/generate-schema-diff.js +355 -0
- package/dist/schema-diff/generate-schema-sql.d.ts +20 -0
- package/dist/schema-diff/generate-schema-sql.js +187 -0
- package/dist/schema-diff/schema-diff-schema.d.ts +1088 -0
- package/dist/schema-diff/schema-diff-schema.js +70 -0
- package/dist/shared/env-values.d.ts +11 -0
- package/dist/shared/env-values.js +100 -0
- package/dist/shared/generator-wizard.d.ts +10 -0
- package/dist/shared/generator-wizard.js +337 -0
- package/dist/shared/identifiers.d.ts +24 -0
- package/dist/shared/identifiers.js +45 -0
- package/dist/shared/prompts.d.ts +26 -0
- package/dist/shared/prompts.js +104 -0
- package/dist/shared/summary.d.ts +33 -0
- package/dist/shared/summary.js +41 -0
- package/dist/sql/generate-sql.d.ts +19 -0
- package/dist/sql/generate-sql.js +223 -0
- package/package.json +39 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConflictError = void 0;
|
|
4
|
+
exports.applyTableDiff = applyTableDiff;
|
|
5
|
+
const serialize_value_1 = require("../diff/serialize-value");
|
|
6
|
+
const sql_1 = require("../db/sql");
|
|
7
|
+
const metadata_1 = require("../db/metadata");
|
|
8
|
+
/**
|
|
9
|
+
* Applies a single table diff to the destination database.
|
|
10
|
+
* Modifies summary in-place.
|
|
11
|
+
*/
|
|
12
|
+
async function applyTableDiff(client, tableDiff, options, summary) {
|
|
13
|
+
const { schema, table, primaryKey, inserts, updates, deletes } = tableDiff;
|
|
14
|
+
// Fetch column type metadata for type-correct guard WHERE clauses.
|
|
15
|
+
// Skip if dry-run and there's nothing to process.
|
|
16
|
+
let columnTypes = {};
|
|
17
|
+
const needsTypes = !options.dryRun &&
|
|
18
|
+
((options.applyUpdates && updates.length > 0) ||
|
|
19
|
+
(options.applyDeletes && deletes.length > 0));
|
|
20
|
+
if (needsTypes) {
|
|
21
|
+
columnTypes = await (0, metadata_1.fetchColumnTypeMap)(client, schema, table);
|
|
22
|
+
}
|
|
23
|
+
// 1. Inserts
|
|
24
|
+
if (options.applyInserts && inserts.length > 0) {
|
|
25
|
+
for (const record of inserts) {
|
|
26
|
+
await applyInsert(client, schema, table, primaryKey, record, options, summary);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// 2. Updates
|
|
30
|
+
if (options.applyUpdates && updates.length > 0) {
|
|
31
|
+
for (const record of updates) {
|
|
32
|
+
await applyUpdate(client, schema, table, primaryKey, record, options, summary, columnTypes);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// 3. Deletes (only if explicitly enabled)
|
|
36
|
+
if (options.applyDeletes && deletes.length > 0) {
|
|
37
|
+
for (const record of deletes) {
|
|
38
|
+
await applyDelete(client, schema, table, primaryKey, record, options, summary, columnTypes);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function applyInsert(client, schema, table, pkColumns, record, options, summary) {
|
|
43
|
+
const columns = Object.keys(record.row);
|
|
44
|
+
const deserializedRow = {};
|
|
45
|
+
for (const col of columns) {
|
|
46
|
+
deserializedRow[col] = (0, serialize_value_1.deserializeValue)(record.row[col]);
|
|
47
|
+
}
|
|
48
|
+
const pkValues = {};
|
|
49
|
+
for (const col of pkColumns) {
|
|
50
|
+
pkValues[col] = deserializedRow[col];
|
|
51
|
+
}
|
|
52
|
+
if (options.verbose) {
|
|
53
|
+
console.log(` INSERT into ${schema}.${table} pk=${JSON.stringify(pkValues)}`);
|
|
54
|
+
}
|
|
55
|
+
if (options.dryRun) {
|
|
56
|
+
summary.applied.inserts++;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (options.conflictMode === "skip" || options.conflictMode === "overwrite") {
|
|
60
|
+
// Use a savepoint so a failure doesn't abort the entire transaction
|
|
61
|
+
const sp = `sp_insert_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
62
|
+
await client.query(`SAVEPOINT ${sp}`);
|
|
63
|
+
try {
|
|
64
|
+
if (options.insertMode === "upsert") {
|
|
65
|
+
await (0, sql_1.upsertRow)(client, schema, table, deserializedRow, columns, pkColumns);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
await (0, sql_1.insertRow)(client, schema, table, deserializedRow, columns);
|
|
69
|
+
}
|
|
70
|
+
await client.query(`RELEASE SAVEPOINT ${sp}`);
|
|
71
|
+
summary.applied.inserts++;
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
await client.query(`ROLLBACK TO SAVEPOINT ${sp}`);
|
|
75
|
+
await client.query(`RELEASE SAVEPOINT ${sp}`);
|
|
76
|
+
if (options.conflictMode === "skip") {
|
|
77
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
78
|
+
summary.skipped.push({
|
|
79
|
+
table: `${schema}.${table}`,
|
|
80
|
+
operation: "insert",
|
|
81
|
+
pk: pkValues,
|
|
82
|
+
reason,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// overwrite: try upsert as fallback
|
|
87
|
+
await (0, sql_1.upsertRow)(client, schema, table, deserializedRow, columns, pkColumns);
|
|
88
|
+
summary.applied.inserts++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// abort mode: let errors propagate directly
|
|
94
|
+
try {
|
|
95
|
+
if (options.insertMode === "upsert") {
|
|
96
|
+
await (0, sql_1.upsertRow)(client, schema, table, deserializedRow, columns, pkColumns);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// strict: will throw if PK already exists
|
|
100
|
+
await (0, sql_1.insertRow)(client, schema, table, deserializedRow, columns);
|
|
101
|
+
}
|
|
102
|
+
summary.applied.inserts++;
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
106
|
+
throw new ConflictError(`Insert conflict in ${schema}.${table} pk=${JSON.stringify(pkValues)}: ${reason}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function applyUpdate(client, schema, table, pkColumns, record, options, summary, columnTypes = {}) {
|
|
111
|
+
const pkValues = {};
|
|
112
|
+
for (const col of pkColumns) {
|
|
113
|
+
pkValues[col] = (0, serialize_value_1.deserializeValue)(record.pk[col]);
|
|
114
|
+
}
|
|
115
|
+
const setColumns = Object.keys(record.changes);
|
|
116
|
+
const setValues = {};
|
|
117
|
+
for (const col of setColumns) {
|
|
118
|
+
setValues[col] = (0, serialize_value_1.deserializeValue)(record.changes[col].to);
|
|
119
|
+
}
|
|
120
|
+
if (options.verbose) {
|
|
121
|
+
console.log(` UPDATE ${schema}.${table} pk=${JSON.stringify(pkValues)} columns=[${setColumns.join(", ")}]`);
|
|
122
|
+
}
|
|
123
|
+
if (options.dryRun) {
|
|
124
|
+
summary.applied.updates++;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const guardCols = [];
|
|
128
|
+
const guardVals = {};
|
|
129
|
+
if (options.conflictMode !== "overwrite") {
|
|
130
|
+
for (const [col, value] of Object.entries(record.guard)) {
|
|
131
|
+
if (!pkColumns.includes(col)) {
|
|
132
|
+
guardCols.push(col);
|
|
133
|
+
guardVals[col] = (0, serialize_value_1.deserializeValue)(value);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const rowsUpdated = await (0, sql_1.updateRow)(client, schema, table, pkColumns, pkValues, setColumns, setValues, guardCols, guardVals, columnTypes);
|
|
138
|
+
if (rowsUpdated === 0) {
|
|
139
|
+
// Either PK not found, or guard failed
|
|
140
|
+
const reason = "Row not found or guard check failed (destination row may have changed)";
|
|
141
|
+
if (options.conflictMode === "abort") {
|
|
142
|
+
throw new ConflictError(`Update conflict in ${schema}.${table} pk=${JSON.stringify(pkValues)}: ${reason}`);
|
|
143
|
+
}
|
|
144
|
+
else if (options.conflictMode === "skip") {
|
|
145
|
+
summary.skipped.push({
|
|
146
|
+
table: `${schema}.${table}`,
|
|
147
|
+
operation: "update",
|
|
148
|
+
pk: pkValues,
|
|
149
|
+
reason,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
summary.applied.updates++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function applyDelete(client, schema, table, pkColumns, record, options, summary, columnTypes = {}) {
|
|
158
|
+
const pkValues = {};
|
|
159
|
+
for (const col of pkColumns) {
|
|
160
|
+
pkValues[col] = (0, serialize_value_1.deserializeValue)(record.pk[col]);
|
|
161
|
+
}
|
|
162
|
+
// Build guard from all guard columns (not just pk)
|
|
163
|
+
const guardColumns = Object.keys(record.guard).filter((c) => !pkColumns.includes(c));
|
|
164
|
+
const guardVals = {};
|
|
165
|
+
for (const col of guardColumns) {
|
|
166
|
+
guardVals[col] = (0, serialize_value_1.deserializeValue)(record.guard[col]);
|
|
167
|
+
}
|
|
168
|
+
if (options.verbose) {
|
|
169
|
+
console.log(` DELETE from ${schema}.${table} pk=${JSON.stringify(pkValues)}`);
|
|
170
|
+
}
|
|
171
|
+
if (options.dryRun) {
|
|
172
|
+
summary.applied.deletes++;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Always use guarded delete (abort/skip modes check guards)
|
|
176
|
+
const rowsDeleted = await (0, sql_1.deleteRow)(client, schema, table, pkColumns, pkValues, guardColumns, guardVals, columnTypes);
|
|
177
|
+
if (rowsDeleted === 0) {
|
|
178
|
+
const reason = "Row not found or guard check failed (destination row may have changed)";
|
|
179
|
+
if (options.conflictMode === "abort") {
|
|
180
|
+
throw new ConflictError(`Delete conflict in ${schema}.${table} pk=${JSON.stringify(pkValues)}: ${reason}`);
|
|
181
|
+
}
|
|
182
|
+
else if (options.conflictMode === "skip") {
|
|
183
|
+
summary.skipped.push({
|
|
184
|
+
table: `${schema}.${table}`,
|
|
185
|
+
operation: "delete",
|
|
186
|
+
pk: pkValues,
|
|
187
|
+
reason,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
summary.applied.deletes++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Thrown when a conflict is detected in abort mode.
|
|
197
|
+
*/
|
|
198
|
+
class ConflictError extends Error {
|
|
199
|
+
constructor(message) {
|
|
200
|
+
super(message);
|
|
201
|
+
this.name = "ConflictError";
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
exports.ConflictError = ConflictError;
|
|
205
|
+
//# sourceMappingURL=apply-diff.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict handling documentation and utilities.
|
|
3
|
+
*
|
|
4
|
+
* Conflict modes:
|
|
5
|
+
*
|
|
6
|
+
* abort - (default) Abort the transaction on the first conflict.
|
|
7
|
+
* The destination database is left unchanged (if transaction=true).
|
|
8
|
+
*
|
|
9
|
+
* skip - Skip the conflicting row and continue applying other rows.
|
|
10
|
+
* Skipped rows are recorded in the summary.
|
|
11
|
+
*
|
|
12
|
+
* overwrite - Ignore the "from" guard for updates and force the "to" value.
|
|
13
|
+
* For inserts, behaves like upsert.
|
|
14
|
+
* Note: overwrite does NOT disable guarded deletes.
|
|
15
|
+
* Deletes still check the guard even in overwrite mode
|
|
16
|
+
* to prevent accidental data loss.
|
|
17
|
+
*/
|
|
18
|
+
export type ConflictMode = "abort" | "skip" | "overwrite";
|
|
19
|
+
export declare function describeConflictMode(mode: ConflictMode): string;
|
|
20
|
+
//# sourceMappingURL=conflict.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.describeConflictMode = describeConflictMode;
|
|
4
|
+
function describeConflictMode(mode) {
|
|
5
|
+
switch (mode) {
|
|
6
|
+
case "abort":
|
|
7
|
+
return "abort: Roll back transaction on first conflict (default, safest)";
|
|
8
|
+
case "skip":
|
|
9
|
+
return "skip: Skip conflicting rows and continue";
|
|
10
|
+
case "overwrite":
|
|
11
|
+
return "overwrite: Force apply changes, ignoring from-value guards for updates";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=conflict.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Pool } from "pg";
|
|
2
|
+
import { type DiffJson } from "../diff/diff-schema";
|
|
3
|
+
import { type ApplyTableOptions } from "./apply-diff";
|
|
4
|
+
import { type ApplySummary } from "../shared/summary";
|
|
5
|
+
export interface RunApplyOptions extends ApplyTableOptions {
|
|
6
|
+
transaction: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Applies a full diff to the destination database.
|
|
10
|
+
* Handles transactions, conflict modes, and dry-run behavior.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runApply(destPool: Pool, diff: DiffJson, options: RunApplyOptions): Promise<ApplySummary>;
|
|
13
|
+
//# sourceMappingURL=plan.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runApply = runApply;
|
|
4
|
+
const apply_diff_1 = require("./apply-diff");
|
|
5
|
+
const summary_1 = require("../shared/summary");
|
|
6
|
+
/**
|
|
7
|
+
* Applies a full diff to the destination database.
|
|
8
|
+
* Handles transactions, conflict modes, and dry-run behavior.
|
|
9
|
+
*/
|
|
10
|
+
async function runApply(destPool, diff, options) {
|
|
11
|
+
const summary = (0, summary_1.createEmptySummary)();
|
|
12
|
+
const client = await destPool.connect();
|
|
13
|
+
try {
|
|
14
|
+
if (options.transaction && !options.dryRun) {
|
|
15
|
+
await client.query("BEGIN");
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
for (const tableDiff of diff.tables) {
|
|
19
|
+
await (0, apply_diff_1.applyTableDiff)(client, tableDiff, options, summary);
|
|
20
|
+
}
|
|
21
|
+
if (options.transaction && !options.dryRun) {
|
|
22
|
+
await client.query("COMMIT");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
if (options.transaction && !options.dryRun) {
|
|
27
|
+
await client.query("ROLLBACK");
|
|
28
|
+
}
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
client.release();
|
|
34
|
+
}
|
|
35
|
+
return summary;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=plan.js.map
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* frg-data-diff apply
|
|
5
|
+
*
|
|
6
|
+
* Reads a JSON diff file and safely applies it to a destination PostgreSQL database.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
const commander_1 = require("commander");
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const load_config_1 = require("../config/load-config");
|
|
46
|
+
const resolve_options_1 = require("../config/resolve-options");
|
|
47
|
+
const connection_1 = require("../db/connection");
|
|
48
|
+
const diff_schema_1 = require("../diff/diff-schema");
|
|
49
|
+
const plan_1 = require("../apply/plan");
|
|
50
|
+
const summary_1 = require("../shared/summary");
|
|
51
|
+
const prompts_1 = require("../shared/prompts");
|
|
52
|
+
const env_values_1 = require("../shared/env-values");
|
|
53
|
+
const program = new commander_1.Command();
|
|
54
|
+
program
|
|
55
|
+
.name("frg-data-diff apply")
|
|
56
|
+
.description("Reads a JSON diff file and safely applies it to a destination PostgreSQL database.")
|
|
57
|
+
.option("--dest-pg-host <host>", "Destination database host")
|
|
58
|
+
.option("--dest-pg-port <port>", "Destination database port", (v) => parseInt(v, 10))
|
|
59
|
+
.option("--dest-pg-database <db>", "Destination database name")
|
|
60
|
+
.option("--dest-pg-user <user>", "Destination database user")
|
|
61
|
+
.option("--dest-pg-password-env <value>", "Destination DB password or $ENV_VAR reference")
|
|
62
|
+
.option("--dest-pg-ssl", "Use SSL for destination database connection")
|
|
63
|
+
.option("--no-dest-pg-ssl", "Do not use SSL for destination database connection")
|
|
64
|
+
.option("--input <file>", "Input diff file path", "frg-data-diff.json")
|
|
65
|
+
.option("--dry-run", "Simulate the apply without making any changes (default)")
|
|
66
|
+
.option("--execute", "Apply changes to the destination database (required to mutate)")
|
|
67
|
+
.option("--apply-inserts", "Apply inserts (default: true)")
|
|
68
|
+
.option("--no-apply-inserts", "Do not apply inserts")
|
|
69
|
+
.option("--apply-updates", "Apply updates (default: true)")
|
|
70
|
+
.option("--no-apply-updates", "Do not apply updates")
|
|
71
|
+
.option("--apply-deletes", "Apply deletes (default: false, requires explicit opt-in)")
|
|
72
|
+
.option("--no-apply-deletes", "Do not apply deletes (default)")
|
|
73
|
+
.option("--conflict-mode <mode>", "Conflict mode: abort, skip, or overwrite", "abort")
|
|
74
|
+
.option("--insert-mode <mode>", "Insert mode: strict or upsert", "strict")
|
|
75
|
+
.option("--transaction", "Wrap all changes in a single transaction (default: true)")
|
|
76
|
+
.option("--no-transaction", "Do not wrap changes in a transaction")
|
|
77
|
+
.option("--verbose", "Enable verbose logging")
|
|
78
|
+
.option("--config <file>", "Path to config file", load_config_1.DEFAULT_CONFIG_FILENAME)
|
|
79
|
+
.option("--yes", "Skip interactive confirmation (for CI/CD)");
|
|
80
|
+
program.parse(process.argv);
|
|
81
|
+
const opts = program.opts();
|
|
82
|
+
async function main() {
|
|
83
|
+
// Validate conflicting flags
|
|
84
|
+
if (opts["dryRun"] && opts["execute"]) {
|
|
85
|
+
console.error("Error: --dry-run and --execute cannot both be specified.");
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const configFilePath = path.resolve(opts["config"] || load_config_1.DEFAULT_CONFIG_FILENAME);
|
|
89
|
+
const configExists = fs.existsSync(configFilePath);
|
|
90
|
+
let applyConfig;
|
|
91
|
+
if (configExists) {
|
|
92
|
+
const config = (0, load_config_1.loadConfig)(configFilePath);
|
|
93
|
+
applyConfig = config.apply;
|
|
94
|
+
}
|
|
95
|
+
// Determine dryRun: --execute overrides config/default dryRun
|
|
96
|
+
const dryRunFromCli = resolveDryRunFromCli(opts["execute"], opts["dryRun"]);
|
|
97
|
+
// Build resolved options from CLI args + config
|
|
98
|
+
const cliArgs = {
|
|
99
|
+
destPgHost: opts["destPgHost"],
|
|
100
|
+
destPgPort: opts["destPgPort"],
|
|
101
|
+
destPgDatabase: opts["destPgDatabase"],
|
|
102
|
+
destPgUser: opts["destPgUser"],
|
|
103
|
+
destPgPassword: opts["destPgPasswordEnv"],
|
|
104
|
+
destPgSsl: normalizeOptionalBoolean(opts["destPgSsl"]),
|
|
105
|
+
input: opts["input"],
|
|
106
|
+
dryRun: dryRunFromCli,
|
|
107
|
+
applyInserts: normalizeOptionalBoolean(opts["applyInserts"]),
|
|
108
|
+
applyUpdates: normalizeOptionalBoolean(opts["applyUpdates"]),
|
|
109
|
+
applyDeletes: normalizeOptionalBoolean(opts["applyDeletes"]),
|
|
110
|
+
conflictMode: opts["conflictMode"],
|
|
111
|
+
insertMode: opts["insertMode"],
|
|
112
|
+
transaction: normalizeOptionalBoolean(opts["transaction"]),
|
|
113
|
+
verbose: opts["verbose"] ? true : undefined,
|
|
114
|
+
};
|
|
115
|
+
// Remove undefined values
|
|
116
|
+
const cleanCliArgs = Object.fromEntries(Object.entries(cliArgs).filter(([, v]) => v !== undefined));
|
|
117
|
+
const resolved = (0, resolve_options_1.resolveApplyOptions)(applyConfig, cleanCliArgs);
|
|
118
|
+
const runtimeResolved = (0, resolve_options_1.resolveRuntimeApplyOptions)(resolved);
|
|
119
|
+
// Validate required values
|
|
120
|
+
if (!resolved.destPgHost ||
|
|
121
|
+
!resolved.destPgDatabase ||
|
|
122
|
+
!resolved.destPgUser ||
|
|
123
|
+
!resolved.destPgPassword) {
|
|
124
|
+
console.error("Error: Missing required destination database connection options.");
|
|
125
|
+
console.error("Provide --dest-pg-host, --dest-pg-database, --dest-pg-user, --dest-pg-password-env");
|
|
126
|
+
console.error("or configure them in .frg-data-diff.config.json");
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
if (!runtimeResolved.input) {
|
|
130
|
+
console.error("Error: Missing required --input diff file path.");
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
const inputPath = path.resolve(runtimeResolved.input);
|
|
134
|
+
if (!fs.existsSync(inputPath)) {
|
|
135
|
+
console.error(`Error: Diff file not found: ${inputPath}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
// Validate conflict mode
|
|
139
|
+
const destConnection = (0, connection_1.resolveConnectionParams)({
|
|
140
|
+
host: resolved.destPgHost,
|
|
141
|
+
port: resolved.destPgPort,
|
|
142
|
+
database: resolved.destPgDatabase,
|
|
143
|
+
user: resolved.destPgUser,
|
|
144
|
+
password: resolved.destPgPassword,
|
|
145
|
+
ssl: runtimeResolved.destPgSsl,
|
|
146
|
+
}, {
|
|
147
|
+
host: "destination host",
|
|
148
|
+
port: "destination port",
|
|
149
|
+
database: "destination database",
|
|
150
|
+
user: "destination user",
|
|
151
|
+
password: "destination password",
|
|
152
|
+
});
|
|
153
|
+
// Print resolved plan
|
|
154
|
+
printResolvedApplyPlan(resolved, runtimeResolved, inputPath, destConnection);
|
|
155
|
+
// Ask for confirmation if no --yes flag
|
|
156
|
+
if (!opts["yes"]) {
|
|
157
|
+
const proceed = await (0, prompts_1.confirmProceed)();
|
|
158
|
+
if (!proceed) {
|
|
159
|
+
console.log("Aborted.");
|
|
160
|
+
process.exit(0);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Load and validate the diff file
|
|
164
|
+
let rawDiff;
|
|
165
|
+
try {
|
|
166
|
+
const content = fs.readFileSync(inputPath, "utf-8");
|
|
167
|
+
rawDiff = JSON.parse(content);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
console.error(`Error reading diff file: ${inputPath}`);
|
|
171
|
+
if (err instanceof Error)
|
|
172
|
+
console.error(err.message);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
let diff;
|
|
176
|
+
try {
|
|
177
|
+
diff = (0, diff_schema_1.validateDiffJson)(rawDiff);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
console.error("Diff file validation failed:");
|
|
181
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
if (runtimeResolved.dryRun) {
|
|
185
|
+
console.log("\n[DRY RUN] No changes will be made to the destination database.");
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
console.log("\nApplying changes to destination database...");
|
|
189
|
+
}
|
|
190
|
+
const destPool = (0, connection_1.createPool)(destConnection);
|
|
191
|
+
try {
|
|
192
|
+
const summary = await (0, plan_1.runApply)(destPool, diff, {
|
|
193
|
+
dryRun: runtimeResolved.dryRun,
|
|
194
|
+
applyInserts: runtimeResolved.applyInserts,
|
|
195
|
+
applyUpdates: runtimeResolved.applyUpdates,
|
|
196
|
+
applyDeletes: runtimeResolved.applyDeletes,
|
|
197
|
+
conflictMode: runtimeResolved.conflictMode,
|
|
198
|
+
insertMode: runtimeResolved.insertMode,
|
|
199
|
+
transaction: runtimeResolved.transaction,
|
|
200
|
+
verbose: resolved.verbose,
|
|
201
|
+
});
|
|
202
|
+
(0, summary_1.printSummary)(summary, runtimeResolved.dryRun);
|
|
203
|
+
// Output machine-readable summary
|
|
204
|
+
console.log("\nMachine-readable summary:");
|
|
205
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
await destPool.end();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function printResolvedApplyPlan(resolved, runtimeResolved, inputPath, destConnection) {
|
|
212
|
+
const resolvedDest = destConnection ??
|
|
213
|
+
(0, connection_1.resolveConnectionParams)({
|
|
214
|
+
host: resolved.destPgHost,
|
|
215
|
+
port: resolved.destPgPort,
|
|
216
|
+
database: resolved.destPgDatabase,
|
|
217
|
+
user: resolved.destPgUser,
|
|
218
|
+
password: resolved.destPgPassword,
|
|
219
|
+
ssl: runtimeResolved.destPgSsl,
|
|
220
|
+
}, {
|
|
221
|
+
host: "destination host",
|
|
222
|
+
port: "destination port",
|
|
223
|
+
database: "destination database",
|
|
224
|
+
user: "destination user",
|
|
225
|
+
password: "destination password",
|
|
226
|
+
});
|
|
227
|
+
console.log("\ntool:");
|
|
228
|
+
console.log(" frg-data-diff apply");
|
|
229
|
+
console.log("\ndest:");
|
|
230
|
+
console.log(` host: ${(0, env_values_1.formatVisibleValue)(resolved.destPgHost, resolvedDest.host)}`);
|
|
231
|
+
console.log(` port: ${typeof resolved.destPgPort === "string" ? `${resolved.destPgPort} -> ${resolvedDest.port}` : resolvedDest.port}`);
|
|
232
|
+
console.log(` database: ${(0, env_values_1.formatVisibleValue)(resolved.destPgDatabase, resolvedDest.database)}`);
|
|
233
|
+
console.log(` user: ${(0, env_values_1.formatVisibleValue)(resolved.destPgUser, resolvedDest.user)}`);
|
|
234
|
+
console.log(` password: ${(0, env_values_1.formatSecretValue)(String(resolved.destPgPassword))}`);
|
|
235
|
+
console.log(` ssl: ${runtimeResolved.destPgSsl}`);
|
|
236
|
+
console.log(`\ninput: ${inputPath}`);
|
|
237
|
+
console.log(`dry-run: ${runtimeResolved.dryRun}`);
|
|
238
|
+
console.log(`apply inserts: ${runtimeResolved.applyInserts}`);
|
|
239
|
+
console.log(`apply updates: ${runtimeResolved.applyUpdates}`);
|
|
240
|
+
console.log(`apply deletes: ${runtimeResolved.applyDeletes}`);
|
|
241
|
+
console.log(`conflict mode: ${runtimeResolved.conflictMode}`);
|
|
242
|
+
console.log(`insert mode: ${runtimeResolved.insertMode}`);
|
|
243
|
+
console.log(`transaction: ${runtimeResolved.transaction}`);
|
|
244
|
+
if (runtimeResolved.applyDeletes) {
|
|
245
|
+
console.log("\nWarning: applyDeletes is enabled. Rows may be deleted from the destination database.");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
main().catch((err) => {
|
|
249
|
+
console.error("Fatal error:", err instanceof Error ? err.message : String(err));
|
|
250
|
+
process.exit(1);
|
|
251
|
+
});
|
|
252
|
+
function normalizeOptionalBoolean(value) {
|
|
253
|
+
if (value === true) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
if (value === false) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
function resolveDryRunFromCli(execute, dryRun) {
|
|
262
|
+
if (execute === true) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
if (dryRun === true) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
//# sourceMappingURL=apply.js.map
|