@noy-db/as-xlsx 0.2.0-pre.2 → 0.2.0-pre.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +312 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +108 -3
- package/dist/index.d.ts +108 -3
- package/dist/index.js +310 -4
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.d.ts
CHANGED
|
@@ -39,7 +39,39 @@ import { DictEntry, VaultDiff, Vault } from '@noy-db/hub';
|
|
|
39
39
|
*
|
|
40
40
|
* @module
|
|
41
41
|
*/
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* A formula cell. Emits `<f>` so the spreadsheet recomputes live; `v` is an
|
|
44
|
+
* optional cached result (what the formula currently evaluates to) so the value
|
|
45
|
+
* shows immediately on open and survives a round-trip read. Build via
|
|
46
|
+
* {@link formula}. The formula string must NOT include a leading `=`.
|
|
47
|
+
*/
|
|
48
|
+
interface XlsxFormulaCell {
|
|
49
|
+
readonly __xlsxFormula: string;
|
|
50
|
+
readonly v?: string | number | boolean;
|
|
51
|
+
}
|
|
52
|
+
/** Build a {@link XlsxFormulaCell}. `f` is the formula body without a leading `=`. */
|
|
53
|
+
declare function formula(f: string, cachedValue?: string | number | boolean): XlsxFormulaCell;
|
|
54
|
+
/**
|
|
55
|
+
* A value cell carrying a number-format code (e.g. `'#,##0.00'` for currency,
|
|
56
|
+
* `'yyyy-mm-dd'` for dates). Build via {@link styled}. The format only renders
|
|
57
|
+
* meaningfully on numeric values.
|
|
58
|
+
*/
|
|
59
|
+
interface XlsxStyledCell {
|
|
60
|
+
readonly __xlsxStyle: string;
|
|
61
|
+
readonly v: string | number | boolean | null;
|
|
62
|
+
}
|
|
63
|
+
/** Build a {@link XlsxStyledCell} — a value plus an Excel number-format code. */
|
|
64
|
+
declare function styled(value: string | number | boolean | null, numberFormat: string): XlsxStyledCell;
|
|
65
|
+
/** A data-validation rule (dropdown) over a cell range. */
|
|
66
|
+
interface XlsxValidation {
|
|
67
|
+
/** Target range in A1 notation, e.g. `'B2:B100'`. */
|
|
68
|
+
readonly sqref: string;
|
|
69
|
+
/** Inline allowed values → a `"a,b,c"` list. Mutually exclusive with `formula1`. */
|
|
70
|
+
readonly values?: readonly string[];
|
|
71
|
+
/** Explicit `formula1` (e.g. a range ref `Clients!$A$2:$A$999`). */
|
|
72
|
+
readonly formula1?: string;
|
|
73
|
+
}
|
|
74
|
+
/** One row in a sheet. A cell is a primitive value or an {@link XlsxFormulaCell}. */
|
|
43
75
|
type XlsxRow = ReadonlyArray<unknown>;
|
|
44
76
|
/** One sheet in a workbook. */
|
|
45
77
|
interface XlsxSheet {
|
|
@@ -60,12 +92,19 @@ interface XlsxSheet {
|
|
|
60
92
|
* for "auto" columns and a number for explicit widths.
|
|
61
93
|
*/
|
|
62
94
|
readonly widths?: ReadonlyArray<number | undefined>;
|
|
95
|
+
/** Data-validation dropdowns applied to ranges on this sheet. */
|
|
96
|
+
readonly validations?: readonly XlsxValidation[];
|
|
63
97
|
}
|
|
64
98
|
/**
|
|
65
99
|
* Build a complete `.xlsx` byte stream from the supplied sheet data.
|
|
66
100
|
* Pure — no I/O beyond the internal zip concatenation.
|
|
67
101
|
*/
|
|
68
|
-
declare function writeXlsx(sheets: readonly XlsxSheet[]
|
|
102
|
+
declare function writeXlsx(sheets: readonly XlsxSheet[], options?: {
|
|
103
|
+
definedNames?: ReadonlyArray<{
|
|
104
|
+
readonly name: string;
|
|
105
|
+
readonly ref: string;
|
|
106
|
+
}>;
|
|
107
|
+
}): Promise<Uint8Array>;
|
|
69
108
|
/**
|
|
70
109
|
* Convert a 1-based column index to Excel A1 letter notation.
|
|
71
110
|
* 1 → A, 26 → Z, 27 → AA, 702 → ZZ, 703 → AAA.
|
|
@@ -179,11 +218,77 @@ interface AsXlsxSheetOptions {
|
|
|
179
218
|
* mirrors `XlsxSheet.widths`, which it threads through.
|
|
180
219
|
*/
|
|
181
220
|
readonly widths?: ReadonlyArray<number | undefined>;
|
|
221
|
+
/**
|
|
222
|
+
* Smart mode only: per-field Excel number-format codes (e.g.
|
|
223
|
+
* `{ amount: '#,##0.00' }` for currency). The value is coerced to a number so
|
|
224
|
+
* the format renders. Money has no introspection signal, so currency
|
|
225
|
+
* formatting is opt-in here.
|
|
226
|
+
*/
|
|
227
|
+
readonly numberFormats?: Record<string, string>;
|
|
228
|
+
/**
|
|
229
|
+
* Smart mode only: per-field explicit dropdown value lists. Overrides the
|
|
230
|
+
* auto-detected enum/ref dropdown for that field.
|
|
231
|
+
*/
|
|
232
|
+
readonly dropdowns?: Record<string, readonly string[]>;
|
|
233
|
+
/**
|
|
234
|
+
* Smart mode only: fields whose stored value is a multi-locale i18n map
|
|
235
|
+
* (`{ en: '…', th: '…' }`). Each becomes per-locale columns plus a display
|
|
236
|
+
* column resolved **live by the global LANG cell** (change LANG → every label
|
|
237
|
+
* re-renders). Read raw, so open the export vault without an active locale.
|
|
238
|
+
*/
|
|
239
|
+
readonly i18nFields?: readonly string[];
|
|
240
|
+
/**
|
|
241
|
+
* Smart mode only: map a dict-backed (code) field to its dictionary name, e.g.
|
|
242
|
+
* `{ status: 'status' }`. Emits a hidden `_Lookups_<dict>` sheet (code +
|
|
243
|
+
* per-locale labels) and a `<field>__label` display column resolved by the
|
|
244
|
+
* global LANG cell via `VLOOKUP(code, …, MATCH(LANG, …))`.
|
|
245
|
+
*/
|
|
246
|
+
readonly dictFields?: Record<string, string>;
|
|
247
|
+
}
|
|
248
|
+
/** One aggregate column in a smart-mode summary sheet (#414 P3). */
|
|
249
|
+
interface AsXlsxSummaryAggregate {
|
|
250
|
+
/** Column header for this aggregate. */
|
|
251
|
+
readonly label: string;
|
|
252
|
+
readonly op: 'sum' | 'count' | 'avg';
|
|
253
|
+
/** Field to aggregate — required for `sum`/`avg`, ignored for `count`. */
|
|
254
|
+
readonly field?: string;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* A groupBy summary sheet (#414 P3): groups the source collection's sheet by
|
|
258
|
+
* `groupBy` and emits live SUMIFS/COUNTIFS/AVERAGEIFS columns (values cached at
|
|
259
|
+
* export). The source value columns must be numeric for the live formulas to
|
|
260
|
+
* compute (apply `numberFormats` to money fields).
|
|
261
|
+
*/
|
|
262
|
+
interface AsXlsxSummarySpec {
|
|
263
|
+
/** Summary sheet name. */
|
|
264
|
+
readonly name: string;
|
|
265
|
+
/** Source collection (must be exported as a sheet). */
|
|
266
|
+
readonly from: string;
|
|
267
|
+
/** Field to group by. */
|
|
268
|
+
readonly groupBy: string;
|
|
269
|
+
readonly aggregates: readonly AsXlsxSummaryAggregate[];
|
|
182
270
|
}
|
|
183
271
|
/** Single-collection convenience — passed where a sheet-list is accepted. */
|
|
184
272
|
interface AsXlsxOptions {
|
|
185
273
|
/** One or more sheets. At least one required. */
|
|
186
274
|
readonly sheets: readonly AsXlsxSheetOptions[];
|
|
275
|
+
/** Smart mode only: groupBy summary sheets (live SUMIFS/COUNTIFS/AVERAGEIFS). */
|
|
276
|
+
readonly summaries?: readonly AsXlsxSummarySpec[];
|
|
277
|
+
/**
|
|
278
|
+
* Smart-workbook mode (#414). Emits a relational workbook instead of a flat
|
|
279
|
+
* dump:
|
|
280
|
+
* - every sheet is **id-first** (record `id` in column A);
|
|
281
|
+
* - each foreign-key field (auto-detected via `vault.dumpSchema()`) gets a
|
|
282
|
+
* `<field>__label` column — a cross-sheet `VLOOKUP` that resolves the
|
|
283
|
+
* reference to the target's first field, carrying a **cached** label so it
|
|
284
|
+
* shows immediately and recomputes live on edit;
|
|
285
|
+
* - a `_manifest` index sheet lists every collection, its row count, and
|
|
286
|
+
* its refs.
|
|
287
|
+
*
|
|
288
|
+
* Requires unique sheet names ≤ 31 chars (so cross-sheet refs resolve) and an
|
|
289
|
+
* `id` field on records. Defaults to the existing flat export when omitted.
|
|
290
|
+
*/
|
|
291
|
+
readonly smart?: boolean;
|
|
187
292
|
}
|
|
188
293
|
/** Options for `download()` — adds optional filename. */
|
|
189
294
|
interface AsXlsxDownloadOptions extends AsXlsxOptions {
|
|
@@ -297,4 +402,4 @@ interface AsXlsxImportPlan {
|
|
|
297
402
|
*/
|
|
298
403
|
declare function fromBytes(vault: Vault, bytes: Uint8Array, options: AsXlsxImportOptions): Promise<AsXlsxImportPlan>;
|
|
299
404
|
|
|
300
|
-
export { type AsXlsxDownloadOptions, type AsXlsxImportOptions, type AsXlsxImportPlan, type AsXlsxOptions, type AsXlsxSheetOptions, type AsXlsxWriteOptions, type ImportPolicy, type ReadXlsxResult, type ReadXlsxRow, type ReadXlsxSheet, XlsxDictAmbiguityError, type XlsxRow, type XlsxSheet, colLetter, download, fromBytes, readXlsx, toBytes, toBytesFromCollection, write, writeXlsx };
|
|
405
|
+
export { type AsXlsxDownloadOptions, type AsXlsxImportOptions, type AsXlsxImportPlan, type AsXlsxOptions, type AsXlsxSheetOptions, type AsXlsxSummaryAggregate, type AsXlsxSummarySpec, type AsXlsxWriteOptions, type ImportPolicy, type ReadXlsxResult, type ReadXlsxRow, type ReadXlsxSheet, XlsxDictAmbiguityError, type XlsxFormulaCell, type XlsxRow, type XlsxSheet, type XlsxStyledCell, type XlsxValidation, colLetter, download, formula, fromBytes, readXlsx, styled, toBytes, toBytesFromCollection, write, writeXlsx };
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,19 @@
|
|
|
2
2
|
import { writeZip } from "@noy-db/as-zip";
|
|
3
3
|
var XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
4
4
|
var ENCODER = new TextEncoder();
|
|
5
|
-
|
|
5
|
+
function formula(f, cachedValue) {
|
|
6
|
+
return cachedValue === void 0 ? { __xlsxFormula: f } : { __xlsxFormula: f, v: cachedValue };
|
|
7
|
+
}
|
|
8
|
+
function isFormulaCell(v) {
|
|
9
|
+
return typeof v === "object" && v !== null && typeof v.__xlsxFormula === "string";
|
|
10
|
+
}
|
|
11
|
+
function styled(value, numberFormat) {
|
|
12
|
+
return { __xlsxStyle: numberFormat, v: value };
|
|
13
|
+
}
|
|
14
|
+
function isStyledCell(v) {
|
|
15
|
+
return typeof v === "object" && v !== null && typeof v.__xlsxStyle === "string";
|
|
16
|
+
}
|
|
17
|
+
async function writeXlsx(sheets, options = {}) {
|
|
6
18
|
if (sheets.length === 0) {
|
|
7
19
|
throw new Error("writeXlsx: at least one sheet is required");
|
|
8
20
|
}
|
|
@@ -27,6 +39,16 @@ async function writeXlsx(sheets) {
|
|
|
27
39
|
stringIndex.set(s, idx);
|
|
28
40
|
return idx;
|
|
29
41
|
};
|
|
42
|
+
const numFmtCodes = [];
|
|
43
|
+
const styleIndexByCode = /* @__PURE__ */ new Map();
|
|
44
|
+
const internStyle = (code) => {
|
|
45
|
+
const existing = styleIndexByCode.get(code);
|
|
46
|
+
if (existing !== void 0) return existing;
|
|
47
|
+
const idx = numFmtCodes.length + 1;
|
|
48
|
+
numFmtCodes.push(code);
|
|
49
|
+
styleIndexByCode.set(code, idx);
|
|
50
|
+
return idx;
|
|
51
|
+
};
|
|
30
52
|
const sheetXmls = safeSheets.map((sheet) => {
|
|
31
53
|
const lines = [
|
|
32
54
|
XML_HEADER,
|
|
@@ -56,12 +78,42 @@ async function writeXlsx(sheets) {
|
|
|
56
78
|
}
|
|
57
79
|
for (const row of sheet.rows) {
|
|
58
80
|
rowNum++;
|
|
59
|
-
const cells = row.map((value, i) => cellXml(value, i + 1, rowNum, internString)).join("");
|
|
81
|
+
const cells = row.map((value, i) => cellXml(value, i + 1, rowNum, internString, internStyle)).join("");
|
|
60
82
|
lines.push(`<row r="${rowNum}">${cells}</row>`);
|
|
61
83
|
}
|
|
62
|
-
lines.push("</sheetData>"
|
|
84
|
+
lines.push("</sheetData>");
|
|
85
|
+
if (sheet.validations && sheet.validations.length > 0) {
|
|
86
|
+
lines.push(`<dataValidations count="${sheet.validations.length}">`);
|
|
87
|
+
for (const dv of sheet.validations) {
|
|
88
|
+
const f1 = dv.formula1 ?? `"${(dv.values ?? []).join(",")}"`;
|
|
89
|
+
lines.push(
|
|
90
|
+
`<dataValidation type="list" allowBlank="1" showInputMessage="1" showErrorMessage="1" sqref="${escapeXmlAttr(dv.sqref)}"><formula1>${escapeXmlText(f1)}</formula1></dataValidation>`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
lines.push("</dataValidations>");
|
|
94
|
+
}
|
|
95
|
+
lines.push("</worksheet>");
|
|
63
96
|
return lines.join("");
|
|
64
97
|
});
|
|
98
|
+
const hasStyles = numFmtCodes.length > 0;
|
|
99
|
+
const stylesXml = hasStyles ? [
|
|
100
|
+
XML_HEADER,
|
|
101
|
+
'<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">',
|
|
102
|
+
`<numFmts count="${numFmtCodes.length}">`,
|
|
103
|
+
...numFmtCodes.map((code, i) => `<numFmt numFmtId="${164 + i}" formatCode="${escapeXmlAttr(code)}"/>`),
|
|
104
|
+
"</numFmts>",
|
|
105
|
+
'<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>',
|
|
106
|
+
'<fills count="1"><fill><patternFill patternType="none"/></fill></fills>',
|
|
107
|
+
'<borders count="1"><border/></borders>',
|
|
108
|
+
'<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>',
|
|
109
|
+
`<cellXfs count="${numFmtCodes.length + 1}">`,
|
|
110
|
+
'<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>',
|
|
111
|
+
...numFmtCodes.map(
|
|
112
|
+
(_, i) => `<xf numFmtId="${164 + i}" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>`
|
|
113
|
+
),
|
|
114
|
+
"</cellXfs>",
|
|
115
|
+
"</styleSheet>"
|
|
116
|
+
].join("") : "";
|
|
65
117
|
const sheetEntries = safeSheets.map((s, i) => ({
|
|
66
118
|
index: i + 1,
|
|
67
119
|
id: `rId${i + 1}`,
|
|
@@ -78,6 +130,7 @@ async function writeXlsx(sheets) {
|
|
|
78
130
|
(s) => `<Override PartName="/${s.path}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`
|
|
79
131
|
),
|
|
80
132
|
'<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>',
|
|
133
|
+
...hasStyles ? ['<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'] : [],
|
|
81
134
|
"</Types>"
|
|
82
135
|
].join("");
|
|
83
136
|
const rootRels = XML_HEADER + '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>';
|
|
@@ -89,6 +142,13 @@ async function writeXlsx(sheets) {
|
|
|
89
142
|
(s) => `<sheet name="${escapeXmlAttr(s.name)}" sheetId="${s.index}" r:id="${s.id}"/>`
|
|
90
143
|
),
|
|
91
144
|
"</sheets>",
|
|
145
|
+
...options.definedNames && options.definedNames.length > 0 ? [
|
|
146
|
+
"<definedNames>",
|
|
147
|
+
...options.definedNames.map(
|
|
148
|
+
(d) => `<definedName name="${escapeXmlAttr(d.name)}">${escapeXmlText(d.ref)}</definedName>`
|
|
149
|
+
),
|
|
150
|
+
"</definedNames>"
|
|
151
|
+
] : [],
|
|
92
152
|
"</workbook>"
|
|
93
153
|
].join("");
|
|
94
154
|
const workbookRels = [
|
|
@@ -98,6 +158,7 @@ async function writeXlsx(sheets) {
|
|
|
98
158
|
(s) => `<Relationship Id="${s.id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${s.index}.xml"/>`
|
|
99
159
|
),
|
|
100
160
|
`<Relationship Id="rIdSharedStrings" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>`,
|
|
161
|
+
...hasStyles ? [`<Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>`] : [],
|
|
101
162
|
"</Relationships>"
|
|
102
163
|
].join("");
|
|
103
164
|
const sharedStringsXml = [
|
|
@@ -112,12 +173,28 @@ async function writeXlsx(sheets) {
|
|
|
112
173
|
{ path: "xl/workbook.xml", bytes: ENCODER.encode(workbookXml) },
|
|
113
174
|
{ path: "xl/_rels/workbook.xml.rels", bytes: ENCODER.encode(workbookRels) },
|
|
114
175
|
{ path: "xl/sharedStrings.xml", bytes: ENCODER.encode(sharedStringsXml) },
|
|
176
|
+
...hasStyles ? [{ path: "xl/styles.xml", bytes: ENCODER.encode(stylesXml) }] : [],
|
|
115
177
|
...sheetEntries.map((s, i) => ({ path: s.path, bytes: ENCODER.encode(sheetXmls[i] ?? "") }))
|
|
116
178
|
];
|
|
117
179
|
return await writeZip(entries);
|
|
118
180
|
}
|
|
119
|
-
function cellXml(value, colIdx, rowNum, intern) {
|
|
181
|
+
function cellXml(value, colIdx, rowNum, intern, internStyle) {
|
|
120
182
|
const ref = `${colLetter(colIdx)}${rowNum}`;
|
|
183
|
+
if (isStyledCell(value)) {
|
|
184
|
+
const s2 = internStyle(value.__xlsxStyle);
|
|
185
|
+
const v = value.v;
|
|
186
|
+
if (v === null || v === void 0 || v === "") return `<c r="${ref}" s="${s2}"/>`;
|
|
187
|
+
if (typeof v === "number" && Number.isFinite(v)) return `<c r="${ref}" s="${s2}"><v>${v}</v></c>`;
|
|
188
|
+
if (typeof v === "boolean") return `<c r="${ref}" s="${s2}" t="b"><v>${v ? 1 : 0}</v></c>`;
|
|
189
|
+
return `<c r="${ref}" s="${s2}" t="s"><v>${intern(typeof v === "string" ? v : String(v))}</v></c>`;
|
|
190
|
+
}
|
|
191
|
+
if (isFormulaCell(value)) {
|
|
192
|
+
const f = escapeXmlText(value.__xlsxFormula);
|
|
193
|
+
if (value.v === void 0) return `<c r="${ref}"><f>${f}</f></c>`;
|
|
194
|
+
if (typeof value.v === "number" && Number.isFinite(value.v)) return `<c r="${ref}"><f>${f}</f><v>${value.v}</v></c>`;
|
|
195
|
+
if (typeof value.v === "boolean") return `<c r="${ref}" t="b"><f>${f}</f><v>${value.v ? 1 : 0}</v></c>`;
|
|
196
|
+
return `<c r="${ref}" t="str"><f>${f}</f><v>${escapeXmlText(String(value.v))}</v></c>`;
|
|
197
|
+
}
|
|
121
198
|
if (value === null || value === void 0 || value === "") return `<c r="${ref}"/>`;
|
|
122
199
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
123
200
|
return `<c r="${ref}"><v>${value}</v></c>`;
|
|
@@ -354,6 +431,10 @@ async function toBytes(vault, options) {
|
|
|
354
431
|
if (options.sheets.length === 0) {
|
|
355
432
|
throw new Error("as-xlsx: at least one sheet is required");
|
|
356
433
|
}
|
|
434
|
+
if (options.smart) {
|
|
435
|
+
const { sheets, definedNames } = await buildSmartSheets(vault, options);
|
|
436
|
+
return writeXlsx(sheets, { definedNames });
|
|
437
|
+
}
|
|
357
438
|
const materialisedSheets = [];
|
|
358
439
|
for (const sheetOpt of options.sheets) {
|
|
359
440
|
const collection = vault.collection(sheetOpt.collection);
|
|
@@ -397,6 +478,229 @@ async function write(vault, path, options) {
|
|
|
397
478
|
const { writeFile } = await import("fs/promises");
|
|
398
479
|
await writeFile(path, bytes);
|
|
399
480
|
}
|
|
481
|
+
function safeStringify(v) {
|
|
482
|
+
if (typeof v === "string") return v;
|
|
483
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
|
|
484
|
+
if (v == null) return "";
|
|
485
|
+
try {
|
|
486
|
+
return JSON.stringify(v) ?? "";
|
|
487
|
+
} catch {
|
|
488
|
+
return "";
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function asCached(v) {
|
|
492
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "string") return v;
|
|
493
|
+
return safeStringify(v);
|
|
494
|
+
}
|
|
495
|
+
async function buildSmartSheets(vault, options) {
|
|
496
|
+
const snapshot = await vault.dumpSchema();
|
|
497
|
+
const sheetNameByCollection = /* @__PURE__ */ new Map();
|
|
498
|
+
for (const s of options.sheets) sheetNameByCollection.set(s.collection, s.name);
|
|
499
|
+
const mats = [];
|
|
500
|
+
const localeSet = /* @__PURE__ */ new Set();
|
|
501
|
+
for (const sheetOpt of options.sheets) {
|
|
502
|
+
const collection = vault.collection(sheetOpt.collection);
|
|
503
|
+
const list = await collection.list();
|
|
504
|
+
const records = [];
|
|
505
|
+
for (const item of list) {
|
|
506
|
+
const r = item;
|
|
507
|
+
if (sheetOpt.filter && !sheetOpt.filter(r)) continue;
|
|
508
|
+
records.push(r);
|
|
509
|
+
}
|
|
510
|
+
const base = sheetOpt.columns ?? inferColumns(records);
|
|
511
|
+
const cols = ["id", ...base.filter((c) => c !== "id")];
|
|
512
|
+
const labelCol = cols.find((c) => c !== "id");
|
|
513
|
+
const labelMap = /* @__PURE__ */ new Map();
|
|
514
|
+
if (labelCol) {
|
|
515
|
+
for (const r of records) if (r.id != null) labelMap.set(safeStringify(r.id), r[labelCol]);
|
|
516
|
+
}
|
|
517
|
+
const i18nFields = (sheetOpt.i18nFields ?? []).filter((f) => cols.includes(f));
|
|
518
|
+
for (const f of i18nFields) {
|
|
519
|
+
for (const r of records) {
|
|
520
|
+
const v = r[f];
|
|
521
|
+
if (v && typeof v === "object") for (const loc of Object.keys(v)) localeSet.add(loc);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
mats.push({ opt: sheetOpt, records, cols, labelMap, i18nFields });
|
|
525
|
+
}
|
|
526
|
+
const matByCollection = new Map(mats.map((m) => [m.opt.collection, m]));
|
|
527
|
+
const dictEntries = /* @__PURE__ */ new Map();
|
|
528
|
+
for (const m of mats) {
|
|
529
|
+
for (const [field, dictName] of Object.entries(m.opt.dictFields ?? {})) {
|
|
530
|
+
if (!m.cols.includes(field) || dictEntries.has(dictName)) continue;
|
|
531
|
+
dictEntries.set(dictName, await vault.dictionary(dictName).list());
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
for (const entries of dictEntries.values()) {
|
|
535
|
+
for (const e of entries) for (const loc of Object.keys(e.labels)) localeSet.add(loc);
|
|
536
|
+
}
|
|
537
|
+
const locales = [...localeSet].sort();
|
|
538
|
+
const hasLang = locales.length > 0;
|
|
539
|
+
const defaultLocale = locales.includes("en") ? "en" : locales[0] ?? "en";
|
|
540
|
+
const ifChain = (refOf) => {
|
|
541
|
+
if (locales.length === 0) return '""';
|
|
542
|
+
let expr = refOf(locales[0]);
|
|
543
|
+
for (let k = locales.length - 1; k >= 0; k--) {
|
|
544
|
+
expr = `IF(LANG="${locales[k]}",${refOf(locales[k])},${expr})`;
|
|
545
|
+
}
|
|
546
|
+
return expr;
|
|
547
|
+
};
|
|
548
|
+
const sheetMeta = /* @__PURE__ */ new Map();
|
|
549
|
+
const dataSheets = mats.map((m) => {
|
|
550
|
+
const refs = snapshot.collections[m.opt.collection]?.refs ?? {};
|
|
551
|
+
const fields = snapshot.collections[m.opt.collection]?.fields ?? {};
|
|
552
|
+
const i18nSet = new Set(m.i18nFields);
|
|
553
|
+
const refFields = Object.keys(refs).filter((f) => m.cols.includes(f) && matByCollection.has(refs[f].target));
|
|
554
|
+
const baseCols = m.cols.filter((c) => !i18nSet.has(c));
|
|
555
|
+
const i18nFlat = m.i18nFields.flatMap((f) => [f, ...locales.map((l) => `${f}__${l}`)]);
|
|
556
|
+
const dictPairs = Object.entries(m.opt.dictFields ?? {}).filter(([f]) => m.cols.includes(f));
|
|
557
|
+
const header = [
|
|
558
|
+
...baseCols,
|
|
559
|
+
...i18nFlat,
|
|
560
|
+
...dictPairs.map(([f]) => `${f}__label`),
|
|
561
|
+
...refFields.map((f) => `${f}__label`)
|
|
562
|
+
];
|
|
563
|
+
const colIndex = /* @__PURE__ */ new Map();
|
|
564
|
+
header.forEach((h, i) => colIndex.set(h, i + 1));
|
|
565
|
+
sheetMeta.set(m.opt.collection, { name: m.opt.name, colIndex });
|
|
566
|
+
const lastRow = Math.max(m.records.length + 1, 2);
|
|
567
|
+
const validations = [];
|
|
568
|
+
for (const field of baseCols) {
|
|
569
|
+
const colL = colLetter(colIndex.get(field));
|
|
570
|
+
const sqref = `${colL}2:${colL}${lastRow}`;
|
|
571
|
+
const explicit = m.opt.dropdowns?.[field];
|
|
572
|
+
if (explicit && explicit.length > 0) {
|
|
573
|
+
validations.push({ sqref, values: explicit });
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const rf = refs[field];
|
|
577
|
+
if (rf && matByCollection.has(rf.target)) {
|
|
578
|
+
const targetSheet = sheetNameByCollection.get(rf.target);
|
|
579
|
+
const targetRows = Math.max(matByCollection.get(rf.target).records.length + 1, 2);
|
|
580
|
+
validations.push({ sqref, formula1: `'${targetSheet}'!$A$2:$A$${targetRows}` });
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const enumVals = fields[field]?.constraints?.["values"];
|
|
584
|
+
if (Array.isArray(enumVals) && enumVals.length > 0) validations.push({ sqref, values: enumVals.map((v) => safeStringify(v)) });
|
|
585
|
+
}
|
|
586
|
+
const rows = m.records.map((r, i) => {
|
|
587
|
+
const rowNum = i + 2;
|
|
588
|
+
const baseCells = baseCols.map((c) => {
|
|
589
|
+
const raw = c === "id" ? r.id ?? null : r[c] ?? null;
|
|
590
|
+
const fmt = m.opt.numberFormats?.[c];
|
|
591
|
+
if (fmt === void 0) return raw;
|
|
592
|
+
const num = typeof raw === "string" && raw.trim() !== "" && Number.isFinite(Number(raw)) ? Number(raw) : raw;
|
|
593
|
+
return styled(num, fmt);
|
|
594
|
+
});
|
|
595
|
+
const i18nCells = m.i18nFields.flatMap((f) => {
|
|
596
|
+
const rawMap = r[f] && typeof r[f] === "object" ? r[f] : {};
|
|
597
|
+
const display = formula(
|
|
598
|
+
ifChain((l) => `${colLetter(colIndex.get(`${f}__${l}`))}${rowNum}`),
|
|
599
|
+
asCached(rawMap[defaultLocale] ?? "")
|
|
600
|
+
);
|
|
601
|
+
return [display, ...locales.map((l) => rawMap[l] ?? null)];
|
|
602
|
+
});
|
|
603
|
+
const dictCells = dictPairs.map(([f, dn]) => {
|
|
604
|
+
const codeRef = `${colLetter(colIndex.get(f))}${rowNum}`;
|
|
605
|
+
const lk = `_Lookups_${dn}`;
|
|
606
|
+
const f1 = `IFERROR(VLOOKUP(${codeRef},'${lk}'!$A:$ZZ,MATCH(LANG,'${lk}'!$1:$1,0),FALSE),${codeRef})`;
|
|
607
|
+
const code = r[f];
|
|
608
|
+
const entry = code == null ? void 0 : dictEntries.get(dn)?.find((e) => e.key === safeStringify(code));
|
|
609
|
+
return formula(f1, asCached(entry?.labels[defaultLocale] ?? (code ?? "")));
|
|
610
|
+
});
|
|
611
|
+
const refCells = refFields.map((f) => {
|
|
612
|
+
const target = refs[f].target;
|
|
613
|
+
const targetSheet = sheetNameByCollection.get(target);
|
|
614
|
+
const targetMat = matByCollection.get(target);
|
|
615
|
+
const codeRef = `${colLetter(colIndex.get(f))}${rowNum}`;
|
|
616
|
+
const f1 = `IFERROR(VLOOKUP(${codeRef},'${targetSheet}'!$A:$ZZ,2,FALSE),"")`;
|
|
617
|
+
const code = r[f];
|
|
618
|
+
const cached = code == null ? "" : asCached(targetMat.labelMap.get(safeStringify(code)));
|
|
619
|
+
return formula(f1, cached);
|
|
620
|
+
});
|
|
621
|
+
return [...baseCells, ...i18nCells, ...dictCells, ...refCells];
|
|
622
|
+
});
|
|
623
|
+
return {
|
|
624
|
+
name: m.opt.name,
|
|
625
|
+
header,
|
|
626
|
+
rows,
|
|
627
|
+
...validations.length > 0 ? { validations } : {},
|
|
628
|
+
...m.opt.widths !== void 0 ? { widths: m.opt.widths } : {}
|
|
629
|
+
};
|
|
630
|
+
});
|
|
631
|
+
const manifest = {
|
|
632
|
+
name: "_manifest",
|
|
633
|
+
header: ["Collection", "Records", "Refs"],
|
|
634
|
+
rows: mats.map((m) => {
|
|
635
|
+
const refs = snapshot.collections[m.opt.collection]?.refs ?? {};
|
|
636
|
+
return [m.opt.name, m.records.length, Object.entries(refs).map(([f, r]) => `${f}\u2192${r.target}`).join(", ")];
|
|
637
|
+
})
|
|
638
|
+
};
|
|
639
|
+
const summarySheets = [];
|
|
640
|
+
for (const spec of options.summaries ?? []) {
|
|
641
|
+
const src = sheetMeta.get(spec.from);
|
|
642
|
+
const srcMat = matByCollection.get(spec.from);
|
|
643
|
+
const gCol = src?.colIndex.get(spec.groupBy);
|
|
644
|
+
if (!src || !srcMat || gCol === void 0) continue;
|
|
645
|
+
const gLetter = colLetter(gCol);
|
|
646
|
+
const seen = /* @__PURE__ */ new Set();
|
|
647
|
+
const groups = [];
|
|
648
|
+
for (const r of srcMat.records) {
|
|
649
|
+
const k = safeStringify(r[spec.groupBy]);
|
|
650
|
+
if (!seen.has(k)) {
|
|
651
|
+
seen.add(k);
|
|
652
|
+
groups.push(r[spec.groupBy]);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const header = [spec.groupBy, ...spec.aggregates.map((a) => a.label)];
|
|
656
|
+
const rows = groups.map((g, i) => {
|
|
657
|
+
const rowNum = i + 2;
|
|
658
|
+
const groupRecs = srcMat.records.filter((r) => safeStringify(r[spec.groupBy]) === safeStringify(g));
|
|
659
|
+
const cells = [g];
|
|
660
|
+
for (const a of spec.aggregates) {
|
|
661
|
+
if (a.op === "count") {
|
|
662
|
+
cells.push(formula(`COUNTIFS('${src.name}'!$${gLetter}:$${gLetter},$A${rowNum})`, groupRecs.length));
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
const vIdx = a.field ? src.colIndex.get(a.field) : void 0;
|
|
666
|
+
if (vIdx === void 0) {
|
|
667
|
+
cells.push(null);
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
const vLetter = colLetter(vIdx);
|
|
671
|
+
const nums = groupRecs.map((r) => Number(r[a.field])).filter((n) => Number.isFinite(n));
|
|
672
|
+
const sum = nums.reduce((s, n) => s + n, 0);
|
|
673
|
+
const cached = a.op === "sum" ? sum : nums.length ? sum / nums.length : 0;
|
|
674
|
+
const fn = a.op === "sum" ? "SUMIFS" : "AVERAGEIFS";
|
|
675
|
+
cells.push(
|
|
676
|
+
formula(`${fn}('${src.name}'!$${vLetter}:$${vLetter},'${src.name}'!$${gLetter}:$${gLetter},$A${rowNum})`, cached)
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
return cells;
|
|
680
|
+
});
|
|
681
|
+
summarySheets.push({ name: spec.name, header, rows });
|
|
682
|
+
}
|
|
683
|
+
const lookupSheets = [...dictEntries.entries()].map(([dn, entries]) => ({
|
|
684
|
+
name: `_Lookups_${dn}`,
|
|
685
|
+
header: ["Code", ...locales],
|
|
686
|
+
rows: entries.map((e) => [e.key, ...locales.map((l) => e.labels[l] ?? "")])
|
|
687
|
+
}));
|
|
688
|
+
const settingsSheets = [];
|
|
689
|
+
const definedNames = [];
|
|
690
|
+
if (hasLang) {
|
|
691
|
+
settingsSheets.push({
|
|
692
|
+
name: "_settings",
|
|
693
|
+
header: ["Setting", "Value"],
|
|
694
|
+
rows: [["Language", defaultLocale]],
|
|
695
|
+
validations: [{ sqref: "B2:B2", values: locales }]
|
|
696
|
+
});
|
|
697
|
+
definedNames.push({ name: "LANG", ref: `'_settings'!$B$2` });
|
|
698
|
+
}
|
|
699
|
+
return {
|
|
700
|
+
sheets: [...settingsSheets, ...lookupSheets, manifest, ...summarySheets, ...dataSheets],
|
|
701
|
+
definedNames
|
|
702
|
+
};
|
|
703
|
+
}
|
|
400
704
|
function inferColumns(records) {
|
|
401
705
|
const seen = /* @__PURE__ */ new Set();
|
|
402
706
|
const out = [];
|
|
@@ -582,8 +886,10 @@ export {
|
|
|
582
886
|
XlsxDictAmbiguityError,
|
|
583
887
|
colLetter,
|
|
584
888
|
download,
|
|
889
|
+
formula,
|
|
585
890
|
fromBytes,
|
|
586
891
|
readXlsx,
|
|
892
|
+
styled,
|
|
587
893
|
toBytes,
|
|
588
894
|
toBytesFromCollection,
|
|
589
895
|
write,
|