@noy-db/as-csv 0.6.0 → 0.7.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/dist/index.d.ts +40 -78
- package/dist/index.js +31 -69
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Vault } from '@noy-db/hub';
|
|
2
|
+
import { ImportPlan, NoydbFormat } from '@noy-db/hub/as';
|
|
3
|
+
export { ImportPolicy } from '@noy-db/hub/as';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* **@noy-db/as-csv** — CSV plaintext export for noy-db.
|
|
@@ -9,7 +11,7 @@ import { VaultDiff, Vault } from '@noy-db/hub';
|
|
|
9
11
|
* escape embedded quotes by doubling them).
|
|
10
12
|
*
|
|
11
13
|
* **Authorization.** Every call is gated by the invoking keyring's
|
|
12
|
-
* `
|
|
14
|
+
* `assertCanExport('plaintext', …)` gate — plaintext crossings of the
|
|
13
15
|
* library boundary require an explicit grant from the vault owner
|
|
14
16
|
*. The package calls `vault.assertCanExport('plaintext',
|
|
15
17
|
* 'csv')` before decrypting anything.
|
|
@@ -23,45 +25,15 @@ import { VaultDiff, Vault } from '@noy-db/hub';
|
|
|
23
25
|
* @packageDocumentation
|
|
24
26
|
*/
|
|
25
27
|
|
|
26
|
-
interface AsCSVOptions {
|
|
28
|
+
interface AsCSVOptions extends AsCSVFormatOptions {
|
|
27
29
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `exportStream` layer).
|
|
30
|
+
* Collections to export. Was a single `collection`; now plural and a READ
|
|
31
|
+
* concern, because hub owns the read — `vault.export(fmt, { collections })`.
|
|
31
32
|
*/
|
|
32
|
-
readonly
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* order. Specify explicitly for deterministic exports or when the
|
|
37
|
-
* source data has sparse fields.
|
|
38
|
-
*/
|
|
39
|
-
readonly columns?: readonly string[];
|
|
40
|
-
/**
|
|
41
|
-
* Row separator. Default `'\n'` (LF). Use `'\r\n'` for Windows-
|
|
42
|
-
* friendly output (Excel prefers CRLF but accepts LF).
|
|
43
|
-
*/
|
|
44
|
-
readonly eol?: '\n' | '\r\n';
|
|
45
|
-
/**
|
|
46
|
-
* Apply the hub's `applyListProjection` read-projection before
|
|
47
|
-
* serialising rows. `true` redacts only `classifiedFields` (mask /
|
|
48
|
-
* omit / rider, per the field's preset). The object form additionally
|
|
49
|
-
* redacts fields carrying a plain `fieldMeta` `sensitivity: 'pii' |
|
|
50
|
-
* 'secret'` tag, per `sensitivity: 'omit' | 'mask'`.
|
|
51
|
-
*
|
|
52
|
-
* Caveat: `describe()` reflects the declarations of *this session's*
|
|
53
|
-
* collection instance — redaction only takes effect when the
|
|
54
|
-
* collection was opened (this call or earlier in the session) with
|
|
55
|
-
* its `classifiedFields` / `fieldMeta` options. This is presentation-
|
|
56
|
-
* layer redaction; it never affects what's on disk. Sealed handles
|
|
57
|
-
* are unaffected either way — they always serialize as `'[sealed]'`,
|
|
58
|
-
* so ciphertext never leaks regardless of this option.
|
|
59
|
-
*
|
|
60
|
-
* Rider companion fields (e.g. `pan_last4`) remain visible as their own
|
|
61
|
-
* columns — they are safe write-time projections.
|
|
62
|
-
*/
|
|
63
|
-
readonly redact?: boolean | {
|
|
64
|
-
readonly sensitivity: 'omit' | 'mask';
|
|
33
|
+
readonly collections?: readonly string[];
|
|
34
|
+
/** Redact before encoding. Hub applies the projection; the format never sees it. */
|
|
35
|
+
readonly redact?: true | {
|
|
36
|
+
readonly sensitivity?: string;
|
|
65
37
|
};
|
|
66
38
|
}
|
|
67
39
|
interface AsCSVWriteOptions extends AsCSVOptions {
|
|
@@ -77,55 +49,45 @@ interface AsCSVDownloadOptions extends AsCSVOptions {
|
|
|
77
49
|
readonly filename?: string;
|
|
78
50
|
}
|
|
79
51
|
/**
|
|
80
|
-
*
|
|
81
|
-
* effects beyond the authorization check + audit ledger write.
|
|
82
|
-
*/
|
|
83
|
-
declare function toString(vault: Vault, options: AsCSVOptions): Promise<string>;
|
|
84
|
-
/**
|
|
85
|
-
* Browser download — wraps `toString()` in a `Blob` + triggers the
|
|
52
|
+
* Browser download — wraps `vault.export(asCsv())` in a `Blob` + triggers the
|
|
86
53
|
* browser's download prompt. Tier 2 egress per the pattern doc.
|
|
87
54
|
*
|
|
88
55
|
* Requires a browser-like environment with `URL.createObjectURL` and
|
|
89
56
|
* `document.createElement`. No-op in headless environments; use
|
|
90
|
-
* `
|
|
57
|
+
* `vault.export(asCsv())` there instead.
|
|
91
58
|
*/
|
|
92
|
-
declare function download(vault: Vault, options
|
|
59
|
+
declare function download(vault: Vault, options?: AsCSVDownloadOptions): Promise<void>;
|
|
93
60
|
/**
|
|
94
|
-
* Node file
|
|
95
|
-
*
|
|
96
|
-
*
|
|
61
|
+
* Node file write. Still here, and not in hub, for a measured reason:
|
|
62
|
+
* `check-architecture`'s `hub-portable` rule forbids Node builtins in
|
|
63
|
+
* `hub/src` because hub must run in a browser, Worker, Deno and Bun. The gate,
|
|
64
|
+
* the read and the redaction all moved; these three lines are the part that
|
|
65
|
+
* legitimately differs per runtime.
|
|
97
66
|
*/
|
|
98
67
|
declare function write(vault: Vault, path: string, options: AsCSVWriteOptions): Promise<void>;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
readonly idKey?: string;
|
|
111
|
-
/** Reconciliation policy. Default `'merge'`. */
|
|
112
|
-
readonly policy?: ImportPolicy;
|
|
113
|
-
}
|
|
114
|
-
interface AsCSVImportPlan {
|
|
115
|
-
readonly plan: VaultDiff;
|
|
116
|
-
readonly policy: ImportPolicy;
|
|
117
|
-
apply(): Promise<void>;
|
|
68
|
+
|
|
69
|
+
/** @deprecated Use `ImportPlan` from `@noy-db/hub/as` — this is now an alias. */
|
|
70
|
+
type AsCSVImportPlan = ImportPlan;
|
|
71
|
+
/** Options a CSV format instance carries. Read concerns live on `vault.export`. */
|
|
72
|
+
interface AsCSVFormatOptions {
|
|
73
|
+
/** Line ending. Default `'\n'`. */
|
|
74
|
+
readonly eol?: string;
|
|
75
|
+
/** Explicit column list. Omitted: inferred from the records. */
|
|
76
|
+
readonly columns?: readonly string[];
|
|
77
|
+
/** Per-column coercion on decode. */
|
|
78
|
+
readonly columnTypes?: Readonly<Record<string, 'string' | 'number' | 'boolean'>>;
|
|
118
79
|
}
|
|
119
80
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
81
|
+
* The CSV format — the `as-*` port instance.
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* const csv = await vault.export(asCsv(), { collections: ['invoices'] })
|
|
85
|
+
* const plan = await vault.import(asCsv(), csv, { collection: 'invoices' })
|
|
86
|
+
* await plan.apply()
|
|
87
|
+
* ```
|
|
124
88
|
*
|
|
125
|
-
*
|
|
126
|
-
* For the common case of numeric ids ("1001" → 1001), pass
|
|
127
|
-
* `columnTypes: { id: 'number' }`.
|
|
89
|
+
* Requires `formatsStrategy: withFormats()` on `createNoydb`.
|
|
128
90
|
*/
|
|
129
|
-
declare function
|
|
91
|
+
declare function asCsv(options?: AsCSVFormatOptions): NoydbFormat<string>;
|
|
130
92
|
|
|
131
|
-
export { type AsCSVDownloadOptions, type
|
|
93
|
+
export { type AsCSVDownloadOptions, type AsCSVFormatOptions, type AsCSVImportPlan, type AsCSVOptions, type AsCSVWriteOptions, asCsv, download, write };
|
package/dist/index.js
CHANGED
|
@@ -1,37 +1,20 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
|
|
3
|
-
async function toString(vault, options) {
|
|
4
|
-
vault.assertCanExport("plaintext", "csv");
|
|
2
|
+
function encodeCsv(chunks, options) {
|
|
5
3
|
const eol = options.eol ?? "\n";
|
|
6
|
-
const
|
|
7
|
-
let records = [];
|
|
8
|
-
for await (const chunk of vault.exportStream({ granularity: "collection" })) {
|
|
9
|
-
if (chunk.collection === collection) {
|
|
10
|
-
records.push(...chunk.records);
|
|
11
|
-
break;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
if (options.redact !== void 0 && options.redact !== false) {
|
|
15
|
-
const desc = vault.collection(collection).describe();
|
|
16
|
-
const projectionOpts = options.redact === true ? void 0 : { sensitivity: options.redact.sensitivity };
|
|
17
|
-
records = records.map((r) => applyListProjection(desc, r, projectionOpts));
|
|
18
|
-
}
|
|
4
|
+
const records = chunks.flatMap((c) => c.records);
|
|
19
5
|
const columns = options.columns ?? inferColumns(records);
|
|
20
|
-
if (columns.length === 0)
|
|
21
|
-
return "";
|
|
22
|
-
}
|
|
6
|
+
if (columns.length === 0) return "";
|
|
23
7
|
const lines = [columns.map(escapeField).join(",")];
|
|
24
8
|
for (const record of records) {
|
|
25
|
-
|
|
26
|
-
lines.push(row.join(","));
|
|
9
|
+
lines.push(columns.map((c) => escapeField(record[c])).join(","));
|
|
27
10
|
}
|
|
28
11
|
return lines.join(eol);
|
|
29
12
|
}
|
|
30
|
-
async function download(vault, options) {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const url = URL.createObjectURL(
|
|
13
|
+
async function download(vault, options = {}) {
|
|
14
|
+
const fmt = asCsv(options);
|
|
15
|
+
const csv = await vault.export(fmt, readOpts(options));
|
|
16
|
+
const filename = options.filename ?? `${options.collections?.[0] ?? "export"}.${fmt.extension}`;
|
|
17
|
+
const url = URL.createObjectURL(new Blob([csv], { type: fmt.mimeType }));
|
|
35
18
|
const a = document.createElement("a");
|
|
36
19
|
a.href = url;
|
|
37
20
|
a.download = filename;
|
|
@@ -41,12 +24,18 @@ async function download(vault, options) {
|
|
|
41
24
|
async function write(vault, path, options) {
|
|
42
25
|
if (options.acknowledgeRisks !== true) {
|
|
43
26
|
throw new Error(
|
|
44
|
-
|
|
27
|
+
"as-csv.write: acknowledgeRisks: true is required for on-disk plaintext output. See docs/patterns/as-exports.md - the three tiers of plaintext out."
|
|
45
28
|
);
|
|
46
29
|
}
|
|
47
|
-
const csv = await
|
|
30
|
+
const csv = await vault.export(asCsv(options), readOpts(options));
|
|
48
31
|
const { writeFile } = await import("fs/promises");
|
|
49
|
-
await writeFile(path, csv, "
|
|
32
|
+
await writeFile(path, csv, "utf8");
|
|
33
|
+
}
|
|
34
|
+
function readOpts(o) {
|
|
35
|
+
return {
|
|
36
|
+
...o.collections ? { collections: o.collections } : {},
|
|
37
|
+
...o.redact !== void 0 ? { redact: o.redact } : {}
|
|
38
|
+
};
|
|
50
39
|
}
|
|
51
40
|
function escapeField(value) {
|
|
52
41
|
if (value === null || value === void 0) return "";
|
|
@@ -73,15 +62,10 @@ function inferColumns(records) {
|
|
|
73
62
|
}
|
|
74
63
|
return columns;
|
|
75
64
|
}
|
|
76
|
-
|
|
77
|
-
vault.assertCanImport("plaintext", "csv");
|
|
78
|
-
const policy = options.policy ?? "merge";
|
|
79
|
-
const idKey = options.idKey ?? "id";
|
|
65
|
+
function decodeCsv(csv, options) {
|
|
80
66
|
const types = options.columnTypes ?? {};
|
|
81
67
|
const rows = parseCSV(csv);
|
|
82
|
-
if (rows.length === 0) {
|
|
83
|
-
return emptyPlan(vault, options.collection, policy, idKey);
|
|
84
|
-
}
|
|
68
|
+
if (rows.length === 0) return [{ collection: "", records: [] }];
|
|
85
69
|
const header = rows[0] ?? [];
|
|
86
70
|
const records = [];
|
|
87
71
|
for (let r = 1; r < rows.length; r++) {
|
|
@@ -90,43 +74,22 @@ async function fromString(vault, csv, options) {
|
|
|
90
74
|
const record = {};
|
|
91
75
|
for (let c = 0; c < header.length; c++) {
|
|
92
76
|
const col = header[c] ?? "";
|
|
93
|
-
|
|
94
|
-
record[col] = coerceCell(cell, types[col]);
|
|
77
|
+
record[col] = coerceCell(row[c] ?? "", types[col]);
|
|
95
78
|
}
|
|
96
79
|
records.push(record);
|
|
97
80
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
});
|
|
81
|
+
return [{ collection: "", records }];
|
|
82
|
+
}
|
|
83
|
+
function asCsv(options = {}) {
|
|
102
84
|
return {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
txVault.collection(entry.collection).put(entry.id, entry.record, { reason: "import:csv" });
|
|
110
|
-
}
|
|
111
|
-
if (policy !== "insert-only") {
|
|
112
|
-
for (const entry of plan.modified) {
|
|
113
|
-
txVault.collection(entry.collection).put(entry.id, entry.record, { reason: "import:csv" });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (policy === "replace") {
|
|
117
|
-
for (const entry of plan.deleted) {
|
|
118
|
-
txVault.collection(entry.collection).delete(entry.id);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
}
|
|
85
|
+
id: "csv",
|
|
86
|
+
extension: "csv",
|
|
87
|
+
mimeType: "text/csv;charset=utf-8",
|
|
88
|
+
tier: "plaintext",
|
|
89
|
+
encode: (chunks) => encodeCsv(chunks, options),
|
|
90
|
+
decode: (input) => decodeCsv(input, options)
|
|
123
91
|
};
|
|
124
92
|
}
|
|
125
|
-
async function emptyPlan(vault, collection, policy, idKey) {
|
|
126
|
-
const plan = await diffVault(vault, { [collection]: [] }, { collections: [collection], idKey });
|
|
127
|
-
return { plan, policy, async apply() {
|
|
128
|
-
} };
|
|
129
|
-
}
|
|
130
93
|
function coerceCell(cell, type) {
|
|
131
94
|
if (type === "number") {
|
|
132
95
|
if (cell === "") return void 0;
|
|
@@ -200,9 +163,8 @@ function parseCSV(input) {
|
|
|
200
163
|
return rows;
|
|
201
164
|
}
|
|
202
165
|
export {
|
|
166
|
+
asCsv,
|
|
203
167
|
download,
|
|
204
|
-
fromString,
|
|
205
|
-
toString,
|
|
206
168
|
write
|
|
207
169
|
};
|
|
208
170
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/as-csv** — CSV plaintext export for noy-db.\n *\n * Decrypts records from a single collection and formats them as\n * comma-separated values suitable for spreadsheet import. RFC 4180\n * escaping (quote fields containing commas, quotes, or newlines;\n * escape embedded quotes by doubling them).\n *\n * **Authorization.** Every call is gated by the invoking keyring's\n * `canExportPlaintext` capability — plaintext crossings of the\n * library boundary require an explicit grant from the vault owner\n *. The package calls `vault.assertCanExport('plaintext',\n * 'csv')` before decrypting anything.\n *\n * **Scope.** One collection per call. Multi-collection + attachments\n * → use `@noy-db/as-zip`. Structured JSON → `@noy-db/as-json`.\n * Excel with dictionary-label expansion → `@noy-db/as-xlsx`.\n *\n * See [`docs/patterns/as-exports.md`](https://github.com/vLannaAi/noy-db/blob/main/docs/patterns/as-exports.md).\n *\n * @packageDocumentation\n */\n\nimport { applyListProjection, diffVault, type Vault, type CollectionDescription, type VaultDiff } from '@noy-db/hub'\n\nexport interface AsCSVOptions {\n /**\n * Collection to export. Must be in the caller's read ACL; otherwise\n * the resulting CSV will be empty (ACL-scoping applies at the\n * `exportStream` layer).\n */\n readonly collection: string\n\n /**\n * Explicit column list. When omitted, columns are inferred from\n * the union of keys across all records, in first-record-wins\n * order. Specify explicitly for deterministic exports or when the\n * source data has sparse fields.\n */\n readonly columns?: readonly string[]\n\n /**\n * Row separator. Default `'\\n'` (LF). Use `'\\r\\n'` for Windows-\n * friendly output (Excel prefers CRLF but accepts LF).\n */\n readonly eol?: '\\n' | '\\r\\n'\n\n /**\n * Apply the hub's `applyListProjection` read-projection before\n * serialising rows. `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 * columns — they are safe write-time projections.\n */\n readonly redact?: boolean | { readonly sensitivity: 'omit' | 'mask' }\n}\n\nexport interface AsCSVWriteOptions extends AsCSVOptions {\n /**\n * Required for Node file-write calls — consumer acknowledgement\n * that plaintext bytes will persist on disk past the current\n * process lifetime (Tier 3 risk per `docs/patterns/as-exports.md`).\n */\n readonly acknowledgeRisks: true\n}\n\nexport interface AsCSVDownloadOptions extends AsCSVOptions {\n /** Filename offered to the browser. Default `'<collection>.csv'`. */\n readonly filename?: string\n}\n\n/**\n * Serialise a collection as a CSV string. Pure operation — no side\n * effects beyond the authorization check + audit ledger write.\n */\nexport async function toString(vault: Vault, options: AsCSVOptions): Promise<string> {\n vault.assertCanExport('plaintext', 'csv')\n\n const eol = options.eol ?? '\\n'\n const collection = options.collection\n\n // Pull the one collection via exportStream in collection granularity.\n let records: unknown[] = []\n for await (const chunk of vault.exportStream({ granularity: 'collection' })) {\n if (chunk.collection === collection) {\n records.push(...chunk.records)\n break\n }\n }\n\n if (options.redact !== undefined && options.redact !== false) {\n const desc: CollectionDescription = vault.collection(collection).describe()\n const projectionOpts = options.redact === true ? undefined\n : { sensitivity: options.redact.sensitivity }\n records = records.map((r) => applyListProjection(desc, r as Record<string, unknown>, projectionOpts))\n }\n\n // Determine columns.\n const columns = options.columns ?? inferColumns(records)\n if (columns.length === 0) {\n // Empty collection or no accessible records — emit header-only csv.\n return ''\n }\n\n // Build header + rows\n const lines: string[] = [columns.map(escapeField).join(',')]\n for (const record of records) {\n const row = columns.map(c => escapeField((record as Record<string, unknown>)[c]))\n lines.push(row.join(','))\n }\n return lines.join(eol)\n}\n\n/**\n * Browser download — wraps `toString()` in a `Blob` + triggers the\n * browser's download prompt. Tier 2 egress per the pattern doc.\n *\n * Requires a browser-like environment with `URL.createObjectURL` and\n * `document.createElement`. No-op in headless environments; use\n * `toString()` there instead.\n */\nexport async function download(vault: Vault, options: AsCSVDownloadOptions): Promise<void> {\n const csv = await toString(vault, options)\n const filename = options.filename ?? `${options.collection}.csv`\n const blob = new Blob([csv], { type: 'text/csv;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 CSV to the filesystem. Requires\n * explicit `acknowledgeRisks: true` because the plaintext file\n * outlives the current process (Tier 3 egress).\n */\nexport async function write(\n vault: Vault,\n path: string,\n options: AsCSVWriteOptions,\n): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error(\n 'as-csv.write: acknowledgeRisks: true is required for on-disk plaintext output. ' +\n 'This call creates a persistent plaintext copy of your data outside noy-db\\'s ' +\n 'encrypted storage — see docs/patterns/as-exports.md §\"The three tiers of \\\\\"plaintext out\\\\\"\"',\n )\n }\n const csv = await toString(vault, options)\n // Defer the node:fs import so this package remains browser-safe.\n const { writeFile } = await import('node:fs/promises')\n await writeFile(path, csv, 'utf-8')\n}\n\n// ── CSV formatting internals ───────────────────────────────────────────\n\n/**\n * RFC 4180 escaping: wrap a field in double quotes if it contains\n * comma, double quote, CR, or LF. Embedded double quotes become `\"\"`.\n * Other values stringify naturally.\n */\nfunction escapeField(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n if (value instanceof Date) return value.toISOString()\n const s =\n typeof value === 'string' ? value : JSON.stringify(value)\n if (/[\",\\r\\n]/.test(s)) {\n return `\"${s.replace(/\"/g, '\"\"')}\"`\n }\n return s\n}\n\n/**\n * Derive column list from the records array, preserving first-\n * encountered-wins ordering. An explicit `options.columns` bypasses\n * this.\n */\nfunction inferColumns(records: readonly unknown[]): string[] {\n const columns: string[] = []\n const seen = new Set<string>()\n for (const r of records) {\n if (r && typeof r === 'object') {\n for (const key of Object.keys(r)) {\n if (!seen.has(key)) {\n seen.add(key)\n columns.push(key)\n }\n }\n }\n }\n return columns\n}\n\n// ─── Reader ─────────────────────────────────────────────\n\nexport type ImportPolicy = 'merge' | 'replace' | 'insert-only'\n\nexport interface AsCSVImportOptions {\n /** Target collection. CSV has no native collection grouping. Required. */\n readonly collection: string\n /**\n * Optional column type hints. When omitted, every cell is parsed as\n * a string. Number / boolean cells are auto-detected when the hint\n * matches: `'1'` → `1`, `'true'` → `true`, etc.\n */\n readonly columnTypes?: Record<string, 'string' | 'number' | 'boolean'>\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 AsCSVImportPlan {\n readonly plan: VaultDiff\n readonly policy: ImportPolicy\n apply(): Promise<void>\n}\n\n/**\n * Parse RFC-4180 CSV into records and build an import plan for one\n * collection. The first row is the header; subsequent rows are\n * records. Quoted fields, embedded commas, embedded `\"\"`, and\n * CRLF line endings all round-trip with `as-csv.toString()`.\n *\n * Cells are returned as strings unless overridden via `columnTypes`.\n * For the common case of numeric ids (\"1001\" → 1001), pass\n * `columnTypes: { id: 'number' }`.\n */\nexport async function fromString(\n vault: Vault,\n csv: string,\n options: AsCSVImportOptions,\n): Promise<AsCSVImportPlan> {\n vault.assertCanImport('plaintext', 'csv')\n const policy: ImportPolicy = options.policy ?? 'merge'\n const idKey = options.idKey ?? 'id'\n const types = options.columnTypes ?? {}\n\n const rows = parseCSV(csv)\n if (rows.length === 0) {\n return emptyPlan(vault, options.collection, policy, idKey)\n }\n const header = rows[0] ?? []\n const records: Record<string, unknown>[] = []\n for (let r = 1; r < rows.length; r++) {\n const row = rows[r]!\n if (row.length === 1 && row[0] === '') continue // ignore blank lines\n const record: Record<string, unknown> = {}\n for (let c = 0; c < header.length; c++) {\n const col = header[c] ?? ''\n const cell = row[c] ?? ''\n record[col] = coerceCell(cell, types[col])\n }\n records.push(record)\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 transactionsStrategy seam — vault.noydb.transaction()\n // throws a clear error pointing at withTransactions() when the\n // strategy is not opted in. Atomicity ensures a partial failure\n // rolls back every executed put.\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:csv' })\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:csv' })\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\nasync function emptyPlan(\n vault: Vault,\n collection: string,\n policy: ImportPolicy,\n idKey: string,\n): Promise<AsCSVImportPlan> {\n const plan = await diffVault(vault, { [collection]: [] }, { collections: [collection], idKey })\n return { plan, policy, async apply() { /* nothing to do */ } }\n}\n\nfunction coerceCell(cell: string, type?: 'string' | 'number' | 'boolean'): unknown {\n if (type === 'number') {\n if (cell === '') return undefined\n const n = Number(cell)\n return Number.isFinite(n) ? n : cell\n }\n if (type === 'boolean') {\n if (cell === 'true') return true\n if (cell === 'false') return false\n return cell\n }\n return cell\n}\n\n/**\n * Minimal RFC-4180 CSV parser. Recognises:\n * - Comma-separated fields\n * - Quoted fields with embedded commas, newlines, and `\"\"` escapes\n * - Both CRLF and LF row endings\n *\n * Returns a 2D string array. The caller maps the first row to a\n * header and the rest to records.\n */\nfunction parseCSV(input: string): string[][] {\n const rows: string[][] = []\n let row: string[] = []\n let field = ''\n let inQuotes = false\n let i = 0\n\n while (i < input.length) {\n const ch = input[i]!\n if (inQuotes) {\n if (ch === '\"') {\n if (input[i + 1] === '\"') {\n field += '\"'\n i += 2\n continue\n }\n inQuotes = false\n i++\n continue\n }\n field += ch\n i++\n continue\n }\n if (ch === '\"') {\n inQuotes = true\n i++\n continue\n }\n if (ch === ',') {\n row.push(field)\n field = ''\n i++\n continue\n }\n if (ch === '\\r' && input[i + 1] === '\\n') {\n row.push(field)\n rows.push(row)\n row = []\n field = ''\n i += 2\n continue\n }\n if (ch === '\\n' || ch === '\\r') {\n row.push(field)\n rows.push(row)\n row = []\n field = ''\n i++\n continue\n }\n field += ch\n i++\n }\n\n // Final field / row.\n if (field !== '' || row.length > 0) {\n row.push(field)\n rows.push(row)\n }\n\n return rows\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/as-csv** — CSV plaintext export for noy-db.\n *\n * Decrypts records from a single collection and formats them as\n * comma-separated values suitable for spreadsheet import. RFC 4180\n * escaping (quote fields containing commas, quotes, or newlines;\n * escape embedded quotes by doubling them).\n *\n * **Authorization.** Every call is gated by the invoking keyring's\n * `assertCanExport('plaintext', …)` gate — plaintext crossings of the\n * library boundary require an explicit grant from the vault owner\n *. The package calls `vault.assertCanExport('plaintext',\n * 'csv')` before decrypting anything.\n *\n * **Scope.** One collection per call. Multi-collection + attachments\n * → use `@noy-db/as-zip`. Structured JSON → `@noy-db/as-json`.\n * Excel with dictionary-label expansion → `@noy-db/as-xlsx`.\n *\n * See [`docs/patterns/as-exports.md`](https://github.com/vLannaAi/noy-db/blob/main/docs/patterns/as-exports.md).\n *\n * @packageDocumentation\n */\n\nimport type { Vault } from '@noy-db/hub'\n\n\nexport interface AsCSVOptions extends AsCSVFormatOptions {\n /**\n * Collections to export. Was a single `collection`; now plural and a READ\n * concern, because hub owns the read — `vault.export(fmt, { collections })`.\n */\n readonly collections?: readonly string[]\n /** Redact before encoding. Hub applies the projection; the format never sees it. */\n readonly redact?: true | { readonly sensitivity?: string }\n}\n\nexport interface AsCSVWriteOptions extends AsCSVOptions {\n /**\n * Required for Node file-write calls — consumer acknowledgement\n * that plaintext bytes will persist on disk past the current\n * process lifetime (Tier 3 risk per `docs/patterns/as-exports.md`).\n */\n readonly acknowledgeRisks: true\n}\n\nexport interface AsCSVDownloadOptions extends AsCSVOptions {\n /** Filename offered to the browser. Default `'<collection>.csv'`. */\n readonly filename?: string\n}\n\n/**\n * Serialise a collection as a CSV string. Pure operation — no side\n * effects beyond the authorization check + audit ledger write.\n */\n/**\n * The pure encoder. Receives records — already gated and already redacted by\n * hub — and returns CSV. It has no vault, which is what makes the export gate\n * unskippable rather than merely checked (ADR 0004).\n */\nfunction encodeCsv(chunks: readonly ExportChunk[], options: AsCSVFormatOptions): string {\n const eol = options.eol ?? '\\n'\n const records: unknown[] = chunks.flatMap((c) => c.records)\n const columns = options.columns ?? inferColumns(records)\n if (columns.length === 0) return ''\n const lines: string[] = [columns.map(escapeField).join(',')]\n for (const record of records) {\n lines.push(columns.map((c) => escapeField((record as Record<string, unknown>)[c])).join(','))\n }\n return lines.join(eol)\n}\n\n/**\n * Browser download — wraps `vault.export(asCsv())` in a `Blob` + triggers the\n * browser's download prompt. Tier 2 egress per the pattern doc.\n *\n * Requires a browser-like environment with `URL.createObjectURL` and\n * `document.createElement`. No-op in headless environments; use\n * `vault.export(asCsv())` there instead.\n */\nexport async function download(vault: Vault, options: AsCSVDownloadOptions = {}): Promise<void> {\n const fmt = asCsv(options)\n const csv = await vault.export(fmt, readOpts(options))\n const filename = options.filename ?? `${options.collections?.[0] ?? 'export'}.${fmt.extension}`\n const url = URL.createObjectURL(new Blob([csv], { type: fmt.mimeType }))\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. Still here, and not in hub, for a measured reason:\n * `check-architecture`'s `hub-portable` rule forbids Node builtins in\n * `hub/src` because hub must run in a browser, Worker, Deno and Bun. The gate,\n * the read and the redaction all moved; these three lines are the part that\n * legitimately differs per runtime.\n */\nexport async function write(\n vault: Vault,\n path: string,\n options: AsCSVWriteOptions,\n): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error(\n 'as-csv.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 csv = await vault.export(asCsv(options), readOpts(options))\n const { writeFile } = await import('node:fs/promises')\n await writeFile(path, csv, 'utf8')\n}\n\n/** Only the keys that are actually set — `exactOptionalPropertyTypes` is on. */\nfunction readOpts(o: AsCSVOptions): FormatExportOptions {\n return {\n ...(o.collections ? { collections: o.collections } : {}),\n ...(o.redact !== undefined ? { redact: o.redact } : {}),\n }\n}\n\nfunction escapeField(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n if (value instanceof Date) return value.toISOString()\n const s =\n typeof value === 'string' ? value : JSON.stringify(value)\n if (/[\",\\r\\n]/.test(s)) {\n return `\"${s.replace(/\"/g, '\"\"')}\"`\n }\n return s\n}\n\n/**\n * Derive column list from the records array, preserving first-\n * encountered-wins ordering. An explicit `options.columns` bypasses\n * this.\n */\nfunction inferColumns(records: readonly unknown[]): string[] {\n const columns: string[] = []\n const seen = new Set<string>()\n for (const r of records) {\n if (r && typeof r === 'object') {\n for (const key of Object.keys(r)) {\n if (!seen.has(key)) {\n seen.add(key)\n columns.push(key)\n }\n }\n }\n }\n return columns\n}\n\n// ─── Reader ─────────────────────────────────────────────\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 {\n ImportPolicy,\n ImportPlan,\n NoydbFormat,\n DecodedChunk,\n ExportChunk,\n FormatExportOptions,\n} from '@noy-db/hub/as'\nexport type { ImportPolicy }\n\n/** @deprecated Use `ImportPlan` from `@noy-db/hub/as` — this is now an alias. */\nexport type AsCSVImportPlan = ImportPlan\n\n/**\n * Parse RFC-4180 CSV into records and build an import plan for one\n * collection. The first row is the header; subsequent rows are\n * records. Quoted fields, embedded commas, embedded `\"\"`, and\n * CRLF line endings all round-trip through `asCsv()`.\n *\n * Cells are returned as strings unless overridden via `columnTypes`.\n * For the common case of numeric ids (\"1001\" → 1001), pass\n * `columnTypes: { id: 'number' }`.\n */\n/**\n * The pure decoder. Bytes in, records out — no vault, no gate, no diff. Hub\n * gates, plans against the live vault, and owns `apply()`.\n */\nfunction decodeCsv(csv: string, options: AsCSVFormatOptions): readonly DecodedChunk[] {\n const types = options.columnTypes ?? {}\n const rows = parseCSV(csv)\n if (rows.length === 0) return [{ collection: '', records: [] }]\n const header = rows[0] ?? []\n const records: Record<string, unknown>[] = []\n for (let r = 1; r < rows.length; r++) {\n const row = rows[r]!\n if (row.length === 1 && row[0] === '') continue\n const record: Record<string, unknown> = {}\n for (let c = 0; c < header.length; c++) {\n const col = header[c] ?? ''\n record[col] = coerceCell(row[c] ?? '', types[col])\n }\n records.push(record)\n }\n // No collection name: CSV carries none. Hub resolves it from\n // `vault.import(fmt, csv, { collection })`.\n return [{ collection: '', records }]\n}\n\n/** Options a CSV format instance carries. Read concerns live on `vault.export`. */\nexport interface AsCSVFormatOptions {\n /** Line ending. Default `'\\n'`. */\n readonly eol?: string\n /** Explicit column list. Omitted: inferred from the records. */\n readonly columns?: readonly string[]\n /** Per-column coercion on decode. */\n readonly columnTypes?: Readonly<Record<string, 'string' | 'number' | 'boolean'>>\n}\n\n/**\n * The CSV format — the `as-*` port instance.\n *\n * ```ts\n * const csv = await vault.export(asCsv(), { collections: ['invoices'] })\n * const plan = await vault.import(asCsv(), csv, { collection: 'invoices' })\n * await plan.apply()\n * ```\n *\n * Requires `formatsStrategy: withFormats()` on `createNoydb`.\n */\nexport function asCsv(options: AsCSVFormatOptions = {}): NoydbFormat<string> {\n return {\n id: 'csv',\n extension: 'csv',\n mimeType: 'text/csv;charset=utf-8',\n tier: 'plaintext',\n encode: (chunks) => encodeCsv(chunks, options),\n decode: (input) => decodeCsv(input, options),\n }\n}\n\nfunction coerceCell(cell: string, type?: 'string' | 'number' | 'boolean'): unknown {\n if (type === 'number') {\n if (cell === '') return undefined\n const n = Number(cell)\n return Number.isFinite(n) ? n : cell\n }\n if (type === 'boolean') {\n if (cell === 'true') return true\n if (cell === 'false') return false\n return cell\n }\n return cell\n}\n\n/**\n * Minimal RFC-4180 CSV parser. Recognises:\n * - Comma-separated fields\n * - Quoted fields with embedded commas, newlines, and `\"\"` escapes\n * - Both CRLF and LF row endings\n *\n * Returns a 2D string array. The caller maps the first row to a\n * header and the rest to records.\n */\nfunction parseCSV(input: string): string[][] {\n const rows: string[][] = []\n let row: string[] = []\n let field = ''\n let inQuotes = false\n let i = 0\n\n while (i < input.length) {\n const ch = input[i]!\n if (inQuotes) {\n if (ch === '\"') {\n if (input[i + 1] === '\"') {\n field += '\"'\n i += 2\n continue\n }\n inQuotes = false\n i++\n continue\n }\n field += ch\n i++\n continue\n }\n if (ch === '\"') {\n inQuotes = true\n i++\n continue\n }\n if (ch === ',') {\n row.push(field)\n field = ''\n i++\n continue\n }\n if (ch === '\\r' && input[i + 1] === '\\n') {\n row.push(field)\n rows.push(row)\n row = []\n field = ''\n i += 2\n continue\n }\n if (ch === '\\n' || ch === '\\r') {\n row.push(field)\n rows.push(row)\n row = []\n field = ''\n i++\n continue\n }\n field += ch\n i++\n }\n\n // Final field / row.\n if (field !== '' || row.length > 0) {\n row.push(field)\n rows.push(row)\n }\n\n return rows\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noy-db/as-csv",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CSV plaintext export for noy-db — decrypts records and formats as comma-separated values. Gated by the RFC #249 `
|
|
3
|
+
"version": "0.7.0-pre.0",
|
|
4
|
+
"description": "CSV plaintext export for noy-db — decrypts records and formats as comma-separated values. Gated by the RFC #249 `assertCanExport('plaintext', …)` gate; 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>",
|
|
7
7
|
"homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/as-csv#readme",
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"node": ">=22.0.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@noy-db/hub": "0.
|
|
35
|
+
"@noy-db/hub": "0.7.0-pre.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^22.0.0",
|
|
39
|
-
"@noy-db/hub": "0.
|
|
39
|
+
"@noy-db/hub": "0.7.0-pre.0"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"noy-db",
|