@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.
@@ -104,12 +104,16 @@ var g = (e, t) => {
104
104
  stdoutFile: r
105
105
  };
106
106
  }
107
- case "sqlite":
108
- case "turso": return {
109
- kind: "copy",
107
+ case "sqlite": return {
108
+ kind: "sqlite-backup",
110
109
  from: y(n),
111
110
  to: r
112
111
  };
112
+ case "turso": return {
113
+ kind: "vacuum-into",
114
+ conn: n,
115
+ to: r
116
+ };
113
117
  case "mssql": return {
114
118
  kind: "spawn",
115
119
  tool: "sqlpackage",
@@ -273,92 +277,137 @@ END`;
273
277
  return t.gen(function* () {
274
278
  yield* a(`preparing ${n.scratch}`, i.prepare), yield* L(n.load).pipe(t.tapError(() => o)), yield* a(`carrying ${n.carry.join(", ")} into the imported copy`, i.carry).pipe(t.tapError(() => o)), yield* a(`swapping ${n.scratch} in as ${n.live}`, i.swap).pipe(t.tapError(() => a("putting the original database back", i.undoSwap).pipe(t.ignore)));
275
279
  });
276
- }, L = (n, r) => n.kind === "mssql-replace" ? I(n, r) : n.kind === "copy" ? t.callback((r) => {
277
- let o = `${n.to}.restoring-${i()}`, s = (n) => {
278
- try {
279
- d(o);
280
- } catch {}
281
- r(t.fail(new e({
282
- tool: "copy",
283
- reason: n
284
- })));
285
- };
286
- a(n.from, o, (e) => {
287
- if (e) s(`copy ${n.from} → ${n.to}: ${e.message}`);
288
- else {
289
- for (let e of [
290
- `${n.to}-wal`,
291
- `${n.to}-shm`,
292
- `${n.to}-journal`
293
- ]) try {
294
- d(e);
280
+ }, L = (n, r) => {
281
+ if (n.kind === "mssql-replace") return I(n, r);
282
+ if (n.kind === "vacuum-into") {
283
+ if (r === void 0) return t.fail(new e({
284
+ tool: "vacuum-into",
285
+ reason: "a VACUUM INTO backup needs a SQL connection to the database"
286
+ }));
287
+ let a = `${n.to}.backing-up-${i()}`;
288
+ return r(n.conn, [`VACUUM INTO '${a.replaceAll("'", "''")}'`]).pipe(t.flatMap(() => t.callback((e) => {
289
+ l(a, n.to, (n) => e(n ? t.fail(n) : t.void));
290
+ })), t.mapError((t) => {
291
+ try {
292
+ d(a);
295
293
  } catch {}
296
- l(o, n.to, (e) => {
297
- e ? s(`copy ${n.from} → ${n.to}: ${e.message}`) : r(t.void);
294
+ return new e({
295
+ tool: "vacuum-into",
296
+ reason: `VACUUM INTO ${n.to}: ${t instanceof Error ? t.message : String(t)}`
298
297
  });
299
- }
300
- });
301
- }) : t.callback((r) => {
302
- let i = h(n.tool, [...n.args], {
303
- env: {
304
- ...process.env,
305
- ...n.env
306
- },
307
- stdio: [
308
- "pipe",
309
- n.stdoutFile ? "pipe" : "inherit",
310
- "pipe"
311
- ]
312
- }), a = "";
313
- i.stderr?.on("data", (e) => {
314
- a += String(e);
315
- });
316
- let c = !1, l = (e) => {
317
- c || (c = !0, r(e));
318
- }, u, p = (e) => {
319
- u ??= e;
320
- };
321
- if (n.stdoutFile && i.stdout) {
322
- let e = s(n.stdoutFile);
323
- e.on("error", (e) => p(`writing ${f(n.stdoutFile)}: ${e.message}`)), i.stdout.on("error", (e) => p(`reading the tool's output: ${e.message}`)), i.stdout.pipe(e);
324
- }
325
- let m = 0;
326
- if (n.stdinFile && i.stdin) {
327
- let e = o(n.stdinFile);
328
- e.on("data", (e) => {
329
- m += e.length;
330
- }), e.on("error", (e) => {
331
- p(`reading ${f(n.stdinFile)}: ${e.message}`), i.stdin?.end();
332
- }), i.stdin.on("error", (e) => {
333
- e.code !== "EPIPE" && p(`feeding ${f(n.stdinFile)} to ${n.tool}: ${e.message}`);
334
- }), e.pipe(i.stdin);
298
+ }));
335
299
  }
336
- let g = () => {
337
- if (n.stdoutFile !== void 0) try {
338
- d(n.stdoutFile);
339
- } catch {}
340
- };
341
- i.on("error", (r) => {
342
- g(), l(t.fail(new e({
343
- tool: n.tool,
344
- reason: `failed to launch (is '${n.tool}' installed + on PATH?): ${r.message}`,
345
- ...n.hint === void 0 ? {} : { hint: n.hint }
346
- })));
347
- }), i.on("close", (r) => {
348
- if (r === 0 && u === void 0) {
349
- l(t.void);
350
- return;
300
+ return n.kind === "sqlite-backup" ? t.tryPromise({
301
+ try: async () => {
302
+ let { backup: e, DatabaseSync: t } = await import("node:sqlite"), r = `${n.to}.backing-up-${i()}`, a = new t(n.from, { readOnly: !0 });
303
+ try {
304
+ await e(a, r);
305
+ } catch (e) {
306
+ try {
307
+ d(r);
308
+ } catch {}
309
+ throw e;
310
+ } finally {
311
+ a.close();
312
+ }
313
+ await new Promise((e, t) => {
314
+ l(r, n.to, (n) => {
315
+ n ? t(n) : e();
316
+ });
317
+ });
318
+ },
319
+ catch: (t) => new e({
320
+ tool: "sqlite-backup",
321
+ reason: `backup ${n.from} → ${n.to}: ${t instanceof Error ? t.message : String(t)}`
322
+ })
323
+ }) : n.kind === "copy" ? t.callback((r) => {
324
+ let o = `${n.to}.restoring-${i()}`, s = (n) => {
325
+ try {
326
+ d(o);
327
+ } catch {}
328
+ r(t.fail(new e({
329
+ tool: "copy",
330
+ reason: n
331
+ })));
332
+ };
333
+ a(n.from, o, (e) => {
334
+ if (e) s(`copy ${n.from} → ${n.to}: ${e.message}`);
335
+ else {
336
+ for (let e of [
337
+ `${n.to}-wal`,
338
+ `${n.to}-shm`,
339
+ `${n.to}-journal`,
340
+ `${n.to}-log`
341
+ ]) try {
342
+ d(e);
343
+ } catch {}
344
+ l(o, n.to, (e) => {
345
+ e ? s(`copy ${n.from} → ${n.to}: ${e.message}`) : r(t.void);
346
+ });
347
+ }
348
+ });
349
+ }) : t.callback((r) => {
350
+ let i = h(n.tool, [...n.args], {
351
+ env: {
352
+ ...process.env,
353
+ ...n.env
354
+ },
355
+ stdio: [
356
+ "pipe",
357
+ n.stdoutFile ? "pipe" : "inherit",
358
+ "pipe"
359
+ ]
360
+ }), a = "";
361
+ i.stderr?.on("data", (e) => {
362
+ a += String(e);
363
+ });
364
+ let c = !1, l = (e) => {
365
+ c || (c = !0, r(e));
366
+ }, u, p = (e) => {
367
+ u ??= e;
368
+ };
369
+ if (n.stdoutFile && i.stdout) {
370
+ let e = s(n.stdoutFile);
371
+ e.on("error", (e) => p(`writing ${f(n.stdoutFile)}: ${e.message}`)), i.stdout.on("error", (e) => p(`reading the tool's output: ${e.message}`)), i.stdout.pipe(e);
372
+ }
373
+ let m = 0;
374
+ if (n.stdinFile && i.stdin) {
375
+ let e = o(n.stdinFile);
376
+ e.on("data", (e) => {
377
+ m += e.length;
378
+ }), e.on("error", (e) => {
379
+ p(`reading ${f(n.stdinFile)}: ${e.message}`), i.stdin?.end();
380
+ }), i.stdin.on("error", (e) => {
381
+ e.code !== "EPIPE" && p(`feeding ${f(n.stdinFile)} to ${n.tool}: ${e.message}`);
382
+ }), e.pipe(i.stdin);
351
383
  }
352
- g();
353
- let i = n.stdinFile === void 0 ? "" : ` — ${m} of ${R(n.stdinFile)} byte(s) of ${f(n.stdinFile)} had been fed to it`;
354
- l(t.fail(new e({
355
- tool: n.tool,
356
- reason: (r === 0 ? `exited cleanly, but ${u}` : `exited with code ${r}${u === void 0 ? "" : ` (${u})`}`) + i + (n.stdoutFile === void 0 ? "" : ` — the partial ${f(n.stdoutFile)} was removed, so it cannot be mistaken for a backup`),
357
- stderr: a.trim() === "" ? `(${n.tool} printed nothing to stderr)` : a.slice(-2e3),
358
- ...n.hint === void 0 ? {} : { hint: n.hint }
359
- })));
384
+ let g = () => {
385
+ if (n.stdoutFile !== void 0) try {
386
+ d(n.stdoutFile);
387
+ } catch {}
388
+ };
389
+ i.on("error", (r) => {
390
+ g(), l(t.fail(new e({
391
+ tool: n.tool,
392
+ reason: `failed to launch (is '${n.tool}' installed + on PATH?): ${r.message}`,
393
+ ...n.hint === void 0 ? {} : { hint: n.hint }
394
+ })));
395
+ }), i.on("close", (r) => {
396
+ if (r === 0 && u === void 0) {
397
+ l(t.void);
398
+ return;
399
+ }
400
+ g();
401
+ let i = n.stdinFile === void 0 ? "" : ` — ${m} of ${R(n.stdinFile)} byte(s) of ${f(n.stdinFile)} had been fed to it`;
402
+ l(t.fail(new e({
403
+ tool: n.tool,
404
+ reason: (r === 0 ? `exited cleanly, but ${u}` : `exited with code ${r}${u === void 0 ? "" : ` (${u})`}`) + i + (n.stdoutFile === void 0 ? "" : ` — the partial ${f(n.stdoutFile)} was removed, so it cannot be mistaken for a backup`),
405
+ stderr: a.trim() === "" ? `(${n.tool} printed nothing to stderr)` : a.slice(-2e3),
406
+ ...n.hint === void 0 ? {} : { hint: n.hint }
407
+ })));
408
+ });
360
409
  });
361
- }), R = (e) => {
410
+ }, R = (e) => {
362
411
  try {
363
412
  return String(u(e).size);
364
413
  } catch {
@@ -0,0 +1,10 @@
1
+ import { Effect as e } from "effect";
2
+ //#region src/physicalRequest.ts
3
+ var t = (t, n) => e.callback((r) => {
4
+ let i = new AbortController(), a = Promise.resolve().then(() => t(i.signal));
5
+ return a.then((t) => r(e.succeed(t)), (t) => r(e.fail(n(t)))), e.sync(() => {
6
+ i.abort();
7
+ }).pipe(e.andThen(e.promise(() => a.then(() => void 0, () => void 0))));
8
+ });
9
+ //#endregion
10
+ export { t as physicalRequest };
@@ -0,0 +1,40 @@
1
+ import { ciphertextKeyId as e } from "@voltro/database";
2
+ //#region src/sealedKeys.ts
3
+ var t = /enc:v[12]:[A-Za-z0-9+/=:_-]+/g, n = () => ({
4
+ byKey: {},
5
+ unnamed: 0
6
+ }), r = (e) => typeof e == "string" ? e : typeof e == "object" && e && !(e instanceof Uint8Array) && !(e instanceof Date) ? JSON.stringify(e) : void 0, i = (n, i) => {
7
+ for (let a of Object.values(n)) {
8
+ let n = r(a);
9
+ if (n !== void 0 && n.includes("enc:v")) for (let r of n.match(t) ?? []) {
10
+ let t = e(r);
11
+ t === void 0 ? i.unnamed += 1 : i.byKey[t] = (i.byKey[t] ?? 0) + 1;
12
+ }
13
+ }
14
+ }, a = (e, t) => {
15
+ for (let [n, r] of Object.entries(t.byKey)) e.byKey[n] = (e.byKey[n] ?? 0) + r;
16
+ e.unnamed += t.unnamed;
17
+ }, o = (e) => {
18
+ let n = e.match(t);
19
+ return n !== null && n.length === 1 && n[0] === e;
20
+ }, s = (n, i, a, s) => {
21
+ let c = new Set(a.held), l = (t, r) => {
22
+ let o = e(t);
23
+ return o === void 0 ? a.opens?.(t, {
24
+ table: n,
25
+ column: r,
26
+ row: i
27
+ }) ?? !0 : c.has(o);
28
+ }, u = { ...i };
29
+ for (let [e, n] of Object.entries(i)) {
30
+ if (typeof n == "string" && o(n)) {
31
+ l(n, e) || (u[e] = null, s.nulled.set(e, (s.nulled.get(e) ?? 0) + 1));
32
+ continue;
33
+ }
34
+ let i = r(n);
35
+ if (i !== void 0 && i.includes("enc:v")) for (let n of i.match(t) ?? []) l(n, e) || s.unreadable.set(e, (s.unreadable.get(e) ?? 0) + 1);
36
+ }
37
+ return u;
38
+ };
39
+ //#endregion
40
+ export { n as emptyTally, a as mergeTally, s as nullUnreadableSealed, i as tallyRow };
@@ -0,0 +1,47 @@
1
+ import { BundleError as e } from "./errors.js";
2
+ import { admitFilesystemPath as t } from "./filesystemAdmission.js";
3
+ import { isSafeBundlePath as n } from "./bundlePaths.js";
4
+ import { MANIFEST_FILE as r, decodeManifest as i } from "./manifest.js";
5
+ import { AssetSourceInfo as a } from "./assetAdmission.js";
6
+ import { makeDecompressor as o } from "./compress.js";
7
+ import { ndjsonToRows as s } from "./ndjson.js";
8
+ import { Effect as c, Schema as l, Stream as u } from "effect";
9
+ import { createReadStream as d } from "node:fs";
10
+ import { readFile as f } from "node:fs/promises";
11
+ import { join as p } from "node:path";
12
+ import { STORAGE_REFS_TABLE as m, fromNodeReadable as h } from "@voltro/plugin-storage";
13
+ import { compose as g } from "node:stream";
14
+ //#region src/storageAssetMetadata.ts
15
+ var _ = (t) => c.tryPromise({
16
+ try: async () => {
17
+ let e = i(await f(p(t, r), "utf8"));
18
+ return {
19
+ dir: t,
20
+ tables: e.tables,
21
+ compression: e.compression
22
+ };
23
+ },
24
+ catch: () => new e({ reason: "exported asset manifest unavailable" })
25
+ }), v = (e) => l.decodeUnknownSync(a)({
26
+ destination: e.bucket,
27
+ tenantId: e.tenantId,
28
+ key: e.key,
29
+ ...typeof e.checksum == "string" && /^[a-f0-9]{64}$/.test(e.checksum) ? { sha256: e.checksum } : {},
30
+ contentType: e.contentType,
31
+ size: typeof e.size == "bigint" || typeof e.size == "string" && /^(0|[1-9]\d*)$/.test(e.size) ? Number(e.size) : e.size
32
+ }), y = (r) => {
33
+ let i = r.tables.find((e) => e.name === m);
34
+ return i === void 0 ? u.empty : u.unwrap(c.gen(function* () {
35
+ if (!n(i.file)) return yield* c.fail(new e({ reason: "invalid asset metadata table path" }));
36
+ let a = p(r.dir, i.file);
37
+ return yield* c.tryPromise({
38
+ try: () => t(a, "file"),
39
+ catch: () => new e({ reason: "asset metadata path unavailable" })
40
+ }), s(h("bundle", () => g(d(a), o(r.compression))), i.name, i.columnTypes).pipe(u.mapEffect((t) => c.try({
41
+ try: () => v(t),
42
+ catch: () => new e({ reason: "invalid storage ownership in exported rows" })
43
+ })));
44
+ }));
45
+ };
46
+ //#endregion
47
+ export { _ as assetBundleContext, y as bundleAssetInfoStream, v as storageAssetInfo };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/data-transfer",
3
- "version": "0.86.0",
3
+ "version": "0.87.0",
4
4
  "description": "Portable export/import for a Voltro app's data + assets — typed-NDJSON row codec, content-addressed assets, framed compression, a resumable chunk ledger, and FK-ordered idempotent import. Dialect-agnostic; the resilient backbone under `voltro data export|import` and native backups.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -33,8 +33,8 @@
33
33
  "node": ">=24.0.0"
34
34
  },
35
35
  "dependencies": {
36
- "@voltro/database": "0.86.0",
37
- "@voltro/plugin-storage": "0.86.0"
36
+ "@voltro/database": "0.87.0",
37
+ "@voltro/plugin-storage": "0.87.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "effect": "4.0.1"