@noy-db/as-json 0.8.0-pre.0 → 0.9.0-pre.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 +1 -1
- package/dist/index.d.ts +17 -2
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -12,8 +12,23 @@ import { Vault } from '@noy-db/hub';
|
|
|
12
12
|
* Node file-write helpers.
|
|
13
13
|
*
|
|
14
14
|
* **Scope.** Multi-collection per call (unlike `as-csv` which is
|
|
15
|
-
* single-collection). Whole-vault by default
|
|
16
|
-
*
|
|
15
|
+
* single-collection). Whole-vault by default.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ **Through `@noy-db/hub@0.8.0`, `collections` FILTERS THE OUTPUT and
|
|
18
|
+
* does not scope the read** — hub streams every collection and drops
|
|
19
|
+
* the ones you did not name, so asking for one still decrypts the
|
|
20
|
+
* rest. Measured in the published 0.8.0 tarball, not inferred: the
|
|
21
|
+
* port calls `exportStream({ granularity: 'collection' })` with no
|
|
22
|
+
* collection argument and applies the filter per chunk afterwards.
|
|
23
|
+
* That matters here because `as-*` sees plaintext by design — a
|
|
24
|
+
* property of the read path, not a defect in this package.
|
|
25
|
+
*
|
|
26
|
+
* ⭐ **Anchored to a version because it is changing.** core#45(b)
|
|
27
|
+
* narrows the read upstream; the claim above holds for every hub
|
|
28
|
+
* published as of 2026-09-18 and becomes false at core's next cut.
|
|
29
|
+
* **Re-measure against the hub you actually resolve** — unpack its
|
|
30
|
+
* tarball and look — rather than trusting either version of this
|
|
31
|
+
* sentence.
|
|
17
32
|
*
|
|
18
33
|
* See `docs/patterns/as-exports.md` for the three-tier egress model
|
|
19
34
|
* (Tier 1 in-memory → Tier 2 browser download → Tier 3 disk write).
|
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 * `assertCanExport('plaintext')` 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 {\n ExportChunk,\n NoydbFormat,\n DecodedChunk,\n FormatExportOptions,\n} from '@noy-db/hub/as'\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 /**\n * Apply the hub's `applyListProjection` read-projection before\n * serialising records. `true` redacts only `classifiedFields` (mask /\n * omit / rider, per the field's preset). The object form additionally\n * redacts fields carrying a plain `fieldMeta` `sensitivity: 'pii' |\n * 'secret'` tag, per `sensitivity: 'omit' | 'mask'`.\n *\n * Caveat: `describe()` reflects the declarations of *this session's*\n * collection instance — redaction only takes effect when the\n * collection was opened (this call or earlier in the session) with\n * its `classifiedFields` / `fieldMeta` options. This is presentation-\n * layer redaction; it never affects what's on disk. Sealed handles\n * are unaffected either way — they always serialize as `'[sealed]'`,\n * so ciphertext never leaks regardless of this option. Rider companion\n * fields (e.g. `pan_last4`) remain visible as their own keys — they\n * are safe write-time projections.\n */\n readonly redact?: boolean | { readonly sensitivity: 'omit' | 'mask' }\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 */\n/**\n * The pure encoders — records in, JSON out. Already gated and already redacted\n * by hub; neither has a vault (ADR 0004).\n */\nfunction encodeJsonDoc(\n chunks: readonly ExportChunk[],\n options: AsJSONFormatOptions,\n): AsJSONDocument {\n const doc: Record<string, Record<string, unknown>[]> = {}\n for (const chunk of chunks) {\n const bucket = doc[chunk.collection] ?? (doc[chunk.collection] = [])\n for (const record of chunk.records) {\n const r = record as Record<string, unknown>\n bucket.push(options.includeMeta ? r : stripMeta(r))\n }\n }\n return doc\n}\n\nfunction encodeJson(chunks: readonly ExportChunk[], options: AsJSONFormatOptions): string {\n const indent = typeof options.pretty === 'number' ? options.pretty : options.pretty === false ? 0 : 2\n return JSON.stringify(encodeJsonDoc(chunks, options), null, indent)\n}\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 */\n/** Options a JSON format instance carries. Read concerns live on `vault.export`. */\nexport interface AsJSONFormatOptions {\n /** Indent width, or `false` for compact. Default 2. */\n readonly pretty?: number | boolean\n /** Keep `_noydb_*` metadata fields. Default false. */\n readonly includeMeta?: boolean\n}\n\n/**\n * The JSON format — the `as-*` port instance.\n *\n * Unlike CSV and XML, JSON carries collection names in the payload, so\n * `vault.import(asJson(), doc)` needs no `{ collection }`.\n */\nexport function asJson(options: AsJSONFormatOptions = {}): NoydbFormat<string> {\n return {\n id: 'json',\n extension: 'json',\n mimeType: 'application/json;charset=utf-8',\n tier: 'plaintext',\n encode: (chunks) => encodeJson(chunks, options),\n decode: (input) => decodeJson(input),\n }\n}\n\n/** Only the keys actually set — `exactOptionalPropertyTypes` is on. */\nfunction fmtOpts(o: AsJSONOptions): AsJSONFormatOptions {\n return {\n ...(o.pretty !== undefined ? { pretty: o.pretty } : {}),\n ...(o.includeMeta !== undefined ? { includeMeta: o.includeMeta } : {}),\n }\n}\n\n\nfunction readOpts(o: AsJSONOptions): FormatExportOptions {\n return {\n ...(o.collections ? { collections: o.collections } : {}),\n ...(o.redact !== undefined && o.redact !== false\n ? { redact: o.redact === true ? true : { sensitivity: o.redact.sensitivity } }\n : {}),\n }\n}\n\n/**\n * Serialise to a JSON string.\n *\n * Kept as a thin wrapper rather than removed, unlike as-csv/as-sql/as-xml: the\n * gate, the read and the redaction still moved to hub, and this is now three\n * lines over `vault.export`. Its sibling `toObject` is the reason — a document\n * is not bytes, so `NoydbFormat<string>` cannot express it, and removing one\n * while keeping the other would be a worse API than keeping both.\n */\nexport async function toString(vault: Vault, options: AsJSONOptions = {}): Promise<string> {\n return vault.export(asJson(fmtOpts(options)), readOpts(options))\n}\n\n/**\n * Serialise to the parsed `{ collection: records[] }` document.\n *\n * The one export shape the format port does not carry, because a format\n * produces bytes by definition. Round-tripping through `encode` keeps a single\n * implementation rather than a second walk of the chunks.\n */\nexport async function toObject(vault: Vault, options: AsJSONOptions = {}): Promise<AsJSONDocument> {\n return JSON.parse(await toString(vault, options)) as AsJSONDocument\n}\n\n/** Browser download. Hub gates, reads and redacts; this wraps the bytes. */\nexport async function download(vault: Vault, options: AsJSONDownloadOptions = {}): Promise<void> {\n const fmt = asJson(fmtOpts(options))\n const json = await vault.export(fmt, readOpts(options))\n const url = URL.createObjectURL(new Blob([json], { type: fmt.mimeType }))\n const a = document.createElement('a')\n a.href = url\n a.download = options.filename ?? `vault-export.${fmt.extension}`\n a.click()\n URL.revokeObjectURL(url)\n}\n\n/**\n * Node file write. Not in hub because `hub-portable` forbids Node builtins\n * there. The gate, the read and the redaction all moved.\n */\nexport async function write(vault: Vault, path: string, options: AsJSONWriteOptions): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error('as-json.write: acknowledgeRisks: true is required for on-disk plaintext output.')\n }\n const json = await vault.export(asJson(fmtOpts(options)), readOpts(options))\n const { writeFile } = await import('node:fs/promises')\n await writeFile(path, json, 'utf8')\n}\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\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 */\n// Hub-owned as of 0.7 (ADR 0004). This line replaced a local declaration that\n// existed identically in six as-* packages, with nothing comparing them.\nimport type { ImportPolicy, ImportPlan } from '@noy-db/hub/as'\nexport type { ImportPolicy }\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 type AsJSONImportPlan = ImportPlan\n\n/**\n * Build an import plan from a parsed JSON document. Same shape\n * `as-json.toObject()` produces — `Record<collection, records[]>`.\n */\n/**\n * The pure decoder — JSON in, records out. No vault, no gate, no diff: hub\n * gates, plans against the live vault and owns `apply()`.\n */\nfunction decodeJson(json: string): readonly DecodedChunk[] {\n let parsed: unknown\n try {\n parsed = JSON.parse(json)\n } catch (err) {\n throw new Error(`as-json decode: input is not valid JSON (${(err as Error).message})`)\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('as-json decode: expected an object mapping { collection: records[] }')\n }\n // JSON DOES carry collection names — one key per collection — so unlike CSV\n // and XML this format needs no `{ collection }` from the caller.\n return Object.entries(parsed as Record<string, unknown>).map(([collection, records]) => ({\n collection,\n records: Array.isArray(records) ? records : [],\n }))\n}\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 * `assertCanExport('plaintext')` 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.\n *\n * ⚠️ **Through `@noy-db/hub@0.8.0`, `collections` FILTERS THE OUTPUT and\n * does not scope the read** — hub streams every collection and drops\n * the ones you did not name, so asking for one still decrypts the\n * rest. Measured in the published 0.8.0 tarball, not inferred: the\n * port calls `exportStream({ granularity: 'collection' })` with no\n * collection argument and applies the filter per chunk afterwards.\n * That matters here because `as-*` sees plaintext by design — a\n * property of the read path, not a defect in this package.\n *\n * ⭐ **Anchored to a version because it is changing.** core#45(b)\n * narrows the read upstream; the claim above holds for every hub\n * published as of 2026-09-18 and becomes false at core's next cut.\n * **Re-measure against the hub you actually resolve** — unpack its\n * tarball and look — rather than trusting either version of this\n * sentence.\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 {\n ExportChunk,\n NoydbFormat,\n DecodedChunk,\n FormatExportOptions,\n} from '@noy-db/hub/as'\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 /**\n * Apply the hub's `applyListProjection` read-projection before\n * serialising records. `true` redacts only `classifiedFields` (mask /\n * omit / rider, per the field's preset). The object form additionally\n * redacts fields carrying a plain `fieldMeta` `sensitivity: 'pii' |\n * 'secret'` tag, per `sensitivity: 'omit' | 'mask'`.\n *\n * Caveat: `describe()` reflects the declarations of *this session's*\n * collection instance — redaction only takes effect when the\n * collection was opened (this call or earlier in the session) with\n * its `classifiedFields` / `fieldMeta` options. This is presentation-\n * layer redaction; it never affects what's on disk. Sealed handles\n * are unaffected either way — they always serialize as `'[sealed]'`,\n * so ciphertext never leaks regardless of this option. Rider companion\n * fields (e.g. `pan_last4`) remain visible as their own keys — they\n * are safe write-time projections.\n */\n readonly redact?: boolean | { readonly sensitivity: 'omit' | 'mask' }\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 */\n/**\n * The pure encoders — records in, JSON out. Already gated and already redacted\n * by hub; neither has a vault (ADR 0004).\n */\nfunction encodeJsonDoc(\n chunks: readonly ExportChunk[],\n options: AsJSONFormatOptions,\n): AsJSONDocument {\n const doc: Record<string, Record<string, unknown>[]> = {}\n for (const chunk of chunks) {\n const bucket = doc[chunk.collection] ?? (doc[chunk.collection] = [])\n for (const record of chunk.records) {\n const r = record as Record<string, unknown>\n bucket.push(options.includeMeta ? r : stripMeta(r))\n }\n }\n return doc\n}\n\nfunction encodeJson(chunks: readonly ExportChunk[], options: AsJSONFormatOptions): string {\n const indent = typeof options.pretty === 'number' ? options.pretty : options.pretty === false ? 0 : 2\n return JSON.stringify(encodeJsonDoc(chunks, options), null, indent)\n}\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 */\n/** Options a JSON format instance carries. Read concerns live on `vault.export`. */\nexport interface AsJSONFormatOptions {\n /** Indent width, or `false` for compact. Default 2. */\n readonly pretty?: number | boolean\n /** Keep `_noydb_*` metadata fields. Default false. */\n readonly includeMeta?: boolean\n}\n\n/**\n * The JSON format — the `as-*` port instance.\n *\n * Unlike CSV and XML, JSON carries collection names in the payload, so\n * `vault.import(asJson(), doc)` needs no `{ collection }`.\n */\nexport function asJson(options: AsJSONFormatOptions = {}): NoydbFormat<string> {\n return {\n id: 'json',\n extension: 'json',\n mimeType: 'application/json;charset=utf-8',\n tier: 'plaintext',\n encode: (chunks) => encodeJson(chunks, options),\n decode: (input) => decodeJson(input),\n }\n}\n\n/** Only the keys actually set — `exactOptionalPropertyTypes` is on. */\nfunction fmtOpts(o: AsJSONOptions): AsJSONFormatOptions {\n return {\n ...(o.pretty !== undefined ? { pretty: o.pretty } : {}),\n ...(o.includeMeta !== undefined ? { includeMeta: o.includeMeta } : {}),\n }\n}\n\n\nfunction readOpts(o: AsJSONOptions): FormatExportOptions {\n return {\n ...(o.collections ? { collections: o.collections } : {}),\n ...(o.redact !== undefined && o.redact !== false\n ? { redact: o.redact === true ? true : { sensitivity: o.redact.sensitivity } }\n : {}),\n }\n}\n\n/**\n * Serialise to a JSON string.\n *\n * Kept as a thin wrapper rather than removed, unlike as-csv/as-sql/as-xml: the\n * gate, the read and the redaction still moved to hub, and this is now three\n * lines over `vault.export`. Its sibling `toObject` is the reason — a document\n * is not bytes, so `NoydbFormat<string>` cannot express it, and removing one\n * while keeping the other would be a worse API than keeping both.\n */\nexport async function toString(vault: Vault, options: AsJSONOptions = {}): Promise<string> {\n return vault.export(asJson(fmtOpts(options)), readOpts(options))\n}\n\n/**\n * Serialise to the parsed `{ collection: records[] }` document.\n *\n * The one export shape the format port does not carry, because a format\n * produces bytes by definition. Round-tripping through `encode` keeps a single\n * implementation rather than a second walk of the chunks.\n */\nexport async function toObject(vault: Vault, options: AsJSONOptions = {}): Promise<AsJSONDocument> {\n return JSON.parse(await toString(vault, options)) as AsJSONDocument\n}\n\n/** Browser download. Hub gates, reads and redacts; this wraps the bytes. */\nexport async function download(vault: Vault, options: AsJSONDownloadOptions = {}): Promise<void> {\n const fmt = asJson(fmtOpts(options))\n const json = await vault.export(fmt, readOpts(options))\n const url = URL.createObjectURL(new Blob([json], { type: fmt.mimeType }))\n const a = document.createElement('a')\n a.href = url\n a.download = options.filename ?? `vault-export.${fmt.extension}`\n a.click()\n URL.revokeObjectURL(url)\n}\n\n/**\n * Node file write. Not in hub because `hub-portable` forbids Node builtins\n * there. The gate, the read and the redaction all moved.\n */\nexport async function write(vault: Vault, path: string, options: AsJSONWriteOptions): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error('as-json.write: acknowledgeRisks: true is required for on-disk plaintext output.')\n }\n const json = await vault.export(asJson(fmtOpts(options)), readOpts(options))\n const { writeFile } = await import('node:fs/promises')\n await writeFile(path, json, 'utf8')\n}\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\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 */\n// Hub-owned as of 0.7 (ADR 0004). This line replaced a local declaration that\n// existed identically in six as-* packages, with nothing comparing them.\nimport type { ImportPolicy, ImportPlan } from '@noy-db/hub/as'\nexport type { ImportPolicy }\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 type AsJSONImportPlan = ImportPlan\n\n/**\n * Build an import plan from a parsed JSON document. Same shape\n * `as-json.toObject()` produces — `Record<collection, records[]>`.\n */\n/**\n * The pure decoder — JSON in, records out. No vault, no gate, no diff: hub\n * gates, plans against the live vault and owns `apply()`.\n */\nfunction decodeJson(json: string): readonly DecodedChunk[] {\n let parsed: unknown\n try {\n parsed = JSON.parse(json)\n } catch (err) {\n throw new Error(`as-json decode: input is not valid JSON (${(err as Error).message})`)\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('as-json decode: expected an object mapping { collection: records[] }')\n }\n // JSON DOES carry collection names — one key per collection — so unlike CSV\n // and XML this format needs no `{ collection }` from the caller.\n return Object.entries(parsed as Record<string, unknown>).map(([collection, records]) => ({\n collection,\n records: Array.isArray(records) ? records : [],\n }))\n}\n\n"],"mappings":";AA4GA,SAAS,cACP,QACA,SACgB;AAChB,QAAM,MAAiD,CAAC;AACxD,aAAW,SAAS,QAAQ;AAC1B,UAAM,SAAS,IAAI,MAAM,UAAU,MAAM,IAAI,MAAM,UAAU,IAAI,CAAC;AAClE,eAAW,UAAU,MAAM,SAAS;AAClC,YAAM,IAAI;AACV,aAAO,KAAK,QAAQ,cAAc,IAAI,UAAU,CAAC,CAAC;AAAA,IACpD;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,WAAW,QAAgC,SAAsC;AACxF,QAAM,SAAS,OAAO,QAAQ,WAAW,WAAW,QAAQ,SAAS,QAAQ,WAAW,QAAQ,IAAI;AACpG,SAAO,KAAK,UAAU,cAAc,QAAQ,OAAO,GAAG,MAAM,MAAM;AACpE;AAqBO,SAAS,OAAO,UAA+B,CAAC,GAAwB;AAC7E,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,MAAM;AAAA,IACN,QAAQ,CAAC,WAAW,WAAW,QAAQ,OAAO;AAAA,IAC9C,QAAQ,CAAC,UAAU,WAAW,KAAK;AAAA,EACrC;AACF;AAGA,SAAS,QAAQ,GAAuC;AACtD,SAAO;AAAA,IACL,GAAI,EAAE,WAAW,SAAY,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,IACrD,GAAI,EAAE,gBAAgB,SAAY,EAAE,aAAa,EAAE,YAAY,IAAI,CAAC;AAAA,EACtE;AACF;AAGA,SAAS,SAAS,GAAuC;AACvD,SAAO;AAAA,IACL,GAAI,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,IAAI,CAAC;AAAA,IACtD,GAAI,EAAE,WAAW,UAAa,EAAE,WAAW,QACvC,EAAE,QAAQ,EAAE,WAAW,OAAO,OAAO,EAAE,aAAa,EAAE,OAAO,YAAY,EAAE,IAC3E,CAAC;AAAA,EACP;AACF;AAWA,eAAsB,SAAS,OAAc,UAAyB,CAAC,GAAoB;AACzF,SAAO,MAAM,OAAO,OAAO,QAAQ,OAAO,CAAC,GAAG,SAAS,OAAO,CAAC;AACjE;AASA,eAAsB,SAAS,OAAc,UAAyB,CAAC,GAA4B;AACjG,SAAO,KAAK,MAAM,MAAM,SAAS,OAAO,OAAO,CAAC;AAClD;AAGA,eAAsB,SAAS,OAAc,UAAiC,CAAC,GAAkB;AAC/F,QAAM,MAAM,OAAO,QAAQ,OAAO,CAAC;AACnC,QAAM,OAAO,MAAM,MAAM,OAAO,KAAK,SAAS,OAAO,CAAC;AACtD,QAAM,MAAM,IAAI,gBAAgB,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE,MAAM,IAAI,SAAS,CAAC,CAAC;AACxE,QAAM,IAAI,SAAS,cAAc,GAAG;AACpC,IAAE,OAAO;AACT,IAAE,WAAW,QAAQ,YAAY,gBAAgB,IAAI,SAAS;AAC9D,IAAE,MAAM;AACR,MAAI,gBAAgB,GAAG;AACzB;AAMA,eAAsB,MAAM,OAAc,MAAc,SAA4C;AAClG,MAAI,QAAQ,qBAAqB,MAAM;AACrC,UAAM,IAAI,MAAM,iFAAiF;AAAA,EACnG;AACA,QAAM,OAAO,MAAM,MAAM,OAAO,OAAO,QAAQ,OAAO,CAAC,GAAG,SAAS,OAAO,CAAC;AAC3E,QAAM,EAAE,UAAU,IAAI,MAAM,OAAO,aAAkB;AACrD,QAAM,UAAU,MAAM,MAAM,MAAM;AACpC;AAGA,SAAS,UAAU,QAA0D;AAC3E,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,QAAQ,QAAQ,QAAQ,SAAS,QAAQ,SAAS,QAAQ,SAAS,QAAQ,WAAW,QAAQ,SAAU;AAC5G,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO;AACT;AA8CA,SAAS,WAAW,MAAuC;AACzD,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,IAAI;AAAA,EAC1B,SAAS,KAAK;AACZ,UAAM,IAAI,MAAM,4CAA6C,IAAc,OAAO,GAAG;AAAA,EACvF;AACA,MAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AAC1E,UAAM,IAAI,MAAM,sEAAsE;AAAA,EACxF;AAGA,SAAO,OAAO,QAAQ,MAAiC,EAAE,IAAI,CAAC,CAAC,YAAY,OAAO,OAAO;AAAA,IACvF;AAAA,IACA,SAAS,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC;AAAA,EAC/C,EAAE;AACJ;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noy-db/as-json",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-pre.0",
|
|
4
4
|
"description": "Structured JSON plaintext export for noy-db — decrypts records and emits one JSON document per vault. Gated by `vault.assertCanExport('plaintext', …)` capability; writes an audit-ledger entry on every call. Part of the @noy-db/as-* portable-artefact family (plaintext tier).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "vLannaAi <vicio@lanna.ai>",
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
"node": ">=22.0.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@noy-db/hub": "^0.7.0 || ^0.7.1-pre.0 || ^0.8.0-pre.0"
|
|
36
|
+
"@noy-db/hub": "^0.7.0 || ^0.7.1-pre.0 || ^0.8.0-pre.0 || ^0.9.0-pre.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@noy-db/hub": "0.
|
|
40
|
-
"@noy-db/
|
|
39
|
+
"@noy-db/hub": "0.9.0-pre.2",
|
|
40
|
+
"@noy-db/ports": "0.9.0-pre.2",
|
|
41
41
|
"@types/node": "^22.0.0",
|
|
42
42
|
"happy-dom": "^18.0.0"
|
|
43
43
|
},
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"access": "public",
|
|
54
54
|
"tag": "latest"
|
|
55
55
|
},
|
|
56
|
+
"dependencies": {},
|
|
56
57
|
"scripts": {
|
|
57
58
|
"build": "tsup",
|
|
58
59
|
"test": "vitest run",
|