@noy-db/as-json 0.1.0-pre.8 → 0.2.0-pre.1

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/index.cjs CHANGED
@@ -105,11 +105,11 @@ async function fromObject(vault, doc, options = {}) {
105
105
  await vault.noydb.transaction((tx) => {
106
106
  const txVault = tx.vault(vault.name);
107
107
  for (const entry of plan.added) {
108
- txVault.collection(entry.collection).put(entry.id, entry.record);
108
+ txVault.collection(entry.collection).put(entry.id, entry.record, { reason: "import:json" });
109
109
  }
110
110
  if (policy !== "insert-only") {
111
111
  for (const entry of plan.modified) {
112
- txVault.collection(entry.collection).put(entry.id, entry.record);
112
+ txVault.collection(entry.collection).put(entry.id, entry.record, { reason: "import:json" });
113
113
  }
114
114
  }
115
115
  if (policy === "replace") {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/as-json** — structured JSON plaintext export for noy-db.\n *\n * Decrypts ACL-scoped records from a vault and emits one structured\n * JSON document grouping records by collection. Sibling to the core\n * `exportJSON()` helper — same shape, but gated behind\n * `canExportPlaintext` and paired with browser-download +\n * Node file-write helpers.\n *\n * **Scope.** Multi-collection per call (unlike `as-csv` which is\n * single-collection). Whole-vault by default; pass `collections` to\n * restrict.\n *\n * See `docs/patterns/as-exports.md` for the three-tier egress model\n * (Tier 1 in-memory → Tier 2 browser download → Tier 3 disk write).\n *\n * @packageDocumentation\n */\n\nimport type { Vault } from '@noy-db/hub'\n\nexport interface AsJSONOptions {\n /**\n * Collection allowlist. When omitted, every collection the caller\n * can read is included. Collections not in the caller's ACL silently\n * drop out even when listed here — ACL-scoping runs at the\n * `exportStream` layer.\n */\n readonly collections?: readonly string[]\n\n /**\n * Pretty-print with indentation. Default `2` (2-space indent). Pass\n * `0` or `false` for compact single-line output.\n */\n readonly pretty?: number | boolean\n\n /**\n * Include envelope metadata (`_v`, `_ts`, `_by`) alongside each\n * record. Default `false` — stripped so the JSON matches the shape\n * of the raw records the consumer originally put.\n */\n readonly includeMeta?: boolean\n}\n\nexport interface AsJSONDownloadOptions extends AsJSONOptions {\n /** Filename offered to the browser. Default `'vault-export.json'`. */\n readonly filename?: string\n}\n\nexport interface AsJSONWriteOptions extends AsJSONOptions {\n /** Required to write plaintext JSON to disk — Tier 3 risk gate. */\n readonly acknowledgeRisks: true\n}\n\n/**\n * Shape of the emitted document: one top-level key per collection,\n * each mapping to an array of record objects.\n */\nexport type AsJSONDocument = Record<string, readonly Record<string, unknown>[]>\n\n/**\n * Serialise the vault as a JSON string. Pure operation — no side\n * effects beyond the authorization check and audit ledger write.\n */\nexport async function toString(vault: Vault, options: AsJSONOptions = {}): Promise<string> {\n const doc = await toObject(vault, options)\n const indent = typeof options.pretty === 'number'\n ? options.pretty\n : options.pretty === false\n ? 0\n : 2\n return indent > 0 ? JSON.stringify(doc, null, indent) : JSON.stringify(doc)\n}\n\n/**\n * Serialise the vault as a plain JS object. Useful for in-process\n * pipelines that want to post-process the data before writing.\n */\nexport async function toObject(vault: Vault, options: AsJSONOptions = {}): Promise<AsJSONDocument> {\n vault.assertCanExport('plaintext', 'json')\n\n const allowlist = options.collections ? new Set(options.collections) : null\n const doc: Record<string, Record<string, unknown>[]> = {}\n for await (const chunk of vault.exportStream({ granularity: 'collection' })) {\n if (allowlist && !allowlist.has(chunk.collection)) continue\n const bucket = doc[chunk.collection] ?? (doc[chunk.collection] = [])\n for (const record of chunk.records) {\n if (options.includeMeta) {\n bucket.push(record as Record<string, unknown>)\n } else {\n bucket.push(stripMeta(record as Record<string, unknown>))\n }\n }\n }\n return doc\n}\n\n/**\n * Browser download — wraps `toString()` in a Blob and triggers the\n * browser's save-as prompt. Requires a DOM — in Node, use `write()`.\n */\nexport async function download(vault: Vault, options: AsJSONDownloadOptions = {}): Promise<void> {\n const json = await toString(vault, options)\n const filename = options.filename ?? 'vault-export.json'\n const blob = new Blob([json], { type: 'application/json;charset=utf-8' })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n a.click()\n URL.revokeObjectURL(url)\n}\n\n/**\n * Node file-write — persists the JSON to disk. Requires\n * `acknowledgeRisks: true` because plaintext outlives the process.\n */\nexport async function write(\n vault: Vault,\n path: string,\n options: AsJSONWriteOptions,\n): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error(\n 'as-json.write: acknowledgeRisks: true is required for on-disk plaintext output. ' +\n 'See docs/patterns/as-exports.md §\"The three tiers of \\\\\"plaintext out\\\\\"\"',\n )\n }\n const json = await toString(vault, options)\n const { writeFile } = await import('node:fs/promises')\n await writeFile(path, json, 'utf-8')\n}\n\nfunction stripMeta(record: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(record)) {\n if (key === '_v' || key === '_ts' || key === '_by' || key === '_iv' || key === '_data' || key === '_noydb') continue\n out[key] = value\n }\n return out\n}\n\n// ─── Reader ─────────────────────────────────────────────\n\nimport { diffVault, type VaultDiff } from '@noy-db/hub'\n\n/**\n * Reconciliation policy for `apply()`.\n *\n * - `'merge'` (default) — insert + update, never delete. Records\n * present in the live vault but absent from the file are left\n * intact.\n * - `'replace'` — full mirror. Records present in the live vault but\n * absent from the file are deleted.\n * - `'insert-only'` — only insert new records; skip both updates and\n * deletes. Useful for append-only ledgers.\n */\nexport type ImportPolicy = 'merge' | 'replace' | 'insert-only'\n\nexport interface AsJSONImportOptions {\n /** Restrict the diff + apply to a subset of collections. */\n readonly collections?: readonly string[]\n /** Field on each record that carries its id. Default `'id'`. */\n readonly idKey?: string\n /** Reconciliation policy. Default `'merge'`. */\n readonly policy?: ImportPolicy\n}\n\n/**\n * Output of `fromString` / `fromObject` — preview the changes a JSON\n * import would apply, then commit them with `apply()`. Two-step shape\n * keeps the diff cheap and lets consumers render review-and-confirm\n * UIs without a separate dry-run mode.\n */\nexport interface AsJSONImportPlan {\n readonly plan: VaultDiff\n readonly policy: ImportPolicy\n /** Apply every change in `plan` (filtered by `policy`) to the vault. */\n apply(): Promise<void>\n}\n\n/**\n * Build an import plan from a parsed JSON document. Same shape\n * `as-json.toObject()` produces — `Record<collection, records[]>`.\n */\nexport async function fromObject(\n vault: Vault,\n doc: AsJSONDocument,\n options: AsJSONImportOptions = {},\n): Promise<AsJSONImportPlan> {\n vault.assertCanImport('plaintext', 'json')\n const policy: ImportPolicy = options.policy ?? 'merge'\n const idKey = options.idKey ?? 'id'\n\n // Cast through unknown — diffVault is type-erased at the boundary\n // and AsJSONDocument's per-record type is `Record<string, unknown>`.\n const plan = await diffVault(vault, doc as unknown as Record<string, readonly Record<string, unknown>[]>, {\n ...(options.collections ? { collections: options.collections } : {}),\n idKey,\n })\n\n return {\n plan,\n policy,\n async apply(): Promise<void> {\n // Add and modify go through collection.put which runs the normal\n // permissions check + envelope encryption + ledger write.\n // Delete only runs under the 'replace' policy.\n // Wrapped via vault.noydb.transaction so a partial failure rolls\n // back every executed put. Routes through the txStrategy seam —\n // throws a clear error pointing at withTransactions() when the\n // strategy is not opted in.\n await vault.noydb.transaction((tx) => {\n const txVault = tx.vault(vault.name)\n for (const entry of plan.added) {\n txVault.collection(entry.collection).put(entry.id, entry.record)\n }\n if (policy !== 'insert-only') {\n for (const entry of plan.modified) {\n txVault.collection(entry.collection).put(entry.id, entry.record)\n }\n }\n if (policy === 'replace') {\n for (const entry of plan.deleted) {\n txVault.collection(entry.collection).delete(entry.id)\n }\n }\n })\n },\n }\n}\n\n/**\n * Build an import plan from a JSON string — parse, then dispatch to\n * `fromObject`. Convenience for the canonical \"load my exported file\"\n * workflow.\n */\nexport async function fromString(\n vault: Vault,\n json: string,\n options: AsJSONImportOptions = {},\n): Promise<AsJSONImportPlan> {\n let parsed: unknown\n try {\n parsed = JSON.parse(json)\n } catch (err) {\n throw new Error(`as-json.fromString: input is not valid JSON (${(err as Error).message})`)\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(\n `as-json.fromString: top-level value must be an object mapping collections → records[], got ${\n Array.isArray(parsed) ? 'array' : typeof parsed\n }`,\n )\n }\n return fromObject(vault, parsed as AsJSONDocument, options)\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/as-json** — structured JSON plaintext export for noy-db.\n *\n * Decrypts ACL-scoped records from a vault and emits one structured\n * JSON document grouping records by collection. Sibling to the core\n * `exportJSON()` helper — same shape, but gated behind\n * `canExportPlaintext` and paired with browser-download +\n * Node file-write helpers.\n *\n * **Scope.** Multi-collection per call (unlike `as-csv` which is\n * single-collection). Whole-vault by default; pass `collections` to\n * restrict.\n *\n * See `docs/patterns/as-exports.md` for the three-tier egress model\n * (Tier 1 in-memory → Tier 2 browser download → Tier 3 disk write).\n *\n * @packageDocumentation\n */\n\nimport type { Vault } from '@noy-db/hub'\n\nexport interface AsJSONOptions {\n /**\n * Collection allowlist. When omitted, every collection the caller\n * can read is included. Collections not in the caller's ACL silently\n * drop out even when listed here — ACL-scoping runs at the\n * `exportStream` layer.\n */\n readonly collections?: readonly string[]\n\n /**\n * Pretty-print with indentation. Default `2` (2-space indent). Pass\n * `0` or `false` for compact single-line output.\n */\n readonly pretty?: number | boolean\n\n /**\n * Include envelope metadata (`_v`, `_ts`, `_by`) alongside each\n * record. Default `false` — stripped so the JSON matches the shape\n * of the raw records the consumer originally put.\n */\n readonly includeMeta?: boolean\n}\n\nexport interface AsJSONDownloadOptions extends AsJSONOptions {\n /** Filename offered to the browser. Default `'vault-export.json'`. */\n readonly filename?: string\n}\n\nexport interface AsJSONWriteOptions extends AsJSONOptions {\n /** Required to write plaintext JSON to disk — Tier 3 risk gate. */\n readonly acknowledgeRisks: true\n}\n\n/**\n * Shape of the emitted document: one top-level key per collection,\n * each mapping to an array of record objects.\n */\nexport type AsJSONDocument = Record<string, readonly Record<string, unknown>[]>\n\n/**\n * Serialise the vault as a JSON string. Pure operation — no side\n * effects beyond the authorization check and audit ledger write.\n */\nexport async function toString(vault: Vault, options: AsJSONOptions = {}): Promise<string> {\n const doc = await toObject(vault, options)\n const indent = typeof options.pretty === 'number'\n ? options.pretty\n : options.pretty === false\n ? 0\n : 2\n return indent > 0 ? JSON.stringify(doc, null, indent) : JSON.stringify(doc)\n}\n\n/**\n * Serialise the vault as a plain JS object. Useful for in-process\n * pipelines that want to post-process the data before writing.\n */\nexport async function toObject(vault: Vault, options: AsJSONOptions = {}): Promise<AsJSONDocument> {\n vault.assertCanExport('plaintext', 'json')\n\n const allowlist = options.collections ? new Set(options.collections) : null\n const doc: Record<string, Record<string, unknown>[]> = {}\n for await (const chunk of vault.exportStream({ granularity: 'collection' })) {\n if (allowlist && !allowlist.has(chunk.collection)) continue\n const bucket = doc[chunk.collection] ?? (doc[chunk.collection] = [])\n for (const record of chunk.records) {\n if (options.includeMeta) {\n bucket.push(record as Record<string, unknown>)\n } else {\n bucket.push(stripMeta(record as Record<string, unknown>))\n }\n }\n }\n return doc\n}\n\n/**\n * Browser download — wraps `toString()` in a Blob and triggers the\n * browser's save-as prompt. Requires a DOM — in Node, use `write()`.\n */\nexport async function download(vault: Vault, options: AsJSONDownloadOptions = {}): Promise<void> {\n const json = await toString(vault, options)\n const filename = options.filename ?? 'vault-export.json'\n const blob = new Blob([json], { type: 'application/json;charset=utf-8' })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n a.click()\n URL.revokeObjectURL(url)\n}\n\n/**\n * Node file-write — persists the JSON to disk. Requires\n * `acknowledgeRisks: true` because plaintext outlives the process.\n */\nexport async function write(\n vault: Vault,\n path: string,\n options: AsJSONWriteOptions,\n): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error(\n 'as-json.write: acknowledgeRisks: true is required for on-disk plaintext output. ' +\n 'See docs/patterns/as-exports.md §\"The three tiers of \\\\\"plaintext out\\\\\"\"',\n )\n }\n const json = await toString(vault, options)\n const { writeFile } = await import('node:fs/promises')\n await writeFile(path, json, 'utf-8')\n}\n\nfunction stripMeta(record: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(record)) {\n if (key === '_v' || key === '_ts' || key === '_by' || key === '_iv' || key === '_data' || key === '_noydb') continue\n out[key] = value\n }\n return out\n}\n\n// ─── Reader ─────────────────────────────────────────────\n\nimport { diffVault, type VaultDiff } from '@noy-db/hub'\n\n/**\n * Reconciliation policy for `apply()`.\n *\n * - `'merge'` (default) — insert + update, never delete. Records\n * present in the live vault but absent from the file are left\n * intact.\n * - `'replace'` — full mirror. Records present in the live vault but\n * absent from the file are deleted.\n * - `'insert-only'` — only insert new records; skip both updates and\n * deletes. Useful for append-only ledgers.\n */\nexport type ImportPolicy = 'merge' | 'replace' | 'insert-only'\n\nexport interface AsJSONImportOptions {\n /** Restrict the diff + apply to a subset of collections. */\n readonly collections?: readonly string[]\n /** Field on each record that carries its id. Default `'id'`. */\n readonly idKey?: string\n /** Reconciliation policy. Default `'merge'`. */\n readonly policy?: ImportPolicy\n}\n\n/**\n * Output of `fromString` / `fromObject` — preview the changes a JSON\n * import would apply, then commit them with `apply()`. Two-step shape\n * keeps the diff cheap and lets consumers render review-and-confirm\n * UIs without a separate dry-run mode.\n */\nexport interface AsJSONImportPlan {\n readonly plan: VaultDiff\n readonly policy: ImportPolicy\n /** Apply every change in `plan` (filtered by `policy`) to the vault. */\n apply(): Promise<void>\n}\n\n/**\n * Build an import plan from a parsed JSON document. Same shape\n * `as-json.toObject()` produces — `Record<collection, records[]>`.\n */\nexport async function fromObject(\n vault: Vault,\n doc: AsJSONDocument,\n options: AsJSONImportOptions = {},\n): Promise<AsJSONImportPlan> {\n vault.assertCanImport('plaintext', 'json')\n const policy: ImportPolicy = options.policy ?? 'merge'\n const idKey = options.idKey ?? 'id'\n\n // Cast through unknown — diffVault is type-erased at the boundary\n // and AsJSONDocument's per-record type is `Record<string, unknown>`.\n const plan = await diffVault(vault, doc as unknown as Record<string, readonly Record<string, unknown>[]>, {\n ...(options.collections ? { collections: options.collections } : {}),\n idKey,\n })\n\n return {\n plan,\n policy,\n async apply(): Promise<void> {\n // Add and modify go through collection.put which runs the normal\n // permissions check + envelope encryption + ledger write.\n // Delete only runs under the 'replace' policy.\n // Wrapped via vault.noydb.transaction so a partial failure rolls\n // back every executed put. Routes through the txStrategy seam —\n // throws a clear error pointing at withTransactions() when the\n // strategy is not opted in.\n await vault.noydb.transaction((tx) => {\n const txVault = tx.vault(vault.name)\n for (const entry of plan.added) {\n txVault.collection(entry.collection).put(entry.id, entry.record, { reason: 'import:json' })\n }\n if (policy !== 'insert-only') {\n for (const entry of plan.modified) {\n txVault.collection(entry.collection).put(entry.id, entry.record, { reason: 'import:json' })\n }\n }\n if (policy === 'replace') {\n for (const entry of plan.deleted) {\n txVault.collection(entry.collection).delete(entry.id)\n }\n }\n })\n },\n }\n}\n\n/**\n * Build an import plan from a JSON string — parse, then dispatch to\n * `fromObject`. Convenience for the canonical \"load my exported file\"\n * workflow.\n */\nexport async function fromString(\n vault: Vault,\n json: string,\n options: AsJSONImportOptions = {},\n): Promise<AsJSONImportPlan> {\n let parsed: unknown\n try {\n parsed = JSON.parse(json)\n } catch (err) {\n throw new Error(`as-json.fromString: input is not valid JSON (${(err as Error).message})`)\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(\n `as-json.fromString: top-level value must be an object mapping collections → records[], got ${\n Array.isArray(parsed) ? 'array' : typeof parsed\n }`,\n )\n }\n return fromObject(vault, parsed as AsJSONDocument, options)\n}\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -66,11 +66,11 @@ async function fromObject(vault, doc, options = {}) {
66
66
  await vault.noydb.transaction((tx) => {
67
67
  const txVault = tx.vault(vault.name);
68
68
  for (const entry of plan.added) {
69
- txVault.collection(entry.collection).put(entry.id, entry.record);
69
+ txVault.collection(entry.collection).put(entry.id, entry.record, { reason: "import:json" });
70
70
  }
71
71
  if (policy !== "insert-only") {
72
72
  for (const entry of plan.modified) {
73
- txVault.collection(entry.collection).put(entry.id, entry.record);
73
+ txVault.collection(entry.collection).put(entry.id, entry.record, { reason: "import:json" });
74
74
  }
75
75
  }
76
76
  if (policy === "replace") {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/as-json** — structured JSON plaintext export for noy-db.\n *\n * Decrypts ACL-scoped records from a vault and emits one structured\n * JSON document grouping records by collection. Sibling to the core\n * `exportJSON()` helper — same shape, but gated behind\n * `canExportPlaintext` and paired with browser-download +\n * Node file-write helpers.\n *\n * **Scope.** Multi-collection per call (unlike `as-csv` which is\n * single-collection). Whole-vault by default; pass `collections` to\n * restrict.\n *\n * See `docs/patterns/as-exports.md` for the three-tier egress model\n * (Tier 1 in-memory → Tier 2 browser download → Tier 3 disk write).\n *\n * @packageDocumentation\n */\n\nimport type { Vault } from '@noy-db/hub'\n\nexport interface AsJSONOptions {\n /**\n * Collection allowlist. When omitted, every collection the caller\n * can read is included. Collections not in the caller's ACL silently\n * drop out even when listed here — ACL-scoping runs at the\n * `exportStream` layer.\n */\n readonly collections?: readonly string[]\n\n /**\n * Pretty-print with indentation. Default `2` (2-space indent). Pass\n * `0` or `false` for compact single-line output.\n */\n readonly pretty?: number | boolean\n\n /**\n * Include envelope metadata (`_v`, `_ts`, `_by`) alongside each\n * record. Default `false` — stripped so the JSON matches the shape\n * of the raw records the consumer originally put.\n */\n readonly includeMeta?: boolean\n}\n\nexport interface AsJSONDownloadOptions extends AsJSONOptions {\n /** Filename offered to the browser. Default `'vault-export.json'`. */\n readonly filename?: string\n}\n\nexport interface AsJSONWriteOptions extends AsJSONOptions {\n /** Required to write plaintext JSON to disk — Tier 3 risk gate. */\n readonly acknowledgeRisks: true\n}\n\n/**\n * Shape of the emitted document: one top-level key per collection,\n * each mapping to an array of record objects.\n */\nexport type AsJSONDocument = Record<string, readonly Record<string, unknown>[]>\n\n/**\n * Serialise the vault as a JSON string. Pure operation — no side\n * effects beyond the authorization check and audit ledger write.\n */\nexport async function toString(vault: Vault, options: AsJSONOptions = {}): Promise<string> {\n const doc = await toObject(vault, options)\n const indent = typeof options.pretty === 'number'\n ? options.pretty\n : options.pretty === false\n ? 0\n : 2\n return indent > 0 ? JSON.stringify(doc, null, indent) : JSON.stringify(doc)\n}\n\n/**\n * Serialise the vault as a plain JS object. Useful for in-process\n * pipelines that want to post-process the data before writing.\n */\nexport async function toObject(vault: Vault, options: AsJSONOptions = {}): Promise<AsJSONDocument> {\n vault.assertCanExport('plaintext', 'json')\n\n const allowlist = options.collections ? new Set(options.collections) : null\n const doc: Record<string, Record<string, unknown>[]> = {}\n for await (const chunk of vault.exportStream({ granularity: 'collection' })) {\n if (allowlist && !allowlist.has(chunk.collection)) continue\n const bucket = doc[chunk.collection] ?? (doc[chunk.collection] = [])\n for (const record of chunk.records) {\n if (options.includeMeta) {\n bucket.push(record as Record<string, unknown>)\n } else {\n bucket.push(stripMeta(record as Record<string, unknown>))\n }\n }\n }\n return doc\n}\n\n/**\n * Browser download — wraps `toString()` in a Blob and triggers the\n * browser's save-as prompt. Requires a DOM — in Node, use `write()`.\n */\nexport async function download(vault: Vault, options: AsJSONDownloadOptions = {}): Promise<void> {\n const json = await toString(vault, options)\n const filename = options.filename ?? 'vault-export.json'\n const blob = new Blob([json], { type: 'application/json;charset=utf-8' })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n a.click()\n URL.revokeObjectURL(url)\n}\n\n/**\n * Node file-write — persists the JSON to disk. Requires\n * `acknowledgeRisks: true` because plaintext outlives the process.\n */\nexport async function write(\n vault: Vault,\n path: string,\n options: AsJSONWriteOptions,\n): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error(\n 'as-json.write: acknowledgeRisks: true is required for on-disk plaintext output. ' +\n 'See docs/patterns/as-exports.md §\"The three tiers of \\\\\"plaintext out\\\\\"\"',\n )\n }\n const json = await toString(vault, options)\n const { writeFile } = await import('node:fs/promises')\n await writeFile(path, json, 'utf-8')\n}\n\nfunction stripMeta(record: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(record)) {\n if (key === '_v' || key === '_ts' || key === '_by' || key === '_iv' || key === '_data' || key === '_noydb') continue\n out[key] = value\n }\n return out\n}\n\n// ─── Reader ─────────────────────────────────────────────\n\nimport { diffVault, type VaultDiff } from '@noy-db/hub'\n\n/**\n * Reconciliation policy for `apply()`.\n *\n * - `'merge'` (default) — insert + update, never delete. Records\n * present in the live vault but absent from the file are left\n * intact.\n * - `'replace'` — full mirror. Records present in the live vault but\n * absent from the file are deleted.\n * - `'insert-only'` — only insert new records; skip both updates and\n * deletes. Useful for append-only ledgers.\n */\nexport type ImportPolicy = 'merge' | 'replace' | 'insert-only'\n\nexport interface AsJSONImportOptions {\n /** Restrict the diff + apply to a subset of collections. */\n readonly collections?: readonly string[]\n /** Field on each record that carries its id. Default `'id'`. */\n readonly idKey?: string\n /** Reconciliation policy. Default `'merge'`. */\n readonly policy?: ImportPolicy\n}\n\n/**\n * Output of `fromString` / `fromObject` — preview the changes a JSON\n * import would apply, then commit them with `apply()`. Two-step shape\n * keeps the diff cheap and lets consumers render review-and-confirm\n * UIs without a separate dry-run mode.\n */\nexport interface AsJSONImportPlan {\n readonly plan: VaultDiff\n readonly policy: ImportPolicy\n /** Apply every change in `plan` (filtered by `policy`) to the vault. */\n apply(): Promise<void>\n}\n\n/**\n * Build an import plan from a parsed JSON document. Same shape\n * `as-json.toObject()` produces — `Record<collection, records[]>`.\n */\nexport async function fromObject(\n vault: Vault,\n doc: AsJSONDocument,\n options: AsJSONImportOptions = {},\n): Promise<AsJSONImportPlan> {\n vault.assertCanImport('plaintext', 'json')\n const policy: ImportPolicy = options.policy ?? 'merge'\n const idKey = options.idKey ?? 'id'\n\n // Cast through unknown — diffVault is type-erased at the boundary\n // and AsJSONDocument's per-record type is `Record<string, unknown>`.\n const plan = await diffVault(vault, doc as unknown as Record<string, readonly Record<string, unknown>[]>, {\n ...(options.collections ? { collections: options.collections } : {}),\n idKey,\n })\n\n return {\n plan,\n policy,\n async apply(): Promise<void> {\n // Add and modify go through collection.put which runs the normal\n // permissions check + envelope encryption + ledger write.\n // Delete only runs under the 'replace' policy.\n // Wrapped via vault.noydb.transaction so a partial failure rolls\n // back every executed put. Routes through the txStrategy seam —\n // throws a clear error pointing at withTransactions() when the\n // strategy is not opted in.\n await vault.noydb.transaction((tx) => {\n const txVault = tx.vault(vault.name)\n for (const entry of plan.added) {\n txVault.collection(entry.collection).put(entry.id, entry.record)\n }\n if (policy !== 'insert-only') {\n for (const entry of plan.modified) {\n txVault.collection(entry.collection).put(entry.id, entry.record)\n }\n }\n if (policy === 'replace') {\n for (const entry of plan.deleted) {\n txVault.collection(entry.collection).delete(entry.id)\n }\n }\n })\n },\n }\n}\n\n/**\n * Build an import plan from a JSON string — parse, then dispatch to\n * `fromObject`. Convenience for the canonical \"load my exported file\"\n * workflow.\n */\nexport async function fromString(\n vault: Vault,\n json: string,\n options: AsJSONImportOptions = {},\n): Promise<AsJSONImportPlan> {\n let parsed: unknown\n try {\n parsed = JSON.parse(json)\n } catch (err) {\n throw new Error(`as-json.fromString: input is not valid JSON (${(err as Error).message})`)\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(\n `as-json.fromString: top-level value must be an object mapping collections → records[], got ${\n Array.isArray(parsed) ? 'array' : typeof parsed\n }`,\n )\n }\n return fromObject(vault, parsed as AsJSONDocument, options)\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/as-json** — structured JSON plaintext export for noy-db.\n *\n * Decrypts ACL-scoped records from a vault and emits one structured\n * JSON document grouping records by collection. Sibling to the core\n * `exportJSON()` helper — same shape, but gated behind\n * `canExportPlaintext` and paired with browser-download +\n * Node file-write helpers.\n *\n * **Scope.** Multi-collection per call (unlike `as-csv` which is\n * single-collection). Whole-vault by default; pass `collections` to\n * restrict.\n *\n * See `docs/patterns/as-exports.md` for the three-tier egress model\n * (Tier 1 in-memory → Tier 2 browser download → Tier 3 disk write).\n *\n * @packageDocumentation\n */\n\nimport type { Vault } from '@noy-db/hub'\n\nexport interface AsJSONOptions {\n /**\n * Collection allowlist. When omitted, every collection the caller\n * can read is included. Collections not in the caller's ACL silently\n * drop out even when listed here — ACL-scoping runs at the\n * `exportStream` layer.\n */\n readonly collections?: readonly string[]\n\n /**\n * Pretty-print with indentation. Default `2` (2-space indent). Pass\n * `0` or `false` for compact single-line output.\n */\n readonly pretty?: number | boolean\n\n /**\n * Include envelope metadata (`_v`, `_ts`, `_by`) alongside each\n * record. Default `false` — stripped so the JSON matches the shape\n * of the raw records the consumer originally put.\n */\n readonly includeMeta?: boolean\n}\n\nexport interface AsJSONDownloadOptions extends AsJSONOptions {\n /** Filename offered to the browser. Default `'vault-export.json'`. */\n readonly filename?: string\n}\n\nexport interface AsJSONWriteOptions extends AsJSONOptions {\n /** Required to write plaintext JSON to disk — Tier 3 risk gate. */\n readonly acknowledgeRisks: true\n}\n\n/**\n * Shape of the emitted document: one top-level key per collection,\n * each mapping to an array of record objects.\n */\nexport type AsJSONDocument = Record<string, readonly Record<string, unknown>[]>\n\n/**\n * Serialise the vault as a JSON string. Pure operation — no side\n * effects beyond the authorization check and audit ledger write.\n */\nexport async function toString(vault: Vault, options: AsJSONOptions = {}): Promise<string> {\n const doc = await toObject(vault, options)\n const indent = typeof options.pretty === 'number'\n ? options.pretty\n : options.pretty === false\n ? 0\n : 2\n return indent > 0 ? JSON.stringify(doc, null, indent) : JSON.stringify(doc)\n}\n\n/**\n * Serialise the vault as a plain JS object. Useful for in-process\n * pipelines that want to post-process the data before writing.\n */\nexport async function toObject(vault: Vault, options: AsJSONOptions = {}): Promise<AsJSONDocument> {\n vault.assertCanExport('plaintext', 'json')\n\n const allowlist = options.collections ? new Set(options.collections) : null\n const doc: Record<string, Record<string, unknown>[]> = {}\n for await (const chunk of vault.exportStream({ granularity: 'collection' })) {\n if (allowlist && !allowlist.has(chunk.collection)) continue\n const bucket = doc[chunk.collection] ?? (doc[chunk.collection] = [])\n for (const record of chunk.records) {\n if (options.includeMeta) {\n bucket.push(record as Record<string, unknown>)\n } else {\n bucket.push(stripMeta(record as Record<string, unknown>))\n }\n }\n }\n return doc\n}\n\n/**\n * Browser download — wraps `toString()` in a Blob and triggers the\n * browser's save-as prompt. Requires a DOM — in Node, use `write()`.\n */\nexport async function download(vault: Vault, options: AsJSONDownloadOptions = {}): Promise<void> {\n const json = await toString(vault, options)\n const filename = options.filename ?? 'vault-export.json'\n const blob = new Blob([json], { type: 'application/json;charset=utf-8' })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n a.click()\n URL.revokeObjectURL(url)\n}\n\n/**\n * Node file-write — persists the JSON to disk. Requires\n * `acknowledgeRisks: true` because plaintext outlives the process.\n */\nexport async function write(\n vault: Vault,\n path: string,\n options: AsJSONWriteOptions,\n): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error(\n 'as-json.write: acknowledgeRisks: true is required for on-disk plaintext output. ' +\n 'See docs/patterns/as-exports.md §\"The three tiers of \\\\\"plaintext out\\\\\"\"',\n )\n }\n const json = await toString(vault, options)\n const { writeFile } = await import('node:fs/promises')\n await writeFile(path, json, 'utf-8')\n}\n\nfunction stripMeta(record: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(record)) {\n if (key === '_v' || key === '_ts' || key === '_by' || key === '_iv' || key === '_data' || key === '_noydb') continue\n out[key] = value\n }\n return out\n}\n\n// ─── Reader ─────────────────────────────────────────────\n\nimport { diffVault, type VaultDiff } from '@noy-db/hub'\n\n/**\n * Reconciliation policy for `apply()`.\n *\n * - `'merge'` (default) — insert + update, never delete. Records\n * present in the live vault but absent from the file are left\n * intact.\n * - `'replace'` — full mirror. Records present in the live vault but\n * absent from the file are deleted.\n * - `'insert-only'` — only insert new records; skip both updates and\n * deletes. Useful for append-only ledgers.\n */\nexport type ImportPolicy = 'merge' | 'replace' | 'insert-only'\n\nexport interface AsJSONImportOptions {\n /** Restrict the diff + apply to a subset of collections. */\n readonly collections?: readonly string[]\n /** Field on each record that carries its id. Default `'id'`. */\n readonly idKey?: string\n /** Reconciliation policy. Default `'merge'`. */\n readonly policy?: ImportPolicy\n}\n\n/**\n * Output of `fromString` / `fromObject` — preview the changes a JSON\n * import would apply, then commit them with `apply()`. Two-step shape\n * keeps the diff cheap and lets consumers render review-and-confirm\n * UIs without a separate dry-run mode.\n */\nexport interface AsJSONImportPlan {\n readonly plan: VaultDiff\n readonly policy: ImportPolicy\n /** Apply every change in `plan` (filtered by `policy`) to the vault. */\n apply(): Promise<void>\n}\n\n/**\n * Build an import plan from a parsed JSON document. Same shape\n * `as-json.toObject()` produces — `Record<collection, records[]>`.\n */\nexport async function fromObject(\n vault: Vault,\n doc: AsJSONDocument,\n options: AsJSONImportOptions = {},\n): Promise<AsJSONImportPlan> {\n vault.assertCanImport('plaintext', 'json')\n const policy: ImportPolicy = options.policy ?? 'merge'\n const idKey = options.idKey ?? 'id'\n\n // Cast through unknown — diffVault is type-erased at the boundary\n // and AsJSONDocument's per-record type is `Record<string, unknown>`.\n const plan = await diffVault(vault, doc as unknown as Record<string, readonly Record<string, unknown>[]>, {\n ...(options.collections ? { collections: options.collections } : {}),\n idKey,\n })\n\n return {\n plan,\n policy,\n async apply(): Promise<void> {\n // Add and modify go through collection.put which runs the normal\n // permissions check + envelope encryption + ledger write.\n // Delete only runs under the 'replace' policy.\n // Wrapped via vault.noydb.transaction so a partial failure rolls\n // back every executed put. Routes through the txStrategy seam —\n // throws a clear error pointing at withTransactions() when the\n // strategy is not opted in.\n await vault.noydb.transaction((tx) => {\n const txVault = tx.vault(vault.name)\n for (const entry of plan.added) {\n txVault.collection(entry.collection).put(entry.id, entry.record, { reason: 'import:json' })\n }\n if (policy !== 'insert-only') {\n for (const entry of plan.modified) {\n txVault.collection(entry.collection).put(entry.id, entry.record, { reason: 'import:json' })\n }\n }\n if (policy === 'replace') {\n for (const entry of plan.deleted) {\n txVault.collection(entry.collection).delete(entry.id)\n }\n }\n })\n },\n }\n}\n\n/**\n * Build an import plan from a JSON string — parse, then dispatch to\n * `fromObject`. Convenience for the canonical \"load my exported file\"\n * workflow.\n */\nexport async function fromString(\n vault: Vault,\n json: string,\n options: AsJSONImportOptions = {},\n): Promise<AsJSONImportPlan> {\n let parsed: unknown\n try {\n parsed = JSON.parse(json)\n } catch (err) {\n throw new Error(`as-json.fromString: input is not valid JSON (${(err as Error).message})`)\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(\n `as-json.fromString: top-level value must be an object mapping collections → records[], got ${\n Array.isArray(parsed) ? 'array' : typeof parsed\n }`,\n )\n }\n return fromObject(vault, parsed as AsJSONDocument, options)\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noy-db/as-json",
3
- "version": "0.1.0-pre.8",
3
+ "version": "0.2.0-pre.1",
4
4
  "description": "Structured JSON plaintext export for noy-db — decrypts records and emits one JSON document per vault. Gated by RFC #249 canExportPlaintext capability; writes an audit-ledger entry on every call. Part of the @noy-db/as-* portable-artefact family (plaintext tier).",
5
5
  "license": "MIT",
6
6
  "author": "vLannaAi <vicio@lanna.ai>",
@@ -39,11 +39,11 @@
39
39
  "node": ">=18.0.0"
40
40
  },
41
41
  "peerDependencies": {
42
- "@noy-db/hub": "0.1.0-pre.8"
42
+ "@noy-db/hub": "0.2.0-pre.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^22.0.0",
46
- "@noy-db/hub": "0.1.0-pre.8"
46
+ "@noy-db/hub": "0.2.0-pre.1"
47
47
  },
48
48
  "keywords": [
49
49
  "noy-db",