@voltro/data-transfer 0.86.0 → 0.87.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/dist/exporter.js CHANGED
@@ -1,257 +1,271 @@
1
1
  import { snapshotPrelude as e } from "./consistency.js";
2
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";
3
+ import { emptyTally as i, mergeTally as a, tallyRow as o } from "./sealedKeys.js";
4
+ import { BundleError as s, MaskingError as c, ScopeError as l } from "./errors.js";
5
+ import { computeSubsetIds as u, subsetToScope as d } from "./subset.js";
6
+ import { resolveScope as f } from "./scope.js";
7
+ import { ASSETS_DIR as p, ASSET_INDEX_FILE as m, DATA_DIR as h, LEDGER_FILE as g, MANIFEST_FILE as _, README_FILE as v, encodeManifest as y } from "./manifest.js";
8
+ import { captureAssetsToCas as b } from "./casAssetPhase.js";
9
+ import { compressionExt as x, makeCompressor as S, pickCompression as C } from "./compress.js";
10
+ import { buildReadme as w } from "./readme.js";
11
+ import { reportProgress as T, withTableSpan as E } from "./progress.js";
12
+ import { readLedger as D, tableDone as O, writeLedger as k } from "./ledger.js";
13
+ import { rowsToNdjson as A } from "./ndjson.js";
14
+ import { topoSortTables as j } from "./topo.js";
15
+ import { Cause as M, Effect as N, Exit as P, Schedule as F, Stream as I } from "effect";
16
+ import { streamTable as L } from "@voltro/database";
17
+ import { fingerprintSchema as R } from "@voltro/database/sql";
17
18
  //#region src/exporter.ts
18
- var I = (e) => {
19
+ var z = (e) => {
19
20
  let t = e, n = [];
20
21
  typeof t?._tag == "string" && t._tag !== "" && n.push(t._tag);
21
22
  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
+ if (r === "" ? t?.cause !== void 0 && t.cause !== null && n.push(z(t.cause)) : n.push(r), n.length === 0) {
23
24
  let t = String(e ?? "").trim();
24
- return t === "" || t === "[object Object]" ? L(e) : t;
25
+ return t === "" || t === "[object Object]" ? B(e) : t;
25
26
  }
26
27
  return n.join(": ");
27
- }, L = (e) => {
28
+ }, B = (e) => {
28
29
  try {
29
30
  let t = JSON.stringify(e);
30
31
  return t === void 0 || t === "{}" ? "unknown error (no message, cause or tag)" : t;
31
32
  } catch {
32
33
  return "unknown error (not serialisable)";
33
34
  }
34
- }, R = (e, t) => {
35
+ }, V = (e, t) => {
35
36
  let n = e.tables.find((e) => e.name === t), r = {};
36
37
  for (let e of n?.columns ?? []) e.generatedAs === void 0 && (r[e.name] = e.type);
37
38
  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
+ }, H = (e, t) => (e.tables.find((e) => e.name === t)?.columns ?? []).filter((e) => e.generatedAs !== void 0).map((e) => e.name), U = (e, t) => {
39
40
  if (t.length === 0) return e;
40
41
  let n = { ...e };
41
42
  for (let e of t) delete n[e];
42
43
  return n;
43
- }, V = (e, t) => {
44
+ }, W = (e, t) => {
44
45
  let n = e.tables.find((e) => e.name === t);
45
46
  if (n === void 0) return { refusal: `${t}: not in the schema snapshot, so its primary key is unknown.` };
46
47
  let r = n.columns.find((e) => e.type === "id");
47
48
  if (r !== void 0) return { column: r.name };
48
49
  let i = n.indexes.find((e) => e.name === `${t}_pkey`)?.columns ?? n.primaryKey ?? [];
49
50
  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
+ }, G = (e, t, n) => N.tryPromise({
51
52
  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
+ 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, f = new l({ transform(e, t, n) {
53
54
  u.update(e);
54
55
  let r = e;
55
56
  for (let e = 0; e < r.length; e++) r[e] === 10 && d++;
56
57
  n(null, e);
57
- } }), m = c.fromWeb(N.toReadableStream(e)), h = `${t}.tmp-${o()}`;
58
+ } }), p = c.fromWeb(I.toReadableStream(e)), m = `${t}.tmp-${o()}`;
58
59
  try {
59
- await s(m, p, f(n), r(h)), await i.rename(h, t);
60
+ await s(p, f, S(n), r(m)), await i.rename(m, t);
60
61
  } catch (e) {
61
- throw await i.rm(h, { force: !0 }).catch(() => {}), e;
62
+ throw await i.rm(m, { force: !0 }).catch(() => {}), e;
62
63
  }
63
64
  return {
64
65
  rowCount: d,
65
66
  checksum: u.digest("hex")
66
67
  };
67
68
  },
68
- catch: (e) => new i({
69
- reason: `write table failed: ${I(e)}`,
69
+ catch: (e) => new s({
70
+ reason: `write table failed: ${z(e)}`,
70
71
  path: t
71
72
  })
72
- }), U = (e, t) => A.tryPromise({
73
+ }), K = (e, t) => N.tryPromise({
73
74
  try: async () => {
74
75
  await (await import("node:fs/promises")).appendFile(e, `${t}\n`);
75
76
  },
76
- catch: (t) => new i({
77
- reason: `append failed: ${I(t)}`,
77
+ catch: (t) => new s({
78
+ reason: `append failed: ${z(t)}`,
78
79
  path: e
79
80
  })
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, {
81
+ }), q = (e, t) => N.gen(function* () {
82
+ let r = [], s = t.ordered.length;
83
+ for (let c = 0; c < t.ordered.length; c++) {
84
+ let u = t.ordered[c], d = V(t.snapshot, u), f = W(t.snapshot, u);
85
+ if ("refusal" in f) return yield* N.fail(new l({ reason: `export: ${f.refusal}` }));
86
+ let p = f.column, m = `${h}/${u}.ndjson${x(t.compression)}`;
87
+ if (O(t.ledger, u)) {
88
+ let e = t.ledger.tables[u];
89
+ e.sealed !== void 0 && a(t.sealed, e.sealed), T(t.onProgress, {
89
90
  phase: "export",
90
- table: s,
91
- index: a,
92
- tableCount: i,
91
+ table: u,
92
+ index: c,
93
+ tableCount: s,
93
94
  event: "done",
94
95
  rowsDone: e.rowCount,
95
96
  total: e.rowCount
96
97
  }), r.push({
97
- name: s,
98
- file: f,
98
+ name: u,
99
+ file: m,
99
100
  rowCount: e.rowCount,
100
101
  checksum: e.checksum,
101
- columnTypes: c,
102
- primaryKey: u
102
+ columnTypes: d,
103
+ primaryKey: p
103
104
  });
104
105
  continue;
105
106
  }
106
- S(t.onProgress, {
107
+ T(t.onProgress, {
107
108
  phase: "export",
108
- table: s,
109
- index: a,
110
- tableCount: i,
109
+ table: u,
110
+ index: c,
111
+ tableCount: s,
111
112
  event: "start",
112
113
  rowsDone: 0
113
114
  });
114
- let p = t.resolved.predicateFor(s), m = P(e, {
115
- table: s,
116
- pk: u,
115
+ let g = t.resolved.predicateFor(u), _ = L(e, {
116
+ table: u,
117
+ pk: p,
117
118
  chunkSize: t.chunkSize,
118
119
  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, {
120
+ ...g === void 0 ? {} : { where: g }
121
+ }), v = H(t.snapshot, u), y = v.length === 0 ? _ : _.pipe(I.map((e) => U(e, v))), b = t.mask ? y.pipe(I.map((e) => n(e, u, t.mask.actions, t.mask.seed, d))) : y, S = i(), C = b.pipe(I.map((e) => (o(e, S), e))), { rowCount: w, checksum: D } = yield* E("export", u, c, s, (e) => e.rowCount, G(A(C, d), t.bundleFile(m), t.compression));
122
+ t.ledger.tables[u] = {
123
+ rowCount: w,
124
+ checksum: D,
125
+ sealed: S
126
+ }, a(t.sealed, S), yield* k(t.ledgerPath, t.ledger), T(t.onProgress, {
125
127
  phase: "export",
126
- table: s,
127
- index: a,
128
- tableCount: i,
128
+ table: u,
129
+ index: c,
130
+ tableCount: s,
129
131
  event: "done",
130
- rowsDone: y,
131
- total: y
132
+ rowsDone: w,
133
+ total: w
132
134
  }), r.push({
133
- name: s,
134
- file: f,
135
- rowCount: y,
136
- checksum: b,
137
- columnTypes: c,
138
- primaryKey: u
135
+ name: u,
136
+ file: m,
137
+ rowCount: w,
138
+ checksum: D,
139
+ columnTypes: d,
140
+ primaryKey: p
139
141
  });
140
142
  }
141
143
  return r;
142
- }), G = (e) => ({
144
+ }), J = (e) => ({
143
145
  _tag: "RawSqlFragment",
144
146
  strings: [e],
145
147
  values: []
146
- }), K = (t, n, r) => A.tryPromise({
148
+ }), Y = (t, n, r) => N.tryPromise({
147
149
  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));
150
+ if (t.raw) for (let r of e(n)) await t.raw(J(r));
151
+ let i = await N.runPromiseExit(r(t));
152
+ if (P.isFailure(i)) throw Error(M.pretty(i.cause));
151
153
  return i.value;
152
154
  }),
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({
155
+ catch: (e) => e instanceof s ? e : new s({ reason: `snapshot export failed: ${z(e)}` })
156
+ }), X = (e) => N.gen(function* () {
157
+ let n = e.scope ?? { kind: "all" }, a = C(e.compression), o = e.chunkSize ?? 1e3, l = e.retryTimes ?? 5, x = F.max([F.exponential("50 millis").pipe(F.jittered), F.recurs(l)]), S = yield* N.promise(() => import("node:path")), T = (t) => S.join(e.outDir, t);
158
+ yield* N.tryPromise({
157
159
  try: async () => {
158
160
  let t = await import("node:fs/promises");
159
- await t.mkdir(T(g), { recursive: !0 }), e.assets && await t.mkdir(T(m), { recursive: !0 });
161
+ await t.mkdir(T(h), { recursive: !0 }), e.assets && await t.mkdir(T(p), { recursive: !0 });
160
162
  },
161
- catch: (t) => new i({
162
- reason: `mkdir failed: ${I(t)}`,
163
+ catch: (t) => new s({
164
+ reason: `mkdir failed: ${z(t)}`,
163
165
  path: e.outDir
164
166
  })
165
167
  });
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, {
168
+ let E = T(g), O = yield* D(E), A = e.subset ? d(yield* u(e.store, e.snapshot, e.subset), e.snapshot) : yield* f(n, e.snapshot, {
167
169
  store: e.store,
168
170
  ...e.tenantTables ? { tenantTables: e.tenantTables } : {}
169
- }), N = O(j.tables, e.snapshot), P = e.consistency ?? "live", L, R;
171
+ }), M = j(A.tables, e.snapshot), P = e.consistency ?? "live", I, L;
170
172
  if (e.masking) {
171
- let n = e.classification ?? t(e.snapshot), i = N.map((t) => ({
173
+ let n = e.classification ?? t(e.snapshot), i = M.map((t) => ({
172
174
  name: t,
173
175
  columns: (e.snapshot.tables.find((e) => e.name === t)?.columns ?? []).map((e) => ({
174
176
  name: e.name,
175
177
  type: e.type
176
178
  }))
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)}`) } : {}
179
+ })), a = r(i, n, e.masking);
180
+ if (a.unclassified.length > 0 || a.invalid.length > 0) return yield* N.fail(new c({
181
+ unclassified: a.unclassified,
182
+ ...a.invalid.length > 0 ? { invalidActions: a.invalid.map((e) => `${e.column}: ${JSON.stringify(e.action)}`) } : {}
181
183
  }));
182
- R = {
183
- actions: o.actions,
184
+ L = {
185
+ actions: a.actions,
184
186
  seed: e.masking.seed
185
187
  };
186
- let s = Object.entries(o.actions).filter(([, e]) => e !== "keep").map(([e]) => e);
187
- L = {
188
+ let o = Object.entries(a.actions).filter(([, e]) => e !== "keep").map(([e]) => e);
189
+ I = {
188
190
  ...e.masking.id ? { policyId: e.masking.id } : {},
189
- transformed: s
191
+ transformed: o
190
192
  };
191
193
  }
192
- let z = {
194
+ let B = i(), V = {
195
+ sealed: B,
193
196
  snapshot: e.snapshot,
194
- ordered: N,
195
- resolved: j,
196
- compression: o,
197
- chunkSize: d,
198
- retry: S,
199
- ledger: k,
200
- ledgerPath: D,
197
+ ordered: M,
198
+ resolved: A,
199
+ compression: a,
200
+ chunkSize: o,
201
+ retry: x,
202
+ ledger: O,
203
+ ledgerPath: E,
201
204
  bundleFile: T,
202
- ...R ? { mask: R } : {},
205
+ ...L ? { mask: L } : {},
203
206
  ...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;
207
+ }, H = P === "snapshot" ? yield* Y(e.store, e.dialect, (e) => q(e, V)) : yield* q(e.store, V), U = 0, W = 0;
205
208
  if (e.assets) {
206
- let t = T(h), n = yield* u({
207
- source: e.assets,
208
- assetsDir: T(m),
209
+ let t = T(m), n = yield* b({
210
+ source: {
211
+ ...e.assets,
212
+ list: () => e.assets.list({
213
+ dir: e.outDir,
214
+ tables: H,
215
+ compression: a
216
+ })
217
+ },
218
+ assetsDir: T(p),
209
219
  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))))
220
+ done: (e) => e in O.assets,
221
+ mark: (e, n) => (O.assets[e] = !0, K(t, JSON.stringify(n)).pipe(N.andThen(k(E, O))))
212
222
  }
213
223
  });
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` : "")));
224
+ U = n.count, W = n.totalBytes, n.missing.length > 0 && (yield* N.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
225
  }
216
226
  let G = {
217
- formatVersion: 1,
227
+ formatVersion: 2,
218
228
  createdAt: e.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
219
229
  source: {
220
230
  dialect: e.dialect,
221
- schemaFingerprint: F(e.snapshot)
231
+ schemaFingerprint: R(e.snapshot)
222
232
  },
223
233
  scope: n,
224
234
  consistency: P,
225
- compression: o,
226
- tables: B,
227
- assets: V > 0 ? {
228
- count: V,
229
- totalBytes: H,
230
- index: h
235
+ compression: a,
236
+ tables: H,
237
+ assets: U > 0 ? {
238
+ count: U,
239
+ totalBytes: W,
240
+ index: m
231
241
  } : {
232
242
  count: 0,
233
243
  totalBytes: 0
234
244
  },
235
- ...L ? { masking: L } : {}
245
+ ...I ? { masking: I } : {},
246
+ fieldKeys: {
247
+ keyIds: Object.entries(B.byKey).filter(([, e]) => e > 0).map(([e]) => e).sort(),
248
+ unnamed: B.unnamed
249
+ }
236
250
  };
237
- return yield* A.tryPromise({
251
+ return yield* N.tryPromise({
238
252
  try: async () => {
239
- await (await import("node:fs/promises")).writeFile(T(v), b(G));
253
+ await (await import("node:fs/promises")).writeFile(T(_), y(G));
240
254
  },
241
- catch: (e) => new i({
242
- reason: `manifest write failed: ${I(e)}`,
243
- path: v
255
+ catch: (e) => new s({
256
+ reason: `manifest write failed: ${z(e)}`,
257
+ path: _
244
258
  })
245
- }), yield* A.tryPromise({
259
+ }), yield* N.tryPromise({
246
260
  try: async () => {
247
261
  let e = await import("node:fs/promises"), t = await import("node:os");
248
- await e.writeFile(T(y), x(G, { hostname: t.hostname() }));
262
+ await e.writeFile(T(v), w(G, { hostname: t.hostname() }));
249
263
  },
250
- catch: (e) => new i({
251
- reason: `readme write failed: ${I(e)}`,
252
- path: y
264
+ catch: (e) => new s({
265
+ reason: `readme write failed: ${z(e)}`,
266
+ path: v
253
267
  })
254
268
  }), G;
255
269
  });
256
270
  //#endregion
257
- export { q as runExport };
271
+ export { X as runExport };
@@ -0,0 +1,33 @@
1
+ import { constants as e } from "node:fs";
2
+ import { copyFile as t, lstat as n, mkdir as r, mkdtemp as i, rename as a, rm as o } from "node:fs/promises";
3
+ import { dirname as s, join as c, parse as l, resolve as u, sep as d } from "node:path";
4
+ //#region src/filesystemAdmission.ts
5
+ var f = async (e, t) => {
6
+ let r = u(e), i = l(r).root, a = r.slice(i.length).split(d).filter(Boolean), o = i;
7
+ for (let e = 0; e < a.length; e++) {
8
+ o = c(o, a[e]);
9
+ let r;
10
+ try {
11
+ r = await n(o);
12
+ } catch (e) {
13
+ if (typeof e == "object" && e && "code" in e && e.code === "ENOENT") return;
14
+ throw e;
15
+ }
16
+ if (r.isSymbolicLink()) throw Error("bundle path contains a symbolic link");
17
+ if (e === a.length - 1 && t === "file" ? !r.isFile() : !r.isDirectory()) throw Error("bundle path has an incompatible filesystem entry");
18
+ }
19
+ }, p = async (n, l) => {
20
+ await f(l, "file"), await r(s(l), { recursive: !0 }), await f(l, "file");
21
+ let u = await i(c(s(l), ".voltro-unpack-"));
22
+ try {
23
+ let r = c(u, "file");
24
+ await t(n, r, e.COPYFILE_EXCL), await f(l, "file"), await a(r, l);
25
+ } finally {
26
+ await o(u, {
27
+ recursive: !0,
28
+ force: !0
29
+ });
30
+ }
31
+ };
32
+ //#endregion
33
+ export { f as admitFilesystemPath, p as publishBundleFile };