@noy-db/as-csv 0.2.0-pre.8 → 0.3.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/package.json +6 -13
- package/dist/index.cjs +0 -241
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -111
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noy-db/as-csv",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-pre.1",
|
|
4
4
|
"description": "CSV plaintext export for noy-db — decrypts records and formats as comma-separated values. Gated by the RFC #249 `canExportPlaintext` capability bit; 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>",
|
|
@@ -17,17 +17,10 @@
|
|
|
17
17
|
"sideEffects": false,
|
|
18
18
|
"exports": {
|
|
19
19
|
".": {
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"default": "./dist/index.js"
|
|
23
|
-
},
|
|
24
|
-
"require": {
|
|
25
|
-
"types": "./dist/index.d.cts",
|
|
26
|
-
"default": "./dist/index.cjs"
|
|
27
|
-
}
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
28
22
|
}
|
|
29
23
|
},
|
|
30
|
-
"main": "./dist/index.cjs",
|
|
31
24
|
"module": "./dist/index.js",
|
|
32
25
|
"types": "./dist/index.d.ts",
|
|
33
26
|
"files": [
|
|
@@ -36,14 +29,14 @@
|
|
|
36
29
|
"LICENSE"
|
|
37
30
|
],
|
|
38
31
|
"engines": {
|
|
39
|
-
"node": ">=
|
|
32
|
+
"node": ">=22.0.0"
|
|
40
33
|
},
|
|
41
34
|
"peerDependencies": {
|
|
42
|
-
"@noy-db/hub": "0.
|
|
35
|
+
"@noy-db/hub": "0.3.0-pre.1"
|
|
43
36
|
},
|
|
44
37
|
"devDependencies": {
|
|
45
38
|
"@types/node": "^22.0.0",
|
|
46
|
-
"@noy-db/hub": "0.
|
|
39
|
+
"@noy-db/hub": "0.3.0-pre.1"
|
|
47
40
|
},
|
|
48
41
|
"keywords": [
|
|
49
42
|
"noy-db",
|
package/dist/index.cjs
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
|
|
30
|
-
// src/index.ts
|
|
31
|
-
var index_exports = {};
|
|
32
|
-
__export(index_exports, {
|
|
33
|
-
download: () => download,
|
|
34
|
-
fromString: () => fromString,
|
|
35
|
-
toString: () => toString,
|
|
36
|
-
write: () => write
|
|
37
|
-
});
|
|
38
|
-
module.exports = __toCommonJS(index_exports);
|
|
39
|
-
var import_hub = require("@noy-db/hub");
|
|
40
|
-
async function toString(vault, options) {
|
|
41
|
-
vault.assertCanExport("plaintext", "csv");
|
|
42
|
-
const eol = options.eol ?? "\n";
|
|
43
|
-
const collection = options.collection;
|
|
44
|
-
const records = [];
|
|
45
|
-
for await (const chunk of vault.exportStream({ granularity: "collection" })) {
|
|
46
|
-
if (chunk.collection === collection) {
|
|
47
|
-
records.push(...chunk.records);
|
|
48
|
-
break;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
const columns = options.columns ?? inferColumns(records);
|
|
52
|
-
if (columns.length === 0) {
|
|
53
|
-
return "";
|
|
54
|
-
}
|
|
55
|
-
const lines = [columns.map(escapeField).join(",")];
|
|
56
|
-
for (const record of records) {
|
|
57
|
-
const row = columns.map((c) => escapeField(record[c]));
|
|
58
|
-
lines.push(row.join(","));
|
|
59
|
-
}
|
|
60
|
-
return lines.join(eol);
|
|
61
|
-
}
|
|
62
|
-
async function download(vault, options) {
|
|
63
|
-
const csv = await toString(vault, options);
|
|
64
|
-
const filename = options.filename ?? `${options.collection}.csv`;
|
|
65
|
-
const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
|
|
66
|
-
const url = URL.createObjectURL(blob);
|
|
67
|
-
const a = document.createElement("a");
|
|
68
|
-
a.href = url;
|
|
69
|
-
a.download = filename;
|
|
70
|
-
a.click();
|
|
71
|
-
URL.revokeObjectURL(url);
|
|
72
|
-
}
|
|
73
|
-
async function write(vault, path, options) {
|
|
74
|
-
if (options.acknowledgeRisks !== true) {
|
|
75
|
-
throw new Error(
|
|
76
|
-
`as-csv.write: acknowledgeRisks: true is required for on-disk plaintext output. This call creates a persistent plaintext copy of your data outside noy-db's encrypted storage \u2014 see docs/patterns/as-exports.md \xA7"The three tiers of \\"plaintext out\\""`
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
const csv = await toString(vault, options);
|
|
80
|
-
const { writeFile } = await import("fs/promises");
|
|
81
|
-
await writeFile(path, csv, "utf-8");
|
|
82
|
-
}
|
|
83
|
-
function escapeField(value) {
|
|
84
|
-
if (value === null || value === void 0) return "";
|
|
85
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
86
|
-
if (value instanceof Date) return value.toISOString();
|
|
87
|
-
const s = typeof value === "string" ? value : JSON.stringify(value);
|
|
88
|
-
if (/[",\r\n]/.test(s)) {
|
|
89
|
-
return `"${s.replace(/"/g, '""')}"`;
|
|
90
|
-
}
|
|
91
|
-
return s;
|
|
92
|
-
}
|
|
93
|
-
function inferColumns(records) {
|
|
94
|
-
const columns = [];
|
|
95
|
-
const seen = /* @__PURE__ */ new Set();
|
|
96
|
-
for (const r of records) {
|
|
97
|
-
if (r && typeof r === "object") {
|
|
98
|
-
for (const key of Object.keys(r)) {
|
|
99
|
-
if (!seen.has(key)) {
|
|
100
|
-
seen.add(key);
|
|
101
|
-
columns.push(key);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return columns;
|
|
107
|
-
}
|
|
108
|
-
async function fromString(vault, csv, options) {
|
|
109
|
-
vault.assertCanImport("plaintext", "csv");
|
|
110
|
-
const policy = options.policy ?? "merge";
|
|
111
|
-
const idKey = options.idKey ?? "id";
|
|
112
|
-
const types = options.columnTypes ?? {};
|
|
113
|
-
const rows = parseCSV(csv);
|
|
114
|
-
if (rows.length === 0) {
|
|
115
|
-
return emptyPlan(vault, options.collection, policy, idKey);
|
|
116
|
-
}
|
|
117
|
-
const header = rows[0] ?? [];
|
|
118
|
-
const records = [];
|
|
119
|
-
for (let r = 1; r < rows.length; r++) {
|
|
120
|
-
const row = rows[r];
|
|
121
|
-
if (row.length === 1 && row[0] === "") continue;
|
|
122
|
-
const record = {};
|
|
123
|
-
for (let c = 0; c < header.length; c++) {
|
|
124
|
-
const col = header[c] ?? "";
|
|
125
|
-
const cell = row[c] ?? "";
|
|
126
|
-
record[col] = coerceCell(cell, types[col]);
|
|
127
|
-
}
|
|
128
|
-
records.push(record);
|
|
129
|
-
}
|
|
130
|
-
const plan = await (0, import_hub.diffVault)(vault, { [options.collection]: records }, {
|
|
131
|
-
collections: [options.collection],
|
|
132
|
-
idKey
|
|
133
|
-
});
|
|
134
|
-
return {
|
|
135
|
-
plan,
|
|
136
|
-
policy,
|
|
137
|
-
async apply() {
|
|
138
|
-
await vault.noydb.transaction((tx) => {
|
|
139
|
-
const txVault = tx.vault(vault.name);
|
|
140
|
-
for (const entry of plan.added) {
|
|
141
|
-
txVault.collection(entry.collection).put(entry.id, entry.record, { reason: "import:csv" });
|
|
142
|
-
}
|
|
143
|
-
if (policy !== "insert-only") {
|
|
144
|
-
for (const entry of plan.modified) {
|
|
145
|
-
txVault.collection(entry.collection).put(entry.id, entry.record, { reason: "import:csv" });
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (policy === "replace") {
|
|
149
|
-
for (const entry of plan.deleted) {
|
|
150
|
-
txVault.collection(entry.collection).delete(entry.id);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
async function emptyPlan(vault, collection, policy, idKey) {
|
|
158
|
-
const plan = await (0, import_hub.diffVault)(vault, { [collection]: [] }, { collections: [collection], idKey });
|
|
159
|
-
return { plan, policy, async apply() {
|
|
160
|
-
} };
|
|
161
|
-
}
|
|
162
|
-
function coerceCell(cell, type) {
|
|
163
|
-
if (type === "number") {
|
|
164
|
-
if (cell === "") return void 0;
|
|
165
|
-
const n = Number(cell);
|
|
166
|
-
return Number.isFinite(n) ? n : cell;
|
|
167
|
-
}
|
|
168
|
-
if (type === "boolean") {
|
|
169
|
-
if (cell === "true") return true;
|
|
170
|
-
if (cell === "false") return false;
|
|
171
|
-
return cell;
|
|
172
|
-
}
|
|
173
|
-
return cell;
|
|
174
|
-
}
|
|
175
|
-
function parseCSV(input) {
|
|
176
|
-
const rows = [];
|
|
177
|
-
let row = [];
|
|
178
|
-
let field = "";
|
|
179
|
-
let inQuotes = false;
|
|
180
|
-
let i = 0;
|
|
181
|
-
while (i < input.length) {
|
|
182
|
-
const ch = input[i];
|
|
183
|
-
if (inQuotes) {
|
|
184
|
-
if (ch === '"') {
|
|
185
|
-
if (input[i + 1] === '"') {
|
|
186
|
-
field += '"';
|
|
187
|
-
i += 2;
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
inQuotes = false;
|
|
191
|
-
i++;
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
field += ch;
|
|
195
|
-
i++;
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
if (ch === '"') {
|
|
199
|
-
inQuotes = true;
|
|
200
|
-
i++;
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
if (ch === ",") {
|
|
204
|
-
row.push(field);
|
|
205
|
-
field = "";
|
|
206
|
-
i++;
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
if (ch === "\r" && input[i + 1] === "\n") {
|
|
210
|
-
row.push(field);
|
|
211
|
-
rows.push(row);
|
|
212
|
-
row = [];
|
|
213
|
-
field = "";
|
|
214
|
-
i += 2;
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
if (ch === "\n" || ch === "\r") {
|
|
218
|
-
row.push(field);
|
|
219
|
-
rows.push(row);
|
|
220
|
-
row = [];
|
|
221
|
-
field = "";
|
|
222
|
-
i++;
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
field += ch;
|
|
226
|
-
i++;
|
|
227
|
-
}
|
|
228
|
-
if (field !== "" || row.length > 0) {
|
|
229
|
-
row.push(field);
|
|
230
|
-
rows.push(row);
|
|
231
|
-
}
|
|
232
|
-
return rows;
|
|
233
|
-
}
|
|
234
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
235
|
-
0 && (module.exports = {
|
|
236
|
-
download,
|
|
237
|
-
fromString,
|
|
238
|
-
toString,
|
|
239
|
-
write
|
|
240
|
-
});
|
|
241
|
-
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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 type { Vault } 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\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 const 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 // 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\nimport { diffVault, type VaultDiff } from '@noy-db/hub'\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 txStrategy 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":[]}
|
package/dist/index.d.cts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { VaultDiff, Vault } from '@noy-db/hub';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* **@noy-db/as-csv** — CSV plaintext export for noy-db.
|
|
5
|
-
*
|
|
6
|
-
* Decrypts records from a single collection and formats them as
|
|
7
|
-
* comma-separated values suitable for spreadsheet import. RFC 4180
|
|
8
|
-
* escaping (quote fields containing commas, quotes, or newlines;
|
|
9
|
-
* escape embedded quotes by doubling them).
|
|
10
|
-
*
|
|
11
|
-
* **Authorization.** Every call is gated by the invoking keyring's
|
|
12
|
-
* `canExportPlaintext` capability — plaintext crossings of the
|
|
13
|
-
* library boundary require an explicit grant from the vault owner
|
|
14
|
-
*. The package calls `vault.assertCanExport('plaintext',
|
|
15
|
-
* 'csv')` before decrypting anything.
|
|
16
|
-
*
|
|
17
|
-
* **Scope.** One collection per call. Multi-collection + attachments
|
|
18
|
-
* → use `@noy-db/as-zip`. Structured JSON → `@noy-db/as-json`.
|
|
19
|
-
* Excel with dictionary-label expansion → `@noy-db/as-xlsx`.
|
|
20
|
-
*
|
|
21
|
-
* See [`docs/patterns/as-exports.md`](https://github.com/vLannaAi/noy-db/blob/main/docs/patterns/as-exports.md).
|
|
22
|
-
*
|
|
23
|
-
* @packageDocumentation
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
interface AsCSVOptions {
|
|
27
|
-
/**
|
|
28
|
-
* Collection to export. Must be in the caller's read ACL; otherwise
|
|
29
|
-
* the resulting CSV will be empty (ACL-scoping applies at the
|
|
30
|
-
* `exportStream` layer).
|
|
31
|
-
*/
|
|
32
|
-
readonly collection: string;
|
|
33
|
-
/**
|
|
34
|
-
* Explicit column list. When omitted, columns are inferred from
|
|
35
|
-
* the union of keys across all records, in first-record-wins
|
|
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
|
-
interface AsCSVWriteOptions extends AsCSVOptions {
|
|
47
|
-
/**
|
|
48
|
-
* Required for Node file-write calls — consumer acknowledgement
|
|
49
|
-
* that plaintext bytes will persist on disk past the current
|
|
50
|
-
* process lifetime (Tier 3 risk per `docs/patterns/as-exports.md`).
|
|
51
|
-
*/
|
|
52
|
-
readonly acknowledgeRisks: true;
|
|
53
|
-
}
|
|
54
|
-
interface AsCSVDownloadOptions extends AsCSVOptions {
|
|
55
|
-
/** Filename offered to the browser. Default `'<collection>.csv'`. */
|
|
56
|
-
readonly filename?: string;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Serialise a collection as a CSV string. Pure operation — no side
|
|
60
|
-
* effects beyond the authorization check + audit ledger write.
|
|
61
|
-
*/
|
|
62
|
-
declare function toString(vault: Vault, options: AsCSVOptions): Promise<string>;
|
|
63
|
-
/**
|
|
64
|
-
* Browser download — wraps `toString()` in a `Blob` + triggers the
|
|
65
|
-
* browser's download prompt. Tier 2 egress per the pattern doc.
|
|
66
|
-
*
|
|
67
|
-
* Requires a browser-like environment with `URL.createObjectURL` and
|
|
68
|
-
* `document.createElement`. No-op in headless environments; use
|
|
69
|
-
* `toString()` there instead.
|
|
70
|
-
*/
|
|
71
|
-
declare function download(vault: Vault, options: AsCSVDownloadOptions): Promise<void>;
|
|
72
|
-
/**
|
|
73
|
-
* Node file-write — persists the CSV to the filesystem. Requires
|
|
74
|
-
* explicit `acknowledgeRisks: true` because the plaintext file
|
|
75
|
-
* outlives the current process (Tier 3 egress).
|
|
76
|
-
*/
|
|
77
|
-
declare function write(vault: Vault, path: string, options: AsCSVWriteOptions): Promise<void>;
|
|
78
|
-
|
|
79
|
-
type ImportPolicy = 'merge' | 'replace' | 'insert-only';
|
|
80
|
-
interface AsCSVImportOptions {
|
|
81
|
-
/** Target collection. CSV has no native collection grouping. Required. */
|
|
82
|
-
readonly collection: string;
|
|
83
|
-
/**
|
|
84
|
-
* Optional column type hints. When omitted, every cell is parsed as
|
|
85
|
-
* a string. Number / boolean cells are auto-detected when the hint
|
|
86
|
-
* matches: `'1'` → `1`, `'true'` → `true`, etc.
|
|
87
|
-
*/
|
|
88
|
-
readonly columnTypes?: Record<string, 'string' | 'number' | 'boolean'>;
|
|
89
|
-
/** Field on each record that carries its id. Default `'id'`. */
|
|
90
|
-
readonly idKey?: string;
|
|
91
|
-
/** Reconciliation policy. Default `'merge'`. */
|
|
92
|
-
readonly policy?: ImportPolicy;
|
|
93
|
-
}
|
|
94
|
-
interface AsCSVImportPlan {
|
|
95
|
-
readonly plan: VaultDiff;
|
|
96
|
-
readonly policy: ImportPolicy;
|
|
97
|
-
apply(): Promise<void>;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Parse RFC-4180 CSV into records and build an import plan for one
|
|
101
|
-
* collection. The first row is the header; subsequent rows are
|
|
102
|
-
* records. Quoted fields, embedded commas, embedded `""`, and
|
|
103
|
-
* CRLF line endings all round-trip with `as-csv.toString()`.
|
|
104
|
-
*
|
|
105
|
-
* Cells are returned as strings unless overridden via `columnTypes`.
|
|
106
|
-
* For the common case of numeric ids ("1001" → 1001), pass
|
|
107
|
-
* `columnTypes: { id: 'number' }`.
|
|
108
|
-
*/
|
|
109
|
-
declare function fromString(vault: Vault, csv: string, options: AsCSVImportOptions): Promise<AsCSVImportPlan>;
|
|
110
|
-
|
|
111
|
-
export { type AsCSVDownloadOptions, type AsCSVImportOptions, type AsCSVImportPlan, type AsCSVOptions, type AsCSVWriteOptions, type ImportPolicy, download, fromString, toString, write };
|