webspresso 0.0.95 → 0.0.97

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.
Files changed (94) hide show
  1. package/.agents/ADMIN.md +225 -0
  2. package/.agents/AGENTS.md +72 -0
  3. package/.agents/ARCHITECTURE.md +119 -0
  4. package/.agents/AUTH.md +103 -0
  5. package/.agents/CLI.md +115 -0
  6. package/.agents/COMPRESSION.md +59 -0
  7. package/.agents/CONVENTIONS.md +46 -0
  8. package/.agents/EMAIL.md +141 -0
  9. package/.agents/ERRORS.md +201 -0
  10. package/.agents/MCP.md +103 -0
  11. package/.agents/ORM.md +230 -0
  12. package/.agents/PLUGINS.md +419 -0
  13. package/.agents/QUEUE.md +113 -0
  14. package/.agents/REALTIME.md +213 -0
  15. package/.agents/ROUTING.md +292 -0
  16. package/.agents/SERVICES.md +341 -0
  17. package/.agents/SHUTDOWN.md +67 -0
  18. package/.agents/STREAMING.md +73 -0
  19. package/.agents/WORKFLOW.md +27 -0
  20. package/.agents/internal/DOCUMENTATION_REVIEW.md +485 -0
  21. package/.agents/internal/TECHNICAL_REVIEW.md +428 -0
  22. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
  23. package/.agents/skills/webspresso-framework/SKILL.md +56 -0
  24. package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
  25. package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
  26. package/.agents/skills/webspresso-services/SKILL.md +101 -0
  27. package/.cursorrules +14 -0
  28. package/AGENTS.md +175 -0
  29. package/CLAUDE.md +54 -0
  30. package/README.md +18 -14
  31. package/bin/commands/admin-password.js +2 -1
  32. package/bin/commands/admin-setup.js +1 -1
  33. package/bin/commands/agents-init.js +209 -0
  34. package/bin/commands/api.js +114 -48
  35. package/bin/commands/audit-prune.js +1 -1
  36. package/bin/commands/doctor.js +35 -0
  37. package/bin/commands/favicon-generate.js +1 -1
  38. package/bin/commands/new.js +13 -2
  39. package/bin/commands/orm-map.js +5 -4
  40. package/bin/commands/page.js +75 -89
  41. package/bin/commands/polar-migrate.js +1 -1
  42. package/bin/commands/seed.js +1 -1
  43. package/bin/commands/service.js +77 -0
  44. package/bin/webspresso.js +2 -2
  45. package/core/auth/hash.js +10 -5
  46. package/core/compileSchema.js +1 -1
  47. package/core/errors/middleware.js +22 -2
  48. package/core/errors/normalize.js +29 -2
  49. package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
  50. package/index.d.ts +306 -0
  51. package/index.js +35 -1
  52. package/package.json +36 -14
  53. package/plugins/admin-panel/app.js +10 -9
  54. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
  55. package/plugins/admin-panel/core/api-extensions.js +4 -0
  56. package/plugins/basic-auth/index.js +3 -1
  57. package/plugins/content/client/inline-edit.js +12 -12
  58. package/plugins/cors.js +6 -7
  59. package/plugins/csrf/index.js +3 -2
  60. package/plugins/csv/engine.js +447 -0
  61. package/plugins/csv/index.js +94 -0
  62. package/plugins/csv/middleware.js +80 -0
  63. package/plugins/csv/services.js +138 -0
  64. package/plugins/file-manager/admin-component.js +613 -0
  65. package/plugins/file-manager/api-handlers.js +261 -0
  66. package/plugins/file-manager/core.js +601 -0
  67. package/plugins/file-manager/index.js +139 -0
  68. package/plugins/file-manager/security.js +217 -0
  69. package/plugins/index.js +45 -79
  70. package/plugins/mcp/transports/sse.js +4 -2
  71. package/plugins/site-analytics/admin-component.js +7 -8
  72. package/plugins/xlsx/engine.js +397 -0
  73. package/plugins/xlsx/index.js +91 -0
  74. package/plugins/xlsx/middleware.js +87 -0
  75. package/plugins/xlsx/services.js +118 -0
  76. package/src/api/api-loader.js +233 -0
  77. package/src/api/define-api.js +35 -0
  78. package/src/discovery/file-route-parser.js +201 -0
  79. package/src/discovery/scan-routes.js +265 -0
  80. package/src/file-router.js +9 -1
  81. package/src/modules/define-module.js +99 -0
  82. package/src/modules/module-discovery.js +117 -0
  83. package/src/pages/define-page.js +36 -0
  84. package/src/pages/page-loader.js +214 -0
  85. package/src/routing/mount-discovered-routes.js +78 -0
  86. package/src/routing/route-table.js +191 -0
  87. package/src/server.js +112 -15
  88. package/src/services/builtins/file-manager.js +178 -0
  89. package/src/services/discovery.js +6 -1
  90. package/src/services/index.js +6 -11
  91. package/bin/commands/skill.js +0 -187
  92. package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
  93. package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
  94. package/templates/skills/webspresso-usage/SKILL.md +0 -48
@@ -0,0 +1,397 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso XLSX Core Engine
5
+ * High-performance Excel spreadsheet generator, parser, and ORM exporter
6
+ * @module plugins/xlsx/engine
7
+ */
8
+
9
+ const ExcelJS = require('exceljs');
10
+ const { PassThrough } = require('stream');
11
+
12
+ /**
13
+ * Common formula injection trigger characters
14
+ * @type {RegExp}
15
+ */
16
+ const FORMULA_INJECTION_REGEX = /^([=+\-@\t\r])/;
17
+
18
+ /**
19
+ * Neutralizes Excel formula injection by escaping dangerous prefixes
20
+ * @param {*} value
21
+ * @returns {*}
22
+ */
23
+ function sanitizeFormula(value) {
24
+ if (typeof value === 'string' && FORMULA_INJECTION_REGEX.test(value)) {
25
+ return `'${value}`;
26
+ }
27
+ return value;
28
+ }
29
+
30
+ /**
31
+ * Converts a 6-character hex color (e.g. '4F46E5' or '#4F46E5') to ARGB
32
+ * @param {string} hex
33
+ * @returns {string}
34
+ */
35
+ function hexToArgb(hex) {
36
+ if (!hex || typeof hex !== 'string') return 'FF4F46E5';
37
+ let clean = hex.replace(/^#/, '').trim();
38
+ if (clean.length === 3) {
39
+ clean = clean.split('').map((c) => c + c).join('');
40
+ }
41
+ if (clean.length === 6) {
42
+ return `FF${clean.toUpperCase()}`;
43
+ }
44
+ if (clean.length === 8) {
45
+ return clean.toUpperCase();
46
+ }
47
+ return 'FF4F46E5';
48
+ }
49
+
50
+ /**
51
+ * Automatically calculates optimal column widths based on headers and sample rows
52
+ * @param {ExcelJS.Worksheet} worksheet
53
+ * @param {Array<Object>} columns
54
+ * @param {Array<Object>} rows
55
+ */
56
+ function applyAutoWidths(worksheet, columns, rows) {
57
+ if (!columns || !Array.isArray(columns)) return;
58
+
59
+ columns.forEach((col, index) => {
60
+ const colIndex = index + 1;
61
+ let maxLength = col.header ? String(col.header).length : 10;
62
+
63
+ // Sample up to first 100 rows for performance
64
+ const sampleLimit = Math.min(rows.length, 100);
65
+ for (let i = 0; i < sampleLimit; i++) {
66
+ const val = rows[i] ? rows[i][col.key] : '';
67
+ if (val !== null && val !== undefined) {
68
+ const strVal = val instanceof Date ? 'YYYY-MM-DD HH:mm:ss' : String(val);
69
+ if (strVal.length > maxLength) {
70
+ maxLength = strVal.length;
71
+ }
72
+ }
73
+ }
74
+
75
+ const column = worksheet.getColumn(colIndex);
76
+ column.width = Math.min(Math.max(maxLength + 4, 12), 60);
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Applies styled headers to a worksheet
82
+ * @param {ExcelJS.Worksheet} worksheet
83
+ * @param {Object} [style]
84
+ */
85
+ function applyHeaderStyle(worksheet, style = {}) {
86
+ const headerRow = worksheet.getRow(1);
87
+ const bgColor = hexToArgb(style.bg || style.fill || '4F46E5');
88
+ const textColor = hexToArgb(style.color || style.textColor || 'FFFFFF');
89
+
90
+ headerRow.height = style.height || 26;
91
+ headerRow.eachCell((cell) => {
92
+ cell.font = {
93
+ name: style.fontName || 'Segoe UI',
94
+ size: style.fontSize || 11,
95
+ bold: style.bold !== false,
96
+ color: { argb: textColor },
97
+ };
98
+ cell.fill = {
99
+ type: 'pattern',
100
+ pattern: 'solid',
101
+ fgColor: { argb: bgColor },
102
+ };
103
+ cell.alignment = {
104
+ vertical: 'middle',
105
+ horizontal: style.align || 'left',
106
+ wrapText: false,
107
+ };
108
+ cell.border = {
109
+ top: { style: 'thin', color: { argb: 'FFE5E7EB' } },
110
+ left: { style: 'thin', color: { argb: 'FFE5E7EB' } },
111
+ bottom: { style: 'medium', color: { argb: 'FF3730A3' } },
112
+ right: { style: 'thin', color: { argb: 'FFE5E7EB' } },
113
+ };
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Generates an Excel XLSX workbook buffer from declarative options
119
+ * @param {Object} options
120
+ * @param {string} [options.creator='Webspresso']
121
+ * @param {string} [options.title]
122
+ * @param {Date} [options.created]
123
+ * @param {boolean} [options.sanitizeFormulas=true]
124
+ * @param {Array<Object>|Object} options.sheets - Sheet configuration(s)
125
+ * @returns {Promise<Buffer>}
126
+ */
127
+ async function generate(options = {}) {
128
+ const workbook = new ExcelJS.Workbook();
129
+ workbook.creator = options.creator || 'Webspresso';
130
+ workbook.created = options.created || new Date();
131
+ if (options.title) workbook.title = options.title;
132
+
133
+ const sanitize = options.sanitizeFormulas !== false;
134
+
135
+ // Normalize sheets: can be single object or array
136
+ const rawSheets = options.sheets
137
+ ? Array.isArray(options.sheets) ? options.sheets : [options.sheets]
138
+ : [{ name: options.name || 'Sheet1', columns: options.columns, rows: options.rows || options.data || [] }];
139
+
140
+ for (let idx = 0; idx < rawSheets.length; idx++) {
141
+ const sheetConfig = rawSheets[idx];
142
+ const sheetName = sheetConfig.name || `Sheet${idx + 1}`;
143
+ const worksheet = workbook.addWorksheet(sheetName, {
144
+ views: sheetConfig.views || [{ state: 'frozen', xSplit: 0, ySplit: 1 }],
145
+ });
146
+
147
+ const rows = Array.isArray(sheetConfig.rows)
148
+ ? sheetConfig.rows
149
+ : Array.isArray(sheetConfig.data)
150
+ ? sheetConfig.data
151
+ : [];
152
+
153
+ // Derive columns if not explicitly provided
154
+ let columns = sheetConfig.columns;
155
+ if (!columns && rows.length > 0 && typeof rows[0] === 'object' && rows[0] !== null) {
156
+ columns = Object.keys(rows[0]).map((key) => ({
157
+ header: key,
158
+ key,
159
+ }));
160
+ }
161
+
162
+ if (columns && Array.isArray(columns)) {
163
+ worksheet.columns = columns.map((col) => ({
164
+ header: col.header || col.name || col.key,
165
+ key: col.key,
166
+ width: col.width || 15,
167
+ style: col.style || {},
168
+ }));
169
+
170
+ // Apply auto-widths if requested (default: true)
171
+ if (sheetConfig.autoWidth !== false) {
172
+ applyAutoWidths(worksheet, columns, rows);
173
+ }
174
+
175
+ // Add rows
176
+ for (const row of rows) {
177
+ const rowData = {};
178
+ for (const col of columns) {
179
+ let val = row ? row[col.key] : '';
180
+ if (sanitize) {
181
+ val = sanitizeFormula(val);
182
+ }
183
+ rowData[col.key] = val !== undefined ? val : null;
184
+ }
185
+ worksheet.addRow(rowData);
186
+ }
187
+
188
+ // Format headers
189
+ applyHeaderStyle(worksheet, sheetConfig.headerStyle || options.headerStyle);
190
+
191
+ // Apply zebra striping or row borders if enabled
192
+ if (sheetConfig.striped !== false) {
193
+ worksheet.eachRow((row, rowNumber) => {
194
+ if (rowNumber === 1) return;
195
+ row.alignment = { vertical: 'middle' };
196
+ if (rowNumber % 2 === 0) {
197
+ row.eachCell((cell) => {
198
+ if (!cell.fill || cell.fill.type !== 'pattern') {
199
+ cell.fill = {
200
+ type: 'pattern',
201
+ pattern: 'solid',
202
+ fgColor: { argb: 'FFF9FAFB' },
203
+ };
204
+ }
205
+ });
206
+ }
207
+ });
208
+ }
209
+ }
210
+ }
211
+
212
+ const buffer = await workbook.xlsx.writeBuffer();
213
+ return Buffer.from(buffer);
214
+ }
215
+
216
+ /**
217
+ * Parses an XLSX buffer or stream into JSON objects
218
+ * @param {Buffer|import('stream').Readable} input
219
+ * @param {Object} [options]
220
+ * @param {string|number} [options.sheet] - Sheet name or 1-based index
221
+ * @param {number} [options.headerRow=1] - Row index of headers
222
+ * @param {number} [options.dataStartRow=2] - Row index where data begins
223
+ * @param {boolean} [options.raw=false] - Return raw cell values without formatting
224
+ * @param {Function} [options.transformRow] - Custom row transformer
225
+ * @returns {Promise<Array<Object>|Object.<string, Array<Object>>>}
226
+ */
227
+ async function parse(input, options = {}) {
228
+ const workbook = new ExcelJS.Workbook();
229
+
230
+ if (Buffer.isBuffer(input)) {
231
+ await workbook.xlsx.load(input);
232
+ } else if (input && typeof input.pipe === 'function') {
233
+ await workbook.xlsx.read(input);
234
+ } else {
235
+ throw new Error('parseXlsx requires a valid Buffer or Readable Stream');
236
+ }
237
+
238
+ const headerRowIdx = options.headerRow || 1;
239
+ const dataStartRowIdx = options.dataStartRow || headerRowIdx + 1;
240
+ const transformRow = typeof options.transformRow === 'function' ? options.transformRow : null;
241
+
242
+ function parseWorksheet(ws) {
243
+ const headers = [];
244
+ const headerRow = ws.getRow(headerRowIdx);
245
+
246
+ headerRow.eachCell({ includeEmpty: false }, (cell, colNumber) => {
247
+ let val = cell.value;
248
+ if (typeof val === 'object' && val !== null) {
249
+ val = val.text || val.result || String(val);
250
+ }
251
+ const headerName = String(val || `col_${colNumber}`).trim();
252
+ headers[colNumber] = headerName;
253
+ });
254
+
255
+ const rows = [];
256
+ const rowCount = ws.rowCount;
257
+
258
+ for (let r = dataStartRowIdx; r <= rowCount; r++) {
259
+ const row = ws.getRow(r);
260
+ if (!row || !row.hasValues) continue;
261
+
262
+ const rowObj = {};
263
+ let hasData = false;
264
+
265
+ headers.forEach((header, colNum) => {
266
+ if (!header) return;
267
+ const cell = row.getCell(colNum);
268
+ let cellVal = cell.value;
269
+
270
+ if (cellVal && typeof cellVal === 'object') {
271
+ if (cellVal instanceof Date) {
272
+ // Keep Date instance or format
273
+ } else if (cellVal.text !== undefined) {
274
+ cellVal = cellVal.text;
275
+ } else if (cellVal.result !== undefined) {
276
+ cellVal = cellVal.result;
277
+ }
278
+ }
279
+
280
+ if (cellVal !== null && cellVal !== undefined && cellVal !== '') {
281
+ hasData = true;
282
+ }
283
+
284
+ rowObj[header] = cellVal !== undefined ? cellVal : null;
285
+ });
286
+
287
+ if (hasData) {
288
+ rows.push(transformRow ? transformRow(rowObj, rows.length) : rowObj);
289
+ }
290
+ }
291
+
292
+ return rows;
293
+ }
294
+
295
+ // If specific sheet requested
296
+ if (options.sheet !== undefined) {
297
+ let ws;
298
+ if (typeof options.sheet === 'number') {
299
+ ws = workbook.getWorksheet(options.sheet);
300
+ } else {
301
+ ws = workbook.getWorksheet(String(options.sheet));
302
+ }
303
+
304
+ if (!ws) {
305
+ throw new Error(`Worksheet "${options.sheet}" not found in XLSX workbook`);
306
+ }
307
+
308
+ return parseWorksheet(ws);
309
+ }
310
+
311
+ // If only 1 sheet exists, return array directly for convenience
312
+ if (workbook.worksheets.length === 1) {
313
+ return parseWorksheet(workbook.worksheets[0]);
314
+ }
315
+
316
+ // Multiple sheets: return dictionary keyed by sheet name
317
+ const result = {};
318
+ for (const ws of workbook.worksheets) {
319
+ result[ws.name] = parseWorksheet(ws);
320
+ }
321
+ return result;
322
+ }
323
+
324
+ /**
325
+ * Automatically exports records from an ORM Repository into an Excel workbook buffer
326
+ * @param {Object} repository - Model repository from db.getRepository('Model')
327
+ * @param {Object} [filterOrQuery={}] - Query filters
328
+ * @param {Object} [options={}] - Export options
329
+ * @returns {Promise<Buffer>}
330
+ */
331
+ async function fromModel(repository, filterOrQuery = {}, options = {}) {
332
+ if (!repository || (typeof repository.find !== 'function' && typeof repository.query !== 'function')) {
333
+ throw new Error('fromModel requires a valid Webspresso Model Repository');
334
+ }
335
+
336
+ let records;
337
+ if (typeof filterOrQuery === 'function') {
338
+ if (typeof repository.query !== 'function') {
339
+ throw new Error('fromModel query callback requires a repository with query() method');
340
+ }
341
+ records = await filterOrQuery(repository.query());
342
+ } else {
343
+ records = typeof repository.find === 'function' ? await repository.find(filterOrQuery) : [];
344
+ }
345
+
346
+ const modelName = repository.model?.name || 'Records';
347
+ const sheetName = options.sheetName || options.name || modelName;
348
+
349
+ let columns = options.columns;
350
+ if (!columns && repository.model?.schema?.shape) {
351
+ const hidden = new Set(repository.model.hidden || []);
352
+ columns = Object.keys(repository.model.schema.shape)
353
+ .filter((col) => !hidden.has(col))
354
+ .map((col) => ({
355
+ header: col.charAt(0).toUpperCase() + col.slice(1).replace(/_/g, ' '),
356
+ key: col,
357
+ }));
358
+ }
359
+
360
+ return generate({
361
+ creator: options.creator || 'Webspresso ORM',
362
+ title: options.title || `${modelName} Export`,
363
+ sheets: [
364
+ {
365
+ name: sheetName,
366
+ columns,
367
+ rows: records,
368
+ headerStyle: options.headerStyle,
369
+ },
370
+ ],
371
+ });
372
+ }
373
+
374
+ /**
375
+ * Creates a high-performance streaming workbook writer for massive datasets
376
+ * @param {import('stream').Writable} outputStream
377
+ * @param {Object} [options]
378
+ * @returns {ExcelJS.stream.xlsx.WorkbookWriter}
379
+ */
380
+ function createStreamWriter(outputStream, options = {}) {
381
+ return new ExcelJS.stream.xlsx.WorkbookWriter({
382
+ stream: outputStream,
383
+ useStyles: options.useStyles !== false,
384
+ useSharedStrings: options.useSharedStrings || false,
385
+ });
386
+ }
387
+
388
+ module.exports = {
389
+ generate,
390
+ parse,
391
+ fromModel,
392
+ createStreamWriter,
393
+ sanitizeFormula,
394
+ applyHeaderStyle,
395
+ applyAutoWidths,
396
+ hexToArgb,
397
+ };
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso XLSX Plugin
5
+ * Excel spreadsheet generation, parsing, ORM export, streaming, and res.xlsx() integration
6
+ * @module plugins/xlsx
7
+ */
8
+
9
+ const engine = require('./engine');
10
+ const { createXlsxMiddleware, XLSX_MIME_TYPE } = require('./middleware');
11
+ const { createXlsxServices } = require('./services');
12
+
13
+ /**
14
+ * Standalone XLSX Toolkit Singleton
15
+ */
16
+ const xlsx = {
17
+ ...engine,
18
+ MIME_TYPE: XLSX_MIME_TYPE,
19
+ createMiddleware: createXlsxMiddleware,
20
+ createServices: createXlsxServices,
21
+ };
22
+
23
+ /**
24
+ * XLSX Plugin for Webspresso
25
+ * @param {Object} [options]
26
+ * @param {boolean} [options.enabled=true]
27
+ * @param {boolean} [options.sanitizeFormulas=true] - Prevent Formula Injection attacks
28
+ * @param {Object} [options.headerStyle] - Default header styling (fill, font, border)
29
+ * @param {Object} [options.db] - Optional database instance for model export services
30
+ * @returns {import('../../index').Plugin}
31
+ */
32
+ function xlsxPlugin(options = {}) {
33
+ const {
34
+ enabled = true,
35
+ sanitizeFormulas = true,
36
+ headerStyle = { fill: '4F46E5', color: 'FFFFFF', bold: true },
37
+ db: dbOption,
38
+ } = options;
39
+
40
+ return {
41
+ name: 'xlsx',
42
+ version: '1.0.0',
43
+ description: 'Excel XLSX spreadsheet engine, res.xlsx() response decorator, and ORM export tools',
44
+ enabled,
45
+
46
+ register(ctx) {
47
+ if (!enabled) return;
48
+
49
+ // 1. Expose on PluginContext & global app
50
+ ctx.xlsx = xlsx;
51
+ if (ctx.app) {
52
+ ctx.app.xlsx = xlsx;
53
+ }
54
+
55
+ // 2. Register built-in services (xlsx.generate, xlsx.parse, xlsx.exportModel)
56
+ if (ctx.app && ctx.app.serviceRegistry) {
57
+ const db = dbOption ?? ctx.db ?? ctx.app?.get?.('db') ?? null;
58
+ const services = createXlsxServices({ db, ...options });
59
+ for (const [name, def] of Object.entries(services)) {
60
+ if (!ctx.app.serviceRegistry.has(name)) {
61
+ ctx.app.serviceRegistry.register(name, def);
62
+ }
63
+ }
64
+ }
65
+
66
+ // 3. Register named middleware & global decorator
67
+ const middleware = createXlsxMiddleware({
68
+ sanitizeFormulas,
69
+ headerStyle,
70
+ ...options,
71
+ });
72
+
73
+ if (ctx.middlewares) {
74
+ ctx.middlewares.xlsx = middleware;
75
+ }
76
+
77
+ if (ctx.app && typeof ctx.app.use === 'function') {
78
+ ctx.app.use(middleware);
79
+ }
80
+ },
81
+ };
82
+ }
83
+
84
+ module.exports = {
85
+ xlsxPlugin,
86
+ xlsx,
87
+ createXlsxMiddleware,
88
+ createXlsxServices,
89
+ XLSX_MIME_TYPE,
90
+ ...engine,
91
+ };
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * XLSX Express Middleware
5
+ * Decorates res.xlsx() and req.parseXlsx() helpers
6
+ * @module plugins/xlsx/middleware
7
+ */
8
+
9
+ const { generate, parse, fromModel } = require('./engine');
10
+
11
+ const XLSX_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
12
+
13
+ /**
14
+ * Creates the XLSX middleware
15
+ * @param {Object} [globalOptions]
16
+ * @returns {Function}
17
+ */
18
+ function createXlsxMiddleware(globalOptions = {}) {
19
+ return (req, res, next) => {
20
+ // 1. Response Helper: res.xlsx(data, filename, options)
21
+ res.xlsx = async function (data, filename = 'export.xlsx', options = {}) {
22
+ try {
23
+ let buffer;
24
+
25
+ if (Buffer.isBuffer(data)) {
26
+ buffer = data;
27
+ } else if (data && typeof data.find === 'function' && data.model) {
28
+ // Repository instance
29
+ buffer = await fromModel(data, options.filter || options.query || {}, {
30
+ ...globalOptions,
31
+ ...options,
32
+ });
33
+ } else if (Array.isArray(data)) {
34
+ buffer = await generate({
35
+ ...globalOptions,
36
+ ...options,
37
+ sheets: [
38
+ {
39
+ name: options.sheetName || 'Data',
40
+ columns: options.columns,
41
+ rows: data,
42
+ headerStyle: options.headerStyle,
43
+ },
44
+ ],
45
+ });
46
+ } else if (data && typeof data === 'object') {
47
+ buffer = await generate({
48
+ ...globalOptions,
49
+ ...options,
50
+ ...data,
51
+ });
52
+ } else {
53
+ throw new Error('res.xlsx() expects an Array of rows, a sheets object, a Model Repository, or a Buffer');
54
+ }
55
+
56
+ const safeFilename = filename.endsWith('.xlsx') ? filename : `${filename}.xlsx`;
57
+
58
+ res.setHeader('Content-Type', XLSX_MIME_TYPE);
59
+ res.setHeader(
60
+ 'Content-Disposition',
61
+ `attachment; filename="${encodeURIComponent(safeFilename)}"; filename*=UTF-8''${encodeURIComponent(safeFilename)}`
62
+ );
63
+ res.setHeader('Content-Length', buffer.length);
64
+
65
+ return res.end(buffer);
66
+ } catch (err) {
67
+ return next(err);
68
+ }
69
+ };
70
+
71
+ // 2. Request Helper: req.parseXlsx(fileOrBuffer, options)
72
+ req.parseXlsx = async function (fileOrBuffer, options = {}) {
73
+ const target = fileOrBuffer || req.file?.buffer || req.rawBody || req.body;
74
+ if (!target) {
75
+ throw new Error('req.parseXlsx requires a file Buffer or multipart req.file.buffer');
76
+ }
77
+ return parse(target, options);
78
+ };
79
+
80
+ next();
81
+ };
82
+ }
83
+
84
+ module.exports = {
85
+ createXlsxMiddleware,
86
+ XLSX_MIME_TYPE,
87
+ };
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Built-in XLSX Services
5
+ * Exposed via ctx.service('xlsx.*') and req.service('xlsx.*')
6
+ * @module plugins/xlsx/services
7
+ */
8
+
9
+ const { z } = require('zod');
10
+ const { generate, parse, fromModel } = require('./engine');
11
+
12
+ /**
13
+ * Creates built-in services for the XLSX plugin
14
+ * @param {Object} [options]
15
+ * @param {Object} [options.db]
16
+ * @returns {Object.<string, Object>}
17
+ */
18
+ function createXlsxServices(options = {}) {
19
+ return {
20
+ 'xlsx.generate': {
21
+ schema: z.object({
22
+ sheets: z.array(
23
+ z.object({
24
+ name: z.string().optional(),
25
+ columns: z.array(z.object({ header: z.string(), key: z.string(), width: z.number().optional() })).optional(),
26
+ rows: z.array(z.record(z.any())),
27
+ })
28
+ ).or(z.object({
29
+ name: z.string().optional(),
30
+ columns: z.array(z.object({ header: z.string(), key: z.string(), width: z.number().optional() })).optional(),
31
+ rows: z.array(z.record(z.any())),
32
+ })),
33
+ creator: z.string().optional(),
34
+ title: z.string().optional(),
35
+ }),
36
+ handler: async (input) => {
37
+ const buffer = await generate(input);
38
+ return {
39
+ base64: buffer.toString('base64'),
40
+ size: buffer.length,
41
+ mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
42
+ };
43
+ },
44
+ },
45
+
46
+ 'xlsx.parse': {
47
+ schema: z.object({
48
+ base64: z.string().optional(),
49
+ sheet: z.union([z.string(), z.number()]).optional(),
50
+ headerRow: z.number().optional(),
51
+ }),
52
+ handler: async (input) => {
53
+ if (!input.base64 && !input.buffer) {
54
+ throw new Error('xlsx.parse requires base64 string or buffer');
55
+ }
56
+ const buffer = input.buffer || Buffer.from(input.base64, 'base64');
57
+ const data = await parse(buffer, {
58
+ sheet: input.sheet,
59
+ headerRow: input.headerRow,
60
+ });
61
+
62
+ const rowCount = Array.isArray(data)
63
+ ? data.length
64
+ : Object.values(data).reduce((acc, sheetRows) => acc + sheetRows.length, 0);
65
+
66
+ return {
67
+ data,
68
+ rowCount,
69
+ };
70
+ },
71
+ },
72
+
73
+ 'xlsx.exportModel': {
74
+ schema: z.object({
75
+ model: z.string().min(1),
76
+ filter: z.record(z.any()).optional().default({}),
77
+ columns: z.array(z.string()).optional(),
78
+ filename: z.string().optional(),
79
+ }),
80
+ handler: async (input, ctx) => {
81
+ const db = ctx.db || options.db;
82
+ if (!db || typeof db.getRepository !== 'function') {
83
+ throw new Error('xlsx.exportModel requires an active database connection');
84
+ }
85
+
86
+ const repo = db.getRepository(input.model);
87
+ if (!repo) {
88
+ throw new Error(`Model repository "${input.model}" not found`);
89
+ }
90
+
91
+ let customColumns;
92
+ if (input.columns) {
93
+ customColumns = input.columns.map((c) => ({
94
+ header: c.charAt(0).toUpperCase() + c.slice(1).replace(/_/g, ' '),
95
+ key: c,
96
+ }));
97
+ }
98
+
99
+ const buffer = await fromModel(repo, input.filter, {
100
+ columns: customColumns,
101
+ title: `${input.model} Export`,
102
+ });
103
+
104
+ const filename = input.filename || `${input.model.toLowerCase()}_export.xlsx`;
105
+
106
+ return {
107
+ filename: filename.endsWith('.xlsx') ? filename : `${filename}.xlsx`,
108
+ base64: buffer.toString('base64'),
109
+ size: buffer.length,
110
+ };
111
+ },
112
+ },
113
+ };
114
+ }
115
+
116
+ module.exports = {
117
+ createXlsxServices,
118
+ };