@noy-db/as-ndjson 0.3.0-pre.1 → 0.3.0-pre.3

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.d.ts CHANGED
@@ -28,6 +28,27 @@ interface AsNDJSONOptions {
28
28
  readonly includeMeta?: boolean;
29
29
  /** Name of the routing field. Default `'_schema'`. */
30
30
  readonly schemaField?: string;
31
+ /**
32
+ * Apply the hub's `applyListProjection` read-projection before
33
+ * serialising each record. `true` redacts only `classifiedFields` (mask /
34
+ * omit / rider, per the field's preset). The object form additionally
35
+ * redacts fields carrying a plain `fieldMeta` `sensitivity: 'pii' |
36
+ * 'secret'` tag, per `sensitivity: 'omit' | 'mask'`.
37
+ *
38
+ * Caveat: `describe()` reflects the declarations of *this session's*
39
+ * collection instance — redaction only takes effect when the
40
+ * collection was opened (this call or earlier in the session) with
41
+ * its `classifiedFields` / `fieldMeta` options. This is presentation-
42
+ * layer redaction; it never affects what's on disk. Sealed handles
43
+ * are unaffected either way — they always serialize as `'[sealed]'`,
44
+ * so ciphertext never leaks regardless of this option.
45
+ *
46
+ * Rider companion fields (e.g. `pan_last4`) remain visible as their own
47
+ * keys — they are safe write-time projections.
48
+ */
49
+ readonly redact?: boolean | {
50
+ readonly sensitivity: 'omit' | 'mask';
51
+ };
31
52
  }
32
53
  interface AsNDJSONDownloadOptions extends AsNDJSONOptions {
33
54
  /** Filename offered to the browser. Default `'vault-export.ndjson'`. */
package/dist/index.js CHANGED
@@ -1,13 +1,31 @@
1
1
  // src/index.ts
2
+ import { applyListProjection } from "@noy-db/hub";
2
3
  import { diffVault } from "@noy-db/hub";
3
4
  async function* stream(vault, options = {}) {
4
5
  vault.assertCanExport("plaintext", "ndjson");
5
6
  const allowlist = options.collections ? new Set(options.collections) : null;
6
7
  const schemaField = options.schemaField ?? "_schema";
8
+ const descCache = /* @__PURE__ */ new Map();
7
9
  for await (const chunk of vault.exportStream({ granularity: "record" })) {
8
10
  if (allowlist && !allowlist.has(chunk.collection)) continue;
11
+ let desc;
12
+ if (options.redact !== void 0 && options.redact !== false) {
13
+ if (!descCache.has(chunk.collection)) {
14
+ descCache.set(chunk.collection, vault.collection(chunk.collection).describe());
15
+ }
16
+ desc = descCache.get(chunk.collection);
17
+ }
9
18
  for (const record of chunk.records) {
10
- const base = options.includeMeta ? record : stripMeta(record);
19
+ let base;
20
+ if (options.includeMeta) {
21
+ base = record;
22
+ } else {
23
+ base = stripMeta(record);
24
+ }
25
+ if (desc !== void 0) {
26
+ const projectionOpts = typeof options.redact === "object" ? { sensitivity: options.redact.sensitivity } : void 0;
27
+ base = applyListProjection(desc, base, projectionOpts);
28
+ }
11
29
  const out = { [schemaField]: chunk.collection, ...base };
12
30
  yield JSON.stringify(out);
13
31
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/as-ndjson** — newline-delimited JSON plaintext export.\n *\n * Streaming-friendly sibling to `@noy-db/as-json`. Emits one JSON\n * object per line, each carrying a `_schema` field naming the source\n * collection so a downstream reader can route records without a\n * separate header pass:\n *\n * ```\n * {\"_schema\":\"invoices\",\"id\":\"i1\",\"amount\":100,...}\n * {\"_schema\":\"invoices\",\"id\":\"i2\",\"amount\":250,...}\n * {\"_schema\":\"payments\",\"id\":\"p1\",\"invoiceId\":\"i1\",\"amount\":100,...}\n * ```\n *\n * Best for large vaults (10K+ records) where loading a single JSON\n * array into memory is clumsy. Pipes cleanly into `jq`, `fx`, log\n * stacks, or any line-oriented reducer.\n *\n * @packageDocumentation\n */\n\nimport type { Vault } from '@noy-db/hub'\n\nexport interface AsNDJSONOptions {\n /** Collection allowlist. Omit for every collection the caller can read. */\n readonly collections?: readonly string[]\n /** Include envelope metadata (`_v`, `_ts`, `_by`) on each line. Default `false`. */\n readonly includeMeta?: boolean\n /** Name of the routing field. Default `'_schema'`. */\n readonly schemaField?: string\n}\n\nexport interface AsNDJSONDownloadOptions extends AsNDJSONOptions {\n /** Filename offered to the browser. Default `'vault-export.ndjson'`. */\n readonly filename?: string\n}\n\nexport interface AsNDJSONWriteOptions extends AsNDJSONOptions {\n /** Required to write plaintext NDJSON to disk — Tier 3 risk gate. */\n readonly acknowledgeRisks: true\n}\n\n/**\n * Async iterator emitting one NDJSON-formatted line per record (no\n * trailing newline — the caller decides). The authorization check\n * runs before the first record is produced.\n */\nexport async function* stream(vault: Vault, options: AsNDJSONOptions = {}): AsyncGenerator<string> {\n vault.assertCanExport('plaintext', 'ndjson')\n const allowlist = options.collections ? new Set(options.collections) : null\n const schemaField = options.schemaField ?? '_schema'\n\n for await (const chunk of vault.exportStream({ granularity: 'record' })) {\n if (allowlist && !allowlist.has(chunk.collection)) continue\n for (const record of chunk.records) {\n const base = options.includeMeta\n ? (record as Record<string, unknown>)\n : stripMeta(record as Record<string, unknown>)\n const out: Record<string, unknown> = { [schemaField]: chunk.collection, ...base }\n yield JSON.stringify(out)\n }\n }\n}\n\n/** Collect the full NDJSON export into a single string (in-memory). */\nexport async function toString(vault: Vault, options: AsNDJSONOptions = {}): Promise<string> {\n const lines: string[] = []\n for await (const line of stream(vault, options)) lines.push(line)\n return lines.join('\\n')\n}\n\n/** Browser download wrapping `toString()` in a Blob save-as prompt. */\nexport async function download(vault: Vault, options: AsNDJSONDownloadOptions = {}): Promise<void> {\n const ndjson = await toString(vault, options)\n const filename = options.filename ?? 'vault-export.ndjson'\n const blob = new Blob([ndjson + '\\n'], { type: 'application/x-ndjson;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 pipe: stream each line into a WritableStream-like sink (a\n * `fs.WriteStream`, `process.stdout`, or any `.write(chunk)` +\n * `.end()` duck). Memory usage is O(one record) regardless of vault\n * size.\n */\nexport async function pipe(\n vault: Vault,\n sink: { write(chunk: string): unknown; end?(): void },\n options: AsNDJSONWriteOptions,\n): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error(\n 'as-ndjson.pipe: 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 for await (const line of stream(vault, options)) {\n sink.write(line + '\\n')\n }\n sink.end?.()\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\nexport type ImportPolicy = 'merge' | 'replace' | 'insert-only'\n\nexport interface AsNDJSONImportOptions {\n /**\n * Target collection. NDJSON is one record per line — every record\n * lands in the same collection. Required.\n */\n readonly collection: 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\nexport interface AsNDJSONImportPlan {\n readonly plan: VaultDiff\n readonly policy: ImportPolicy\n apply(): Promise<void>\n}\n\n/**\n * Parse newline-delimited JSON into records and build an import plan\n * for one collection. Empty lines and lines with leading whitespace\n * are ignored. A single malformed line throws — callers that want\n * lenient parsing can pre-filter their input.\n */\nexport async function fromString(\n vault: Vault,\n ndjson: string,\n options: AsNDJSONImportOptions,\n): Promise<AsNDJSONImportPlan> {\n vault.assertCanImport('plaintext', 'ndjson')\n const policy: ImportPolicy = options.policy ?? 'merge'\n const idKey = options.idKey ?? 'id'\n\n const records: Record<string, unknown>[] = []\n let lineNo = 0\n for (const raw of ndjson.split('\\n')) {\n lineNo++\n const line = raw.trim()\n if (!line) continue\n let parsed: unknown\n try {\n parsed = JSON.parse(line)\n } catch (err) {\n throw new Error(`as-ndjson.fromString: malformed JSON on line ${lineNo}: ${(err as Error).message}`)\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(`as-ndjson.fromString: line ${lineNo} is not a JSON object`)\n }\n records.push(parsed as Record<string, unknown>)\n }\n\n const plan = await diffVault(vault, { [options.collection]: records }, {\n collections: [options.collection],\n idKey,\n })\n\n return {\n plan,\n policy,\n async apply(): Promise<void> {\n // Routes through the txStrategy seam — throws clearly when\n // withTransactions() isn't opted in. Atomicity rolls back any\n // partial writes if a put fails mid-batch.\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:ndjson' })\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:ndjson' })\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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/as-ndjson** — newline-delimited JSON plaintext export.\n *\n * Streaming-friendly sibling to `@noy-db/as-json`. Emits one JSON\n * object per line, each carrying a `_schema` field naming the source\n * collection so a downstream reader can route records without a\n * separate header pass:\n *\n * ```\n * {\"_schema\":\"invoices\",\"id\":\"i1\",\"amount\":100,...}\n * {\"_schema\":\"invoices\",\"id\":\"i2\",\"amount\":250,...}\n * {\"_schema\":\"payments\",\"id\":\"p1\",\"invoiceId\":\"i1\",\"amount\":100,...}\n * ```\n *\n * Best for large vaults (10K+ records) where loading a single JSON\n * array into memory is clumsy. Pipes cleanly into `jq`, `fx`, log\n * stacks, or any line-oriented reducer.\n *\n * @packageDocumentation\n */\n\nimport type { Vault, CollectionDescription } from '@noy-db/hub'\nimport { applyListProjection } from '@noy-db/hub'\n\nexport interface AsNDJSONOptions {\n /** Collection allowlist. Omit for every collection the caller can read. */\n readonly collections?: readonly string[]\n /** Include envelope metadata (`_v`, `_ts`, `_by`) on each line. Default `false`. */\n readonly includeMeta?: boolean\n /** Name of the routing field. Default `'_schema'`. */\n readonly schemaField?: string\n\n /**\n * Apply the hub's `applyListProjection` read-projection before\n * serialising each record. `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.\n *\n * Rider companion fields (e.g. `pan_last4`) remain visible as their own\n * keys — they are safe write-time projections.\n */\n readonly redact?: boolean | { readonly sensitivity: 'omit' | 'mask' }\n}\n\nexport interface AsNDJSONDownloadOptions extends AsNDJSONOptions {\n /** Filename offered to the browser. Default `'vault-export.ndjson'`. */\n readonly filename?: string\n}\n\nexport interface AsNDJSONWriteOptions extends AsNDJSONOptions {\n /** Required to write plaintext NDJSON to disk — Tier 3 risk gate. */\n readonly acknowledgeRisks: true\n}\n\n/**\n * Async iterator emitting one NDJSON-formatted line per record (no\n * trailing newline — the caller decides). The authorization check\n * runs before the first record is produced.\n */\nexport async function* stream(vault: Vault, options: AsNDJSONOptions = {}): AsyncGenerator<string> {\n vault.assertCanExport('plaintext', 'ndjson')\n const allowlist = options.collections ? new Set(options.collections) : null\n const schemaField = options.schemaField ?? '_schema'\n\n // Cache collection descriptions for redaction\n const descCache = new Map<string, CollectionDescription>()\n\n for await (const chunk of vault.exportStream({ granularity: 'record' })) {\n if (allowlist && !allowlist.has(chunk.collection)) continue\n\n // Compute description once per collection if redaction is enabled\n let desc: CollectionDescription | undefined\n if (options.redact !== undefined && options.redact !== false) {\n if (!descCache.has(chunk.collection)) {\n descCache.set(chunk.collection, vault.collection(chunk.collection).describe())\n }\n desc = descCache.get(chunk.collection)\n }\n\n for (const record of chunk.records) {\n let base: Record<string, unknown>\n if (options.includeMeta) {\n base = record as Record<string, unknown>\n } else {\n base = stripMeta(record as Record<string, unknown>)\n }\n\n // Apply redaction if enabled\n if (desc !== undefined) {\n const projectionOpts = typeof options.redact === 'object'\n ? { sensitivity: options.redact.sensitivity }\n : undefined\n base = applyListProjection(desc, base, projectionOpts)\n }\n\n const out: Record<string, unknown> = { [schemaField]: chunk.collection, ...base }\n yield JSON.stringify(out)\n }\n }\n}\n\n/** Collect the full NDJSON export into a single string (in-memory). */\nexport async function toString(vault: Vault, options: AsNDJSONOptions = {}): Promise<string> {\n const lines: string[] = []\n for await (const line of stream(vault, options)) lines.push(line)\n return lines.join('\\n')\n}\n\n/** Browser download wrapping `toString()` in a Blob save-as prompt. */\nexport async function download(vault: Vault, options: AsNDJSONDownloadOptions = {}): Promise<void> {\n const ndjson = await toString(vault, options)\n const filename = options.filename ?? 'vault-export.ndjson'\n const blob = new Blob([ndjson + '\\n'], { type: 'application/x-ndjson;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 pipe: stream each line into a WritableStream-like sink (a\n * `fs.WriteStream`, `process.stdout`, or any `.write(chunk)` +\n * `.end()` duck). Memory usage is O(one record) regardless of vault\n * size.\n */\nexport async function pipe(\n vault: Vault,\n sink: { write(chunk: string): unknown; end?(): void },\n options: AsNDJSONWriteOptions,\n): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error(\n 'as-ndjson.pipe: 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 for await (const line of stream(vault, options)) {\n sink.write(line + '\\n')\n }\n sink.end?.()\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\nexport type ImportPolicy = 'merge' | 'replace' | 'insert-only'\n\nexport interface AsNDJSONImportOptions {\n /**\n * Target collection. NDJSON is one record per line — every record\n * lands in the same collection. Required.\n */\n readonly collection: 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\nexport interface AsNDJSONImportPlan {\n readonly plan: VaultDiff\n readonly policy: ImportPolicy\n apply(): Promise<void>\n}\n\n/**\n * Parse newline-delimited JSON into records and build an import plan\n * for one collection. Empty lines and lines with leading whitespace\n * are ignored. A single malformed line throws — callers that want\n * lenient parsing can pre-filter their input.\n */\nexport async function fromString(\n vault: Vault,\n ndjson: string,\n options: AsNDJSONImportOptions,\n): Promise<AsNDJSONImportPlan> {\n vault.assertCanImport('plaintext', 'ndjson')\n const policy: ImportPolicy = options.policy ?? 'merge'\n const idKey = options.idKey ?? 'id'\n\n const records: Record<string, unknown>[] = []\n let lineNo = 0\n for (const raw of ndjson.split('\\n')) {\n lineNo++\n const line = raw.trim()\n if (!line) continue\n let parsed: unknown\n try {\n parsed = JSON.parse(line)\n } catch (err) {\n throw new Error(`as-ndjson.fromString: malformed JSON on line ${lineNo}: ${(err as Error).message}`)\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(`as-ndjson.fromString: line ${lineNo} is not a JSON object`)\n }\n records.push(parsed as Record<string, unknown>)\n }\n\n const plan = await diffVault(vault, { [options.collection]: records }, {\n collections: [options.collection],\n idKey,\n })\n\n return {\n plan,\n policy,\n async apply(): Promise<void> {\n // Routes through the txStrategy seam — throws clearly when\n // withTransactions() isn't opted in. Atomicity rolls back any\n // partial writes if a put fails mid-batch.\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:ndjson' })\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:ndjson' })\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"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noy-db/as-ndjson",
3
- "version": "0.3.0-pre.1",
3
+ "version": "0.3.0-pre.3",
4
4
  "description": "Newline-delimited JSON export for noy-db — streaming plaintext export suitable for data pipelines, warehouse ingestion, and jq pipelines. Gated by RFC #249 canExportPlaintext. Part of the @noy-db/as-* portable-artefact family (plaintext tier).",
5
5
  "license": "MIT",
6
6
  "author": "vLannaAi <vicio@lanna.ai>",
@@ -32,11 +32,11 @@
32
32
  "node": ">=22.0.0"
33
33
  },
34
34
  "peerDependencies": {
35
- "@noy-db/hub": "0.3.0-pre.1"
35
+ "@noy-db/hub": "0.3.0-pre.3"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^22.0.0",
39
- "@noy-db/hub": "0.3.0-pre.1"
39
+ "@noy-db/hub": "0.3.0-pre.3"
40
40
  },
41
41
  "keywords": [
42
42
  "noy-db",