@voltro/data-transfer 0.84.0 → 0.86.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/CHANGELOG.md +1215 -0
- package/THIRD-PARTY-NOTICES.md +91 -365
- package/dist/assets.js +47 -0
- package/dist/blobStorage.js +40 -0
- package/dist/bundleArchive.js +165 -0
- package/dist/bundleCrypto.js +106 -0
- package/dist/casAssetPhase.js +43 -0
- package/dist/compress.js +6 -0
- package/dist/consistency.js +14 -0
- package/dist/crossDialect.js +58 -0
- package/dist/driftClassification.js +58 -0
- package/dist/environmentLocal.js +122 -0
- package/dist/errors.js +82 -0
- package/dist/exporter.js +257 -0
- package/dist/importer.js +536 -0
- package/dist/index.d.ts +118 -121
- package/dist/index.js +28 -2578
- package/dist/ledger.js +21 -0
- package/dist/liveGuard.js +11 -0
- package/dist/manifest.js +52 -0
- package/dist/masking.js +246 -0
- package/dist/maskingPreview.js +48 -0
- package/dist/nativeDump.js +369 -0
- package/dist/ndjson.js +13 -0
- package/dist/profile.js +4 -0
- package/dist/progress.js +17 -0
- package/dist/readme.js +43 -0
- package/dist/replacePreflight.js +23 -0
- package/dist/rollbackCapture.js +36 -0
- package/dist/rowFailureReason.js +36 -0
- package/dist/schemaDrift.js +40 -0
- package/dist/scope.js +40 -0
- package/dist/stagedSwap.js +164 -0
- package/dist/subset.js +90 -0
- package/dist/topo.js +24 -0
- package/dist/typedCodec.js +36 -0
- package/package.json +4 -5
package/dist/errors.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Data as e, Schema as t } from "effect";
|
|
2
|
+
//#region src/errors.ts
|
|
3
|
+
var n = class extends t.TaggedError()("BundleError", {
|
|
4
|
+
reason: t.String,
|
|
5
|
+
path: t.optional(t.String)
|
|
6
|
+
}) {
|
|
7
|
+
get message() {
|
|
8
|
+
return this.path === void 0 ? this.reason : `${this.reason} (${this.path})`;
|
|
9
|
+
}
|
|
10
|
+
}, r = class extends t.TaggedError()("CodecError", {
|
|
11
|
+
table: t.String,
|
|
12
|
+
reason: t.String
|
|
13
|
+
}) {}, i = class extends t.TaggedError()("IntegrityError", {
|
|
14
|
+
what: t.String,
|
|
15
|
+
expected: t.String,
|
|
16
|
+
actual: t.String
|
|
17
|
+
}) {
|
|
18
|
+
get message() {
|
|
19
|
+
return `${this.what}: expected ${this.expected}, got ${this.actual}`;
|
|
20
|
+
}
|
|
21
|
+
}, a = class extends e.TaggedError("CompressionError") {}, o = class extends t.TaggedError()("ScopeError", { reason: t.String }) {
|
|
22
|
+
get message() {
|
|
23
|
+
return this.reason;
|
|
24
|
+
}
|
|
25
|
+
}, s = class extends t.TaggedError()("RowsRefusedError", {
|
|
26
|
+
rows: t.Array(t.Struct({
|
|
27
|
+
table: t.String,
|
|
28
|
+
id: t.String,
|
|
29
|
+
reason: t.String,
|
|
30
|
+
derived: t.Boolean
|
|
31
|
+
})),
|
|
32
|
+
primaryCount: t.Number,
|
|
33
|
+
totalCount: t.Number,
|
|
34
|
+
byTable: t.Array(t.Struct({
|
|
35
|
+
table: t.String,
|
|
36
|
+
refused: t.Number,
|
|
37
|
+
primary: t.Number
|
|
38
|
+
}))
|
|
39
|
+
}) {
|
|
40
|
+
get message() {
|
|
41
|
+
return `RowsRefusedError: ${JSON.stringify({
|
|
42
|
+
rows: this.rows,
|
|
43
|
+
primaryCount: this.primaryCount,
|
|
44
|
+
totalCount: this.totalCount,
|
|
45
|
+
byTable: this.byTable
|
|
46
|
+
})}`;
|
|
47
|
+
}
|
|
48
|
+
}, c = class extends t.TaggedError()("SchemaDriftError", {
|
|
49
|
+
bundleFingerprint: t.String,
|
|
50
|
+
targetFingerprint: t.String,
|
|
51
|
+
diff: t.Array(t.String)
|
|
52
|
+
}) {
|
|
53
|
+
get message() {
|
|
54
|
+
return `bundle schema ${this.bundleFingerprint} does not match target ${this.targetFingerprint}: ${this.diff.join("; ")}`;
|
|
55
|
+
}
|
|
56
|
+
}, l = class extends e.TaggedError("NativeToolError") {
|
|
57
|
+
get message() {
|
|
58
|
+
let e = this.stderr?.trim(), t = this.hint?.trim();
|
|
59
|
+
return `${this.tool}: ${this.reason}${e ? `\n${e}` : ""}${t ? `\n\nhint: ${t}` : ""}`;
|
|
60
|
+
}
|
|
61
|
+
}, u = class extends t.TaggedError()("MaskingError", {
|
|
62
|
+
unclassified: t.Array(t.String),
|
|
63
|
+
invalidActions: t.optional(t.Array(t.String))
|
|
64
|
+
}) {
|
|
65
|
+
get message() {
|
|
66
|
+
let e = [];
|
|
67
|
+
return this.unclassified.length > 0 && e.push(`unclassified columns: ${this.unclassified.join(", ")}`), this.invalidActions !== void 0 && this.invalidActions.length > 0 && e.push(`invalidActions: ${this.invalidActions.join(", ")}`), `masking refused — ${e.join("; ") || "no column may be exported"}`;
|
|
68
|
+
}
|
|
69
|
+
}, d = class extends t.TaggedError()("ImportModeError", {
|
|
70
|
+
mode: t.String,
|
|
71
|
+
reason: t.String
|
|
72
|
+
}) {
|
|
73
|
+
get message() {
|
|
74
|
+
return `import mode '${this.mode}' refused: ${this.reason}`;
|
|
75
|
+
}
|
|
76
|
+
}, f = class extends t.TaggedError()("CrossDialectError", {
|
|
77
|
+
sourceDialect: t.String,
|
|
78
|
+
targetDialect: t.String,
|
|
79
|
+
blocking: t.Array(t.String)
|
|
80
|
+
}) {};
|
|
81
|
+
//#endregion
|
|
82
|
+
export { n as BundleError, r as CodecError, a as CompressionError, f as CrossDialectError, d as ImportModeError, i as IntegrityError, u as MaskingError, l as NativeToolError, s as RowsRefusedError, c as SchemaDriftError, o as ScopeError };
|
package/dist/exporter.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { snapshotPrelude as e } from "./consistency.js";
|
|
2
|
+
import { classificationFromSnapshot as t, maskRow as n, planMasking as r } from "./masking.js";
|
|
3
|
+
import { BundleError as i, MaskingError as a, ScopeError as o } from "./errors.js";
|
|
4
|
+
import { computeSubsetIds as s, subsetToScope as c } from "./subset.js";
|
|
5
|
+
import { resolveScope as l } from "./scope.js";
|
|
6
|
+
import { captureAssetsToCas as u } from "./casAssetPhase.js";
|
|
7
|
+
import { compressionExt as d, makeCompressor as f, pickCompression as p } from "./compress.js";
|
|
8
|
+
import { ASSETS_DIR as m, ASSET_INDEX_FILE as h, DATA_DIR as g, LEDGER_FILE as _, MANIFEST_FILE as v, README_FILE as y, encodeManifest as b } from "./manifest.js";
|
|
9
|
+
import { buildReadme as x } from "./readme.js";
|
|
10
|
+
import { reportProgress as S, withTableSpan as C } from "./progress.js";
|
|
11
|
+
import { readLedger as w, tableDone as T, writeLedger as E } from "./ledger.js";
|
|
12
|
+
import { rowsToNdjson as D } from "./ndjson.js";
|
|
13
|
+
import { topoSortTables as O } from "./topo.js";
|
|
14
|
+
import { Cause as k, Effect as A, Exit as j, Schedule as M, Stream as N } from "effect";
|
|
15
|
+
import { streamTable as P } from "@voltro/database";
|
|
16
|
+
import { fingerprintSchema as F } from "@voltro/database/sql";
|
|
17
|
+
//#region src/exporter.ts
|
|
18
|
+
var I = (e) => {
|
|
19
|
+
let t = e, n = [];
|
|
20
|
+
typeof t?._tag == "string" && t._tag !== "" && n.push(t._tag);
|
|
21
|
+
let r = typeof t?.message == "string" ? t.message.trim() : "";
|
|
22
|
+
if (r === "" ? t?.cause !== void 0 && t.cause !== null && n.push(I(t.cause)) : n.push(r), n.length === 0) {
|
|
23
|
+
let t = String(e ?? "").trim();
|
|
24
|
+
return t === "" || t === "[object Object]" ? L(e) : t;
|
|
25
|
+
}
|
|
26
|
+
return n.join(": ");
|
|
27
|
+
}, L = (e) => {
|
|
28
|
+
try {
|
|
29
|
+
let t = JSON.stringify(e);
|
|
30
|
+
return t === void 0 || t === "{}" ? "unknown error (no message, cause or tag)" : t;
|
|
31
|
+
} catch {
|
|
32
|
+
return "unknown error (not serialisable)";
|
|
33
|
+
}
|
|
34
|
+
}, R = (e, t) => {
|
|
35
|
+
let n = e.tables.find((e) => e.name === t), r = {};
|
|
36
|
+
for (let e of n?.columns ?? []) e.generatedAs === void 0 && (r[e.name] = e.type);
|
|
37
|
+
return r;
|
|
38
|
+
}, z = (e, t) => (e.tables.find((e) => e.name === t)?.columns ?? []).filter((e) => e.generatedAs !== void 0).map((e) => e.name), B = (e, t) => {
|
|
39
|
+
if (t.length === 0) return e;
|
|
40
|
+
let n = { ...e };
|
|
41
|
+
for (let e of t) delete n[e];
|
|
42
|
+
return n;
|
|
43
|
+
}, V = (e, t) => {
|
|
44
|
+
let n = e.tables.find((e) => e.name === t);
|
|
45
|
+
if (n === void 0) return { refusal: `${t}: not in the schema snapshot, so its primary key is unknown.` };
|
|
46
|
+
let r = n.columns.find((e) => e.type === "id");
|
|
47
|
+
if (r !== void 0) return { column: r.name };
|
|
48
|
+
let i = n.indexes.find((e) => e.name === `${t}_pkey`)?.columns ?? n.primaryKey ?? [];
|
|
49
|
+
return i.length === 1 ? { column: i[0] } : i.length > 1 ? { refusal: `${t}: composite primary key (${i.join(", ")}). Keyset export orders by ONE column; ordering by the first alone would split equal values across pages and drop or duplicate rows. Exclude this table from the scope, or give it a single-column key.` } : { refusal: `${t}: no primary key, so there is no stable order to paginate by. A keyset export would return rows in an order the database may change between pages. Exclude this table from the scope.` };
|
|
50
|
+
}, H = (e, t, n) => A.tryPromise({
|
|
51
|
+
try: async () => {
|
|
52
|
+
let { createWriteStream: r } = await import("node:fs"), i = await import("node:fs/promises"), { createHash: a, randomUUID: o } = await import("node:crypto"), { pipeline: s } = await import("node:stream/promises"), { Readable: c, Transform: l } = await import("node:stream"), u = a("sha256"), d = 0, p = new l({ transform(e, t, n) {
|
|
53
|
+
u.update(e);
|
|
54
|
+
let r = e;
|
|
55
|
+
for (let e = 0; e < r.length; e++) r[e] === 10 && d++;
|
|
56
|
+
n(null, e);
|
|
57
|
+
} }), m = c.fromWeb(N.toReadableStream(e)), h = `${t}.tmp-${o()}`;
|
|
58
|
+
try {
|
|
59
|
+
await s(m, p, f(n), r(h)), await i.rename(h, t);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
throw await i.rm(h, { force: !0 }).catch(() => {}), e;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
rowCount: d,
|
|
65
|
+
checksum: u.digest("hex")
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
catch: (e) => new i({
|
|
69
|
+
reason: `write table failed: ${I(e)}`,
|
|
70
|
+
path: t
|
|
71
|
+
})
|
|
72
|
+
}), U = (e, t) => A.tryPromise({
|
|
73
|
+
try: async () => {
|
|
74
|
+
await (await import("node:fs/promises")).appendFile(e, `${t}\n`);
|
|
75
|
+
},
|
|
76
|
+
catch: (t) => new i({
|
|
77
|
+
reason: `append failed: ${I(t)}`,
|
|
78
|
+
path: e
|
|
79
|
+
})
|
|
80
|
+
}), W = (e, t) => A.gen(function* () {
|
|
81
|
+
let r = [], i = t.ordered.length;
|
|
82
|
+
for (let a = 0; a < t.ordered.length; a++) {
|
|
83
|
+
let s = t.ordered[a], c = R(t.snapshot, s), l = V(t.snapshot, s);
|
|
84
|
+
if ("refusal" in l) return yield* A.fail(new o({ reason: `export: ${l.refusal}` }));
|
|
85
|
+
let u = l.column, f = `${g}/${s}.ndjson${d(t.compression)}`;
|
|
86
|
+
if (T(t.ledger, s)) {
|
|
87
|
+
let e = t.ledger.tables[s];
|
|
88
|
+
S(t.onProgress, {
|
|
89
|
+
phase: "export",
|
|
90
|
+
table: s,
|
|
91
|
+
index: a,
|
|
92
|
+
tableCount: i,
|
|
93
|
+
event: "done",
|
|
94
|
+
rowsDone: e.rowCount,
|
|
95
|
+
total: e.rowCount
|
|
96
|
+
}), r.push({
|
|
97
|
+
name: s,
|
|
98
|
+
file: f,
|
|
99
|
+
rowCount: e.rowCount,
|
|
100
|
+
checksum: e.checksum,
|
|
101
|
+
columnTypes: c,
|
|
102
|
+
primaryKey: u
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
S(t.onProgress, {
|
|
107
|
+
phase: "export",
|
|
108
|
+
table: s,
|
|
109
|
+
index: a,
|
|
110
|
+
tableCount: i,
|
|
111
|
+
event: "start",
|
|
112
|
+
rowsDone: 0
|
|
113
|
+
});
|
|
114
|
+
let p = t.resolved.predicateFor(s), m = P(e, {
|
|
115
|
+
table: s,
|
|
116
|
+
pk: u,
|
|
117
|
+
chunkSize: t.chunkSize,
|
|
118
|
+
retry: t.retry,
|
|
119
|
+
...p === void 0 ? {} : { where: p }
|
|
120
|
+
}), h = z(t.snapshot, s), _ = h.length === 0 ? m : m.pipe(N.map((e) => B(e, h))), v = t.mask ? _.pipe(N.map((e) => n(e, s, t.mask.actions, t.mask.seed, c))) : _, { rowCount: y, checksum: b } = yield* C("export", s, a, i, (e) => e.rowCount, H(D(v, c), t.bundleFile(f), t.compression));
|
|
121
|
+
t.ledger.tables[s] = {
|
|
122
|
+
rowCount: y,
|
|
123
|
+
checksum: b
|
|
124
|
+
}, yield* E(t.ledgerPath, t.ledger), S(t.onProgress, {
|
|
125
|
+
phase: "export",
|
|
126
|
+
table: s,
|
|
127
|
+
index: a,
|
|
128
|
+
tableCount: i,
|
|
129
|
+
event: "done",
|
|
130
|
+
rowsDone: y,
|
|
131
|
+
total: y
|
|
132
|
+
}), r.push({
|
|
133
|
+
name: s,
|
|
134
|
+
file: f,
|
|
135
|
+
rowCount: y,
|
|
136
|
+
checksum: b,
|
|
137
|
+
columnTypes: c,
|
|
138
|
+
primaryKey: u
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return r;
|
|
142
|
+
}), G = (e) => ({
|
|
143
|
+
_tag: "RawSqlFragment",
|
|
144
|
+
strings: [e],
|
|
145
|
+
values: []
|
|
146
|
+
}), K = (t, n, r) => A.tryPromise({
|
|
147
|
+
try: () => t.transactional(async (t) => {
|
|
148
|
+
if (t.raw) for (let r of e(n)) await t.raw(G(r));
|
|
149
|
+
let i = await A.runPromiseExit(r(t));
|
|
150
|
+
if (j.isFailure(i)) throw Error(k.pretty(i.cause));
|
|
151
|
+
return i.value;
|
|
152
|
+
}),
|
|
153
|
+
catch: (e) => e instanceof i ? e : new i({ reason: `snapshot export failed: ${I(e)}` })
|
|
154
|
+
}), q = (e) => A.gen(function* () {
|
|
155
|
+
let n = e.scope ?? { kind: "all" }, o = p(e.compression), d = e.chunkSize ?? 1e3, f = e.retryTimes ?? 5, S = M.max([M.exponential("50 millis").pipe(M.jittered), M.recurs(f)]), C = yield* A.promise(() => import("node:path")), T = (t) => C.join(e.outDir, t);
|
|
156
|
+
yield* A.tryPromise({
|
|
157
|
+
try: async () => {
|
|
158
|
+
let t = await import("node:fs/promises");
|
|
159
|
+
await t.mkdir(T(g), { recursive: !0 }), e.assets && await t.mkdir(T(m), { recursive: !0 });
|
|
160
|
+
},
|
|
161
|
+
catch: (t) => new i({
|
|
162
|
+
reason: `mkdir failed: ${I(t)}`,
|
|
163
|
+
path: e.outDir
|
|
164
|
+
})
|
|
165
|
+
});
|
|
166
|
+
let D = T(_), k = yield* w(D), j = e.subset ? c(yield* s(e.store, e.snapshot, e.subset), e.snapshot) : yield* l(n, e.snapshot, {
|
|
167
|
+
store: e.store,
|
|
168
|
+
...e.tenantTables ? { tenantTables: e.tenantTables } : {}
|
|
169
|
+
}), N = O(j.tables, e.snapshot), P = e.consistency ?? "live", L, R;
|
|
170
|
+
if (e.masking) {
|
|
171
|
+
let n = e.classification ?? t(e.snapshot), i = N.map((t) => ({
|
|
172
|
+
name: t,
|
|
173
|
+
columns: (e.snapshot.tables.find((e) => e.name === t)?.columns ?? []).map((e) => ({
|
|
174
|
+
name: e.name,
|
|
175
|
+
type: e.type
|
|
176
|
+
}))
|
|
177
|
+
})), o = r(i, n, e.masking);
|
|
178
|
+
if (o.unclassified.length > 0 || o.invalid.length > 0) return yield* A.fail(new a({
|
|
179
|
+
unclassified: o.unclassified,
|
|
180
|
+
...o.invalid.length > 0 ? { invalidActions: o.invalid.map((e) => `${e.column}: ${JSON.stringify(e.action)}`) } : {}
|
|
181
|
+
}));
|
|
182
|
+
R = {
|
|
183
|
+
actions: o.actions,
|
|
184
|
+
seed: e.masking.seed
|
|
185
|
+
};
|
|
186
|
+
let s = Object.entries(o.actions).filter(([, e]) => e !== "keep").map(([e]) => e);
|
|
187
|
+
L = {
|
|
188
|
+
...e.masking.id ? { policyId: e.masking.id } : {},
|
|
189
|
+
transformed: s
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
let z = {
|
|
193
|
+
snapshot: e.snapshot,
|
|
194
|
+
ordered: N,
|
|
195
|
+
resolved: j,
|
|
196
|
+
compression: o,
|
|
197
|
+
chunkSize: d,
|
|
198
|
+
retry: S,
|
|
199
|
+
ledger: k,
|
|
200
|
+
ledgerPath: D,
|
|
201
|
+
bundleFile: T,
|
|
202
|
+
...R ? { mask: R } : {},
|
|
203
|
+
...e.onProgress ? { onProgress: e.onProgress } : {}
|
|
204
|
+
}, B = P === "snapshot" ? yield* K(e.store, e.dialect, (e) => W(e, z)) : yield* W(e.store, z), V = 0, H = 0;
|
|
205
|
+
if (e.assets) {
|
|
206
|
+
let t = T(h), n = yield* u({
|
|
207
|
+
source: e.assets,
|
|
208
|
+
assetsDir: T(m),
|
|
209
|
+
progress: {
|
|
210
|
+
done: (e) => e in k.assets,
|
|
211
|
+
mark: (e, n) => (k.assets[e] = !0, U(t, JSON.stringify(n)).pipe(A.andThen(E(D, k))))
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
V = n.count, H = n.totalBytes, n.missing.length > 0 && (yield* A.logWarning(`${n.missing.length} of ${n.missing.length + n.count} blob reference(s) point at objects the storage provider does not have; they are NOT in this bundle and no import can bring them back. e.g. ${n.missing.slice(0, 3).map((e) => `'${e}'`).join(", ")}` + (n.missing.length > 3 ? `, and ${n.missing.length - 3} more` : "")));
|
|
215
|
+
}
|
|
216
|
+
let G = {
|
|
217
|
+
formatVersion: 1,
|
|
218
|
+
createdAt: e.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
219
|
+
source: {
|
|
220
|
+
dialect: e.dialect,
|
|
221
|
+
schemaFingerprint: F(e.snapshot)
|
|
222
|
+
},
|
|
223
|
+
scope: n,
|
|
224
|
+
consistency: P,
|
|
225
|
+
compression: o,
|
|
226
|
+
tables: B,
|
|
227
|
+
assets: V > 0 ? {
|
|
228
|
+
count: V,
|
|
229
|
+
totalBytes: H,
|
|
230
|
+
index: h
|
|
231
|
+
} : {
|
|
232
|
+
count: 0,
|
|
233
|
+
totalBytes: 0
|
|
234
|
+
},
|
|
235
|
+
...L ? { masking: L } : {}
|
|
236
|
+
};
|
|
237
|
+
return yield* A.tryPromise({
|
|
238
|
+
try: async () => {
|
|
239
|
+
await (await import("node:fs/promises")).writeFile(T(v), b(G));
|
|
240
|
+
},
|
|
241
|
+
catch: (e) => new i({
|
|
242
|
+
reason: `manifest write failed: ${I(e)}`,
|
|
243
|
+
path: v
|
|
244
|
+
})
|
|
245
|
+
}), yield* A.tryPromise({
|
|
246
|
+
try: async () => {
|
|
247
|
+
let e = await import("node:fs/promises"), t = await import("node:os");
|
|
248
|
+
await e.writeFile(T(y), x(G, { hostname: t.hostname() }));
|
|
249
|
+
},
|
|
250
|
+
catch: (e) => new i({
|
|
251
|
+
reason: `readme write failed: ${I(e)}`,
|
|
252
|
+
path: y
|
|
253
|
+
})
|
|
254
|
+
}), G;
|
|
255
|
+
});
|
|
256
|
+
//#endregion
|
|
257
|
+
export { q as runExport };
|