@xeplr/actions 1.1.18 → 1.1.20

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.
@@ -0,0 +1,284 @@
1
+ // .xlsx writer — our own, on fflate's streaming zip.
2
+ //
3
+ // Why not exceljs: it pulls a uuid with a published advisory, has not had a
4
+ // release since December 2024, and the override that silences it has to be
5
+ // repeated in every app that installs this package. Writing SpreadsheetML is
6
+ // the other half of what lib/formats/_xlsx-stream.js already reads, and a
7
+ // workbook of one styled sheet is a small, well-specified thing.
8
+ //
9
+ // WHAT IT WRITES: one sheet; an optional title and subtitle above the header;
10
+ // the header bold on a tint, frozen, with an autofilter; numbers and dates as
11
+ // REAL cells carrying their number format, so the reader can still sum and
12
+ // filter; subtotal rows bold, total rows bold on a tint; column widths from
13
+ // the header and the first rows.
14
+ //
15
+ // Streaming: rows go straight into the zip, which goes straight to disk,
16
+ // waiting on the file stream when it is full — a million rows costs the
17
+ // same memory as a hundred. Only the first SAMPLE_ROWS are held, to size the
18
+ // columns, because <cols> must come before the rows in the sheet.
19
+
20
+ var dateValues = require('../../drivers/db/dateValues');
21
+ var fs = require('fs');
22
+
23
+ var MAX_ROWS = 1048576; // Excel's own limit, header and title rows included
24
+ var SAMPLE_ROWS = 200;
25
+ var CHUNK = 1 << 20; // push to the zip in ~1 MB pieces
26
+
27
+ // XML 1.0 cannot carry most control characters even escaped; Excel refuses
28
+ // the file ("We found a problem with some content"). Dropped.
29
+ var INVALID_XML = /[\u0000-\u0008\u000B\u000C\u000E-\u001F￾￿]/g;
30
+ function esc(value) {
31
+ return String(value).replace(INVALID_XML, '')
32
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
33
+ }
34
+
35
+ /** 0 → A, 25 → Z, 26 → AA. */
36
+ function colName(i) {
37
+ var s = '';
38
+ for (var n = i + 1; n > 0; n = Math.floor((n - 1) / 26)) s = String.fromCharCode(65 + ((n - 1) % 26)) + s;
39
+ return s;
40
+ }
41
+
42
+ /**
43
+ * A Date, or an ISO date string, as an Excel serial — or null. A string with
44
+ * no zone is read as UTC (dateValues.toInstant), never in this machine's zone:
45
+ * `new Date('2025-08-01 10:00')` would put a different time in the cell on a
46
+ * server in India than on one in New York.
47
+ */
48
+ function dateSerial(value) {
49
+ var d = value instanceof Date ? value : (typeof value === 'string' ? dateValues.toInstant(value) : null);
50
+ if (!d || isNaN(d.getTime())) return null;
51
+ return d.getTime() / 86400000 + 25569;
52
+ }
53
+
54
+ // Fonts: 0 normal, 1 bold, 2 title. Fills: 0 none, 1 gray125 (both required
55
+ // by Excel at those positions), 2 header tint, 3 total tint.
56
+ var FONT = { normal: 0, bold: 1, title: 2 };
57
+ var FILL = { none: 0, header: 2, total: 3 };
58
+
59
+ function stylesXml(numFmts, xfs) {
60
+ var fmts = numFmts.map(function(code, i) { return '<numFmt numFmtId="' + (164 + i) + '" formatCode="' + esc(code) + '"/>'; });
61
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
62
+ '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">' +
63
+ (fmts.length ? '<numFmts count="' + fmts.length + '">' + fmts.join('') + '</numFmts>' : '') +
64
+ '<fonts count="3"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font>' +
65
+ '<font><b/><sz val="14"/><name val="Calibri"/></font></fonts>' +
66
+ '<fills count="4"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill>' +
67
+ '<fill><patternFill patternType="solid"><fgColor rgb="FFF1F3F7"/><bgColor indexed="64"/></patternFill></fill>' +
68
+ '<fill><patternFill patternType="solid"><fgColor rgb="FFE8EBF2"/><bgColor indexed="64"/></patternFill></fill></fills>' +
69
+ '<borders count="2"><border><left/><right/><top/><bottom/><diagonal/></border>' +
70
+ '<border><left/><right/><top style="thin"><color rgb="FF9AA1AE"/></top><bottom/><diagonal/></border></borders>' +
71
+ '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>' +
72
+ '<cellXfs count="' + xfs.length + '">' + xfs.map(function(x) {
73
+ return '<xf numFmtId="' + x.numFmtId + '" fontId="' + x.fontId + '" fillId="' + x.fillId + '" borderId="' + x.borderId + '" xfId="0"' +
74
+ (x.numFmtId ? ' applyNumberFormat="1"' : '') + (x.fontId ? ' applyFont="1"' : '') + (x.fillId ? ' applyFill="1"' : '') +
75
+ (x.borderId ? ' applyBorder="1"' : '') + (x.align ? ' applyAlignment="1"><alignment horizontal="' + x.align + '"/></xf>' : '/>');
76
+ }).join('') + '</cellXfs>' +
77
+ '<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>';
78
+ }
79
+
80
+ var STATIC = {
81
+ '[Content_Types].xml': '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
82
+ '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
83
+ '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
84
+ '<Default Extension="xml" ContentType="application/xml"/>' +
85
+ '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>' +
86
+ '<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>' +
87
+ '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>' +
88
+ '</Types>',
89
+ '_rels/.rels': '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
90
+ '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
91
+ '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>' +
92
+ '</Relationships>',
93
+ 'xl/_rels/workbook.xml.rels': '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
94
+ '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
95
+ '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>' +
96
+ '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>' +
97
+ '</Relationships>'
98
+ };
99
+
100
+ function workbookXml(sheetName, filterRef) {
101
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
102
+ '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">' +
103
+ '<sheets><sheet name="' + esc(sheetName) + '" sheetId="1" r:id="rId1"/></sheets>' +
104
+ (filterRef ? '<definedNames><definedName name="_xlnm._FilterDatabase" localSheetId="0" hidden="1">' + esc("'" + sheetName.replace(/'/g, "''") + "'!" + filterRef) + '</definedName></definedNames>' : '') +
105
+ '</workbook>';
106
+ }
107
+
108
+ /** Excel refuses a sheet name over 31 characters or with any of : \ / ? * [ ] */
109
+ function sheetNameOf(title) {
110
+ var name = String(title || 'Sheet1').replace(/[:\\/?*[\]]/g, ' ').trim().slice(0, 31);
111
+ return name || 'Sheet1';
112
+ }
113
+
114
+ module.exports = {
115
+ requires: ['fflate'],
116
+ MAX_ROWS: MAX_ROWS,
117
+
118
+ /**
119
+ * @param {string} filePath
120
+ * @param {object} doc - { title?, subtitle?, columns: [{ header, kind?: 'number'|'date'|'text'|'raw', numFmt?, width?, align? }] }
121
+ * kind 'number': the value is written as a number (numFmt applies);
122
+ * kind 'date': a Date or ISO string, written as a date serial (numFmt applies);
123
+ * anything else: a number stays a number, everything else is text.
124
+ * @returns {{ writeRow(row): Promise<boolean>, close(): Promise<{ rows, truncated }> }}
125
+ * row: { values: [], kind?: 'detail'|'subtotal'|'total' }. writeRow answers
126
+ * false once Excel's row limit is reached (the rest are counted, not written).
127
+ */
128
+ open: function(filePath, doc) {
129
+ var fflate = require('fflate');
130
+ var columns = doc.columns || [];
131
+ var out = fs.createWriteStream(filePath);
132
+ var failed = null;
133
+ out.on('error', function(err) { failed = err; });
134
+ var zipDone;
135
+ var zipFinished = new Promise(function(resolve) { zipDone = resolve; });
136
+ var zip = new fflate.Zip(function(err, chunk, final) {
137
+ if (err) { failed = err; return; }
138
+ out.write(Buffer.from(chunk));
139
+ if (final) zipDone();
140
+ });
141
+ function addFile(name, text) {
142
+ var f = new fflate.ZipDeflate(name, { level: 6 });
143
+ zip.add(f);
144
+ f.push(fflate.strToU8(text), true);
145
+ }
146
+
147
+ // Styles are collected as the rows use them and written at the end.
148
+ var numFmts = [];
149
+ var xfs = [{ numFmtId: 0, fontId: 0, fillId: 0, borderId: 0 }];
150
+ var xfIndex = { '0|0|0|0|': 0 };
151
+ function styleId(numFmt, fontId, fillId, borderId, align) {
152
+ var numFmtId = 0;
153
+ if (numFmt) {
154
+ var at = numFmts.indexOf(numFmt);
155
+ if (at < 0) { numFmts.push(numFmt); at = numFmts.length - 1; }
156
+ numFmtId = 164 + at;
157
+ }
158
+ var key = numFmtId + '|' + fontId + '|' + fillId + '|' + borderId + '|' + (align || '');
159
+ if (xfIndex[key] === undefined) {
160
+ xfs.push({ numFmtId: numFmtId, fontId: fontId, fillId: fillId, borderId: borderId, align: align || null });
161
+ xfIndex[key] = xfs.length - 1;
162
+ }
163
+ return xfIndex[key];
164
+ }
165
+ var LOOK = {
166
+ detail: { font: FONT.normal, fill: FILL.none, border: 0 },
167
+ subtotal: { font: FONT.bold, fill: FILL.none, border: 0 },
168
+ total: { font: FONT.bold, fill: FILL.total, border: 1 }
169
+ };
170
+
171
+ var sheet = new fflate.ZipDeflate('xl/worksheets/sheet1.xml', { level: 6 });
172
+ var buffered = '';
173
+ var rowNo = 0;
174
+ var written = 0; // data rows written
175
+ var offered = 0; // data rows asked for
176
+ var headerRow = 0;
177
+ var sample = [];
178
+ var started = false;
179
+
180
+ function cellXml(ref, value, column, look) {
181
+ if (value === null || value === undefined || value === '') return '';
182
+ var kind = column.kind;
183
+ var align = column.align === 'right' || column.align === 'center' ? column.align : null;
184
+ if (kind === 'date') {
185
+ var serial = dateSerial(value);
186
+ if (serial !== null) return '<c r="' + ref + '" s="' + styleId(column.numFmt || 'yyyy-mm-dd', look.font, look.fill, look.border, align) + '"><v>' + serial + '</v></c>';
187
+ }
188
+ var num = typeof value === 'number' ? value : (kind === 'number' && value !== '' && !isNaN(Number(value)) ? Number(value) : null);
189
+ if (num !== null && isFinite(num)) {
190
+ return '<c r="' + ref + '" s="' + styleId(kind === 'number' ? column.numFmt : null, look.font, look.fill, look.border, align) + '"><v>' + num + '</v></c>';
191
+ }
192
+ if (typeof value === 'boolean') return '<c r="' + ref + '" t="b" s="' + styleId(null, look.font, look.fill, look.border, align) + '"><v>' + (value ? 1 : 0) + '</v></c>';
193
+ return '<c r="' + ref + '" t="inlineStr" s="' + styleId(null, look.font, look.fill, look.border, align) + '"><is><t xml:space="preserve">' + esc(value) + '</t></is></c>';
194
+ }
195
+
196
+ function textRow(cells) { rowNo += 1; return '<row r="' + rowNo + '">' + cells + '</row>'; }
197
+
198
+ async function push(text, final) {
199
+ buffered += text;
200
+ if (buffered.length < CHUNK && !final) return;
201
+ sheet.push(fflate.strToU8(buffered), Boolean(final));
202
+ buffered = '';
203
+ if (failed) throw failed;
204
+ if (out.writableNeedDrain) await new Promise(function(r) { out.once('drain', r); });
205
+ }
206
+
207
+ // <cols> needs the widths, which need a look at the rows: written once
208
+ // the sample is full, or at close for a short table.
209
+ async function start() {
210
+ started = true;
211
+ var widths = columns.map(function(c, i) {
212
+ if (c.width) return c.width;
213
+ var longest = String(c.header || '').length;
214
+ // A date is as wide as its pattern, not as its JS string form.
215
+ if (c.kind === 'date') return Math.min(60, Math.max(10, longest, String(c.numFmt || 'yyyy-mm-dd').length) + 2);
216
+ sample.forEach(function(r) {
217
+ var shown = r.display ? r.display[i] : r.values[i];
218
+ if (shown !== null && shown !== undefined) longest = Math.max(longest, String(shown).length);
219
+ });
220
+ return Math.min(60, Math.max(8, longest + 2));
221
+ });
222
+ zip.add(sheet);
223
+ var head = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
224
+ '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">';
225
+ var titleRows = (doc.title ? 1 : 0) + (doc.subtitle ? 1 : 0);
226
+ headerRow = titleRows + (titleRows ? 2 : 1);
227
+ head += '<sheetViews><sheetView workbookViewId="0"><pane ySplit="' + headerRow + '" topLeftCell="A' + (headerRow + 1) +
228
+ '" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews>';
229
+ head += '<cols>' + widths.map(function(w, i) { return '<col min="' + (i + 1) + '" max="' + (i + 1) + '" width="' + w + '" customWidth="1"/>'; }).join('') + '</cols>';
230
+ head += '<sheetData>';
231
+ if (doc.title) head += textRow('<c r="A' + (rowNo + 1) + '" t="inlineStr" s="' + styleId(null, FONT.title, 0, 0) + '"><is><t xml:space="preserve">' + esc(doc.title) + '</t></is></c>');
232
+ if (doc.subtitle) head += textRow('<c r="A' + (rowNo + 1) + '" t="inlineStr"><is><t xml:space="preserve">' + esc(doc.subtitle) + '</t></is></c>');
233
+ if (titleRows) rowNo += 1; // a blank row between the title block and the table
234
+ head += textRow(columns.map(function(c, i) {
235
+ var align = c.align === 'right' || c.align === 'center' ? c.align : null;
236
+ return '<c r="' + colName(i) + (rowNo + 1) + '" t="inlineStr" s="' + styleId(null, FONT.bold, FILL.header, 0, align) + '"><is><t xml:space="preserve">' + esc(c.header == null ? '' : c.header) + '</t></is></c>';
237
+ }).join(''));
238
+ await push(head);
239
+ var held = sample;
240
+ sample = null;
241
+ for (var k = 0; k < held.length; k++) await emit(held[k]);
242
+ }
243
+
244
+ async function emit(row) {
245
+ if (rowNo >= MAX_ROWS) return false;
246
+ var look = LOOK[row.kind] || LOOK.detail;
247
+ var r = rowNo + 1;
248
+ var cells = '';
249
+ var values = row.values || [];
250
+ for (var i = 0; i < columns.length; i++) cells += cellXml(colName(i) + r, values[i], columns[i], look);
251
+ written += 1;
252
+ await push(textRow(cells));
253
+ return true;
254
+ }
255
+
256
+ return {
257
+ writeRow: async function(row) {
258
+ offered += 1;
259
+ if (!started) {
260
+ sample.push(row);
261
+ if (sample.length >= SAMPLE_ROWS) await start();
262
+ return true;
263
+ }
264
+ return emit(row);
265
+ },
266
+ close: async function() {
267
+ if (!started) await start();
268
+ var lastRow = Math.max(rowNo, headerRow);
269
+ var filterRef = columns.length ? 'A' + headerRow + ':' + colName(columns.length - 1) + lastRow : null;
270
+ await push('</sheetData>' + (filterRef ? '<autoFilter ref="' + filterRef + '"/>' : '') +
271
+ '<pageSetup orientation="' + (columns.length > 6 ? 'landscape' : 'portrait') + '"/></worksheet>', true);
272
+ var sheetName = sheetNameOf(doc.title);
273
+ Object.keys(STATIC).forEach(function(name) { addFile(name, STATIC[name]); });
274
+ addFile('xl/workbook.xml', workbookXml(sheetName, filterRef ? '$A$' + headerRow + ':$' + colName(columns.length - 1) + '$' + lastRow : null));
275
+ addFile('xl/styles.xml', stylesXml(numFmts, xfs));
276
+ zip.end();
277
+ await zipFinished;
278
+ await new Promise(function(resolve, reject) { out.end(function(err) { return err ? reject(err) : resolve(); }); });
279
+ if (failed) throw failed;
280
+ return { rows: written, truncated: offered > written };
281
+ }
282
+ };
283
+ }
284
+ };
@@ -410,6 +410,20 @@ async function runBootstrap(driver, pool, targetTable, columns, primaryKeys, sta
410
410
  var { reconcileColumns } = require('./reconcile');
411
411
  var reconciled = reconcileColumns(columns, targetSchema, driver.dataTypeToLogical);
412
412
 
413
+ // A ZONELESS SOURCE COLUMN IN A ZONED TARGET COLUMN. Tables created before
414
+ // zoneless date-times had their own type hold them as instants (the digits
415
+ // read as UTC). The target's type would win and keep it so forever — so it
416
+ // is retyped to zoneless, once, keeping those same digits. Only where the
417
+ // dialect has the two kinds (Postgres, DuckDB); MySQL DATETIME and SQL
418
+ // Server DATETIME2 are zoneless already.
419
+ var retype = reconciled.filter(function(c) { return c.sourceType === 'localdatetime' && c.targetType === 'datetime'; });
420
+ if (retype.length && typeof driver.buildRetypeToLocalDateTimeSql === 'function') {
421
+ for (var r = 0; r < retype.length; r++) {
422
+ await driver.query(pool, driver.buildRetypeToLocalDateTimeSql(targetTable, retype[r].name));
423
+ }
424
+ reconciled = reconcileColumns(columns, await driver.getTableSchema(pool, targetTable), driver.dataTypeToLogical);
425
+ }
426
+
413
427
  var missing = reconciled.filter(function(c) { return c.missing; });
414
428
  if (missing.length > 0) {
415
429
  throw new Error(
@@ -29,7 +29,8 @@ var PG_TO_LOGICAL = {
29
29
  'double precision': 'number',
30
30
  'boolean': 'boolean',
31
31
  'timestamp with time zone': 'datetime',
32
- 'timestamp without time zone': 'datetime',
32
+ // No zone: copied as its digits, never as an instant (drivers/db/dateValues.js).
33
+ 'timestamp without time zone': 'localdatetime',
33
34
  // A DATE IS NOT A TIMESTAMP, and mapping it to one moves it.
34
35
  //
35
36
  // This said 'datetime', so a source `date` was created as TIMESTAMPTZ in the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xeplr/actions",
3
- "version": "1.1.18",
3
+ "version": "1.1.20",
4
4
  "description": "Named-action registry with typed inputs and safe invocation — plus built-in actions for HTTP, DB, files, email, spawn",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -16,7 +16,11 @@
16
16
  "http",
17
17
  "db",
18
18
  "file",
19
- "email"
19
+ "email",
20
+ "export",
21
+ "csv",
22
+ "xlsx",
23
+ "pdf"
20
24
  ],
21
25
  "author": "xeplr",
22
26
  "license": "MIT",
@@ -28,15 +32,17 @@
28
32
  "access": "public"
29
33
  },
30
34
  "dependencies": {
31
- "@xeplr/schema-handler": "^1.0.0",
32
- "@xeplr/expression-handler": "^2.0.0"
35
+ "@xeplr/expression-handler": "^2.0.0",
36
+ "@xeplr/schema-handler": "^1.0.0"
33
37
  },
34
38
  "peerDependencies": {
35
39
  "@duckdb/node-api": "1.5.5-r.3",
36
40
  "@xeplr/email": "^1.0.0",
37
41
  "@xeplr/utils": "^1.0.0",
38
42
  "csv-parse": "^7.0.2",
43
+ "dejavu-fonts-ttf": "^2.37.3",
39
44
  "exceljs": "^4.0.0",
45
+ "fflate": "^0.8.2",
40
46
  "imapflow": "^1.0.0",
41
47
  "knex": "^3.0.0",
42
48
  "mailparser": "^3.0.0",
@@ -44,6 +50,7 @@
44
50
  "mssql": "^11.0.0",
45
51
  "mysql2": "^3.0.0",
46
52
  "nodemailer": "^10.0.13",
53
+ "pdfkit": "^0.20.2",
47
54
  "pg": "^8.0.0",
48
55
  "saxes": "^6.0.0",
49
56
  "ssh2-sftp-client": "^11.0.0",
@@ -97,15 +104,27 @@
97
104
  },
98
105
  "@xeplr/email": {
99
106
  "optional": true
107
+ },
108
+ "fflate": {
109
+ "optional": true
110
+ },
111
+ "pdfkit": {
112
+ "optional": true
113
+ },
114
+ "dejavu-fonts-ttf": {
115
+ "optional": true
100
116
  }
101
117
  },
102
118
  "devDependencies": {
103
119
  "@duckdb/node-api": "^1.5.5-r.3",
104
120
  "@xeplr/utils": "^1.0.0",
105
121
  "csv-parse": "^7.0.2",
122
+ "dejavu-fonts-ttf": "^2.37.3",
106
123
  "express": "^4.0.0",
124
+ "fflate": "^0.8.3",
107
125
  "mssql": "^12.7.0",
108
126
  "mysql2": "^3.23.1",
127
+ "pdfkit": "^0.20.2",
109
128
  "pg": "^8.0.0",
110
129
  "saxes": "^6.0.0",
111
130
  "unzipper": "^0.12.0"