webspresso 0.0.94 → 0.0.96

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 (83) 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/.cursorrules +14 -0
  23. package/AGENTS.md +175 -0
  24. package/CLAUDE.md +54 -0
  25. package/README.md +18 -14
  26. package/bin/commands/agents-init.js +169 -0
  27. package/bin/commands/api.js +114 -48
  28. package/bin/commands/doctor.js +35 -0
  29. package/bin/commands/new.js +13 -2
  30. package/bin/commands/page.js +75 -89
  31. package/bin/commands/service.js +77 -0
  32. package/bin/webspresso.js +2 -2
  33. package/core/compileSchema.js +1 -1
  34. package/core/errors/middleware.js +22 -2
  35. package/core/errors/normalize.js +29 -2
  36. package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
  37. package/index.d.ts +306 -0
  38. package/index.js +35 -1
  39. package/package.json +19 -6
  40. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
  41. package/plugins/csrf/index.js +3 -2
  42. package/plugins/csv/engine.js +447 -0
  43. package/plugins/csv/index.js +94 -0
  44. package/plugins/csv/middleware.js +80 -0
  45. package/plugins/csv/services.js +138 -0
  46. package/plugins/file-manager/admin-component.js +613 -0
  47. package/plugins/file-manager/api-handlers.js +261 -0
  48. package/plugins/file-manager/core.js +601 -0
  49. package/plugins/file-manager/index.js +139 -0
  50. package/plugins/file-manager/security.js +207 -0
  51. package/plugins/index.js +8 -0
  52. package/plugins/polar/CHANGELOG.md +18 -0
  53. package/plugins/polar/README.md +30 -7
  54. package/plugins/polar/index.js +6 -1
  55. package/plugins/polar/src/billing-hooks.js +51 -0
  56. package/plugins/polar/src/config.js +2 -0
  57. package/plugins/polar/src/rate-limit.js +96 -28
  58. package/plugins/polar/src/routes.js +3 -2
  59. package/plugins/polar/src/urls.js +26 -2
  60. package/plugins/polar/src/user-resolver.js +5 -1
  61. package/plugins/rate-limit/index.js +5 -1
  62. package/plugins/xlsx/engine.js +397 -0
  63. package/plugins/xlsx/index.js +91 -0
  64. package/plugins/xlsx/middleware.js +87 -0
  65. package/plugins/xlsx/services.js +118 -0
  66. package/src/api/api-loader.js +233 -0
  67. package/src/api/define-api.js +35 -0
  68. package/src/discovery/file-route-parser.js +201 -0
  69. package/src/discovery/scan-routes.js +265 -0
  70. package/src/file-router.js +6 -0
  71. package/src/modules/define-module.js +99 -0
  72. package/src/modules/module-discovery.js +117 -0
  73. package/src/pages/define-page.js +36 -0
  74. package/src/pages/page-loader.js +214 -0
  75. package/src/routing/mount-discovered-routes.js +78 -0
  76. package/src/routing/route-table.js +191 -0
  77. package/src/server.js +112 -15
  78. package/src/services/builtins/file-manager.js +178 -0
  79. package/src/services/index.js +2 -0
  80. package/bin/commands/skill.js +0 -187
  81. package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
  82. package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
  83. package/templates/skills/webspresso-usage/SKILL.md +0 -48
@@ -0,0 +1,447 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CSV Core Engine
5
+ * High-performance, RFC 4180 compliant CSV generator, parser, and ORM exporter
6
+ * @module plugins/csv/engine
7
+ */
8
+
9
+ const { parse: parseSync } = require('csv-parse/sync');
10
+ const { parse: parseStream } = require('csv-parse');
11
+ const { Readable } = require('stream');
12
+
13
+ /**
14
+ * UTF-8 Byte Order Mark (BOM)
15
+ * Ensures Microsoft Excel and other spreadsheet editors correctly open UTF-8 files (Turkish/Unicode characters)
16
+ */
17
+ const UTF8_BOM = '\uFEFF';
18
+
19
+ /**
20
+ * Standard CSV MIME type with charset
21
+ */
22
+ const CSV_MIME_TYPE = 'text/csv; charset=utf-8';
23
+
24
+ /**
25
+ * Common formula injection trigger characters
26
+ * @type {RegExp}
27
+ */
28
+ const FORMULA_INJECTION_REGEX = /^([=+\-@\t\r])/;
29
+
30
+ /**
31
+ * Neutralizes Excel/Calc formula injection by escaping dangerous prefixes
32
+ * @param {*} value
33
+ * @returns {*}
34
+ */
35
+ function sanitizeFormula(value) {
36
+ if (typeof value === 'string' && FORMULA_INJECTION_REGEX.test(value)) {
37
+ return `'${value}`;
38
+ }
39
+ return value;
40
+ }
41
+
42
+ /**
43
+ * Escapes a single cell value for CSV (RFC 4180 compliant)
44
+ * @param {*} value
45
+ * @param {Object} [options]
46
+ * @param {string} [options.delimiter=',']
47
+ * @param {string} [options.quote='"']
48
+ * @param {boolean} [options.alwaysQuote=false]
49
+ * @param {boolean} [options.sanitizeFormulas=true]
50
+ * @returns {string}
51
+ */
52
+ function escapeCsvField(value, options = {}) {
53
+ const {
54
+ delimiter = ',',
55
+ quote = '"',
56
+ alwaysQuote = false,
57
+ sanitizeFormulas: shouldSanitize = true,
58
+ } = options;
59
+
60
+ if (value === null || value === undefined) {
61
+ return '';
62
+ }
63
+
64
+ let str = value;
65
+ if (value instanceof Date) {
66
+ str = value.toISOString();
67
+ } else if (typeof value === 'object') {
68
+ try {
69
+ str = JSON.stringify(value);
70
+ } catch {
71
+ str = String(value);
72
+ }
73
+ } else {
74
+ str = String(value);
75
+ }
76
+
77
+ if (shouldSanitize) {
78
+ str = sanitizeFormula(str);
79
+ }
80
+
81
+ const needsQuotes =
82
+ alwaysQuote ||
83
+ str.includes(delimiter) ||
84
+ str.includes(quote) ||
85
+ str.includes('\n') ||
86
+ str.includes('\r') ||
87
+ str.startsWith(' ') ||
88
+ str.endsWith(' ');
89
+
90
+ if (needsQuotes) {
91
+ const escaped = str.split(quote).join(quote + quote);
92
+ return `${quote}${escaped}${quote}`;
93
+ }
94
+
95
+ return str;
96
+ }
97
+
98
+ /**
99
+ * Generates an RFC 4180 compliant CSV string or Buffer from structured data
100
+ * @param {Object} options
101
+ * @param {Array<Object>} [options.rows] - Array of row objects
102
+ * @param {Array<Object>} [options.data] - Alias for rows
103
+ * @param {Array<Object|string>} [options.columns] - Explicit column definitions
104
+ * @param {string} [options.delimiter=','] - Column delimiter (',', ';', '\t', '|')
105
+ * @param {string} [options.eol='\r\n'] - End of line sequence
106
+ * @param {boolean} [options.header=true] - Include header row
107
+ * @param {boolean} [options.bom=true] - Include UTF-8 BOM for Excel UTF-8 compatibility
108
+ * @param {boolean} [options.sanitizeFormulas=true] - Escape dangerous formula prefixes
109
+ * @param {boolean} [options.asBuffer=true] - Return Buffer (true) or String (false)
110
+ * @returns {Buffer|string}
111
+ */
112
+ function generate(options = {}) {
113
+ const {
114
+ delimiter = ',',
115
+ eol = '\r\n',
116
+ header = true,
117
+ bom = true,
118
+ sanitizeFormulas: shouldSanitize = true,
119
+ asBuffer = true,
120
+ } = options;
121
+
122
+ const rows = Array.isArray(options.rows)
123
+ ? options.rows
124
+ : Array.isArray(options.data)
125
+ ? options.data
126
+ : [];
127
+
128
+ // Derive or normalize columns
129
+ let columns = options.columns;
130
+ if (!columns && rows.length > 0 && typeof rows[0] === 'object' && rows[0] !== null) {
131
+ columns = Object.keys(rows[0]).map((key) => ({
132
+ header: key,
133
+ key,
134
+ }));
135
+ } else if (Array.isArray(columns)) {
136
+ columns = columns.map((col) => {
137
+ if (typeof col === 'string') {
138
+ return { header: col, key: col };
139
+ }
140
+ return {
141
+ header: col.header || col.name || col.key,
142
+ key: col.key || col.name || col.header,
143
+ };
144
+ });
145
+ } else {
146
+ columns = [];
147
+ }
148
+
149
+ const lines = [];
150
+
151
+ // Header line
152
+ if (header && columns.length > 0) {
153
+ const headerLine = columns
154
+ .map((col) =>
155
+ escapeCsvField(col.header, {
156
+ delimiter,
157
+ sanitizeFormulas: false,
158
+ })
159
+ )
160
+ .join(delimiter);
161
+ lines.push(headerLine);
162
+ }
163
+
164
+ // Row lines
165
+ for (const row of rows) {
166
+ if (!row || typeof row !== 'object') continue;
167
+ const rowFields = columns.map((col) => {
168
+ const val = row[col.key];
169
+ return escapeCsvField(val, {
170
+ delimiter,
171
+ sanitizeFormulas: shouldSanitize,
172
+ });
173
+ });
174
+ lines.push(rowFields.join(delimiter));
175
+ }
176
+
177
+ let csvContent = lines.join(eol);
178
+ if (lines.length > 0) {
179
+ csvContent += eol;
180
+ }
181
+
182
+ const fullContent = bom ? UTF8_BOM + csvContent : csvContent;
183
+
184
+ if (asBuffer) {
185
+ return Buffer.from(fullContent, 'utf8');
186
+ }
187
+
188
+ return fullContent;
189
+ }
190
+
191
+ /**
192
+ * Parses a CSV buffer, string, or stream into an array of JSON objects
193
+ * @param {Buffer|string|Readable} input
194
+ * @param {Object} [options]
195
+ * @param {string} [options.delimiter] - Explicit delimiter, or auto-detected if omitted
196
+ * @param {boolean|Array<string>} [options.columns=true] - Header row inference
197
+ * @param {boolean} [options.skip_empty_lines=true]
198
+ * @param {boolean} [options.trim=true]
199
+ * @param {boolean} [options.relax_column_count=true]
200
+ * @param {Function} [options.transformRow] - Optional custom transformer: (row, idx) => row
201
+ * @returns {Promise<Array<Object>>}
202
+ */
203
+ async function parse(input, options = {}) {
204
+ const {
205
+ delimiter,
206
+ columns = true,
207
+ skip_empty_lines = true,
208
+ trim = true,
209
+ relax_column_count = true,
210
+ transformRow,
211
+ ...restOptions
212
+ } = options;
213
+
214
+ let text;
215
+ if (Buffer.isBuffer(input)) {
216
+ text = input.toString('utf8');
217
+ } else if (typeof input === 'string') {
218
+ text = input;
219
+ } else if (input && typeof input.pipe === 'function') {
220
+ // Stream input
221
+ return new Promise((resolve, reject) => {
222
+ const results = [];
223
+ let index = 0;
224
+
225
+ const parser = parseStream({
226
+ delimiter,
227
+ columns,
228
+ skip_empty_lines,
229
+ trim,
230
+ relax_column_count,
231
+ bom: true,
232
+ ...restOptions,
233
+ });
234
+
235
+ parser.on('readable', () => {
236
+ let record;
237
+ while ((record = parser.read()) !== null) {
238
+ const transformed = typeof transformRow === 'function' ? transformRow(record, index++) : record;
239
+ if (transformed !== null && transformed !== undefined) {
240
+ results.push(transformed);
241
+ }
242
+ }
243
+ });
244
+
245
+ parser.on('error', reject);
246
+ parser.on('end', () => resolve(results));
247
+
248
+ input.pipe(parser);
249
+ });
250
+ } else {
251
+ throw new Error('parseCsv requires a valid Buffer, String, or Readable Stream');
252
+ }
253
+
254
+ // Strip BOM if present in string
255
+ if (text.charCodeAt(0) === 0xfeff) {
256
+ text = text.slice(1);
257
+ }
258
+
259
+ // Auto-detect delimiter if not specified
260
+ let detectedDelimiter = delimiter;
261
+ if (!detectedDelimiter) {
262
+ const firstLine = text.split(/\r\n|\r|\n/)[0] || '';
263
+ const semicolons = (firstLine.match(/;/g) || []).length;
264
+ const commas = (firstLine.match(/,/g) || []).length;
265
+ const tabs = (firstLine.match(/\t/g) || []).length;
266
+ const pipes = (firstLine.match(/\|/g) || []).length;
267
+
268
+ if (semicolons > commas && semicolons > tabs && semicolons > pipes) {
269
+ detectedDelimiter = ';';
270
+ } else if (tabs > commas && tabs > semicolons && tabs > pipes) {
271
+ detectedDelimiter = '\t';
272
+ } else if (pipes > commas && pipes > semicolons && pipes > tabs) {
273
+ detectedDelimiter = '|';
274
+ } else {
275
+ detectedDelimiter = ',';
276
+ }
277
+ }
278
+
279
+ const records = parseSync(text, {
280
+ delimiter: detectedDelimiter,
281
+ columns,
282
+ skip_empty_lines,
283
+ trim,
284
+ relax_column_count,
285
+ bom: true,
286
+ ...restOptions,
287
+ });
288
+
289
+ if (typeof transformRow === 'function') {
290
+ return records.map((record, idx) => transformRow(record, idx)).filter((r) => r !== null && r !== undefined);
291
+ }
292
+
293
+ return records;
294
+ }
295
+
296
+ /**
297
+ * Automatically exports records from an ORM Repository into a CSV buffer
298
+ * @param {Object} repository - Model repository from db.getRepository('Model')
299
+ * @param {Object|Function} [filterOrQuery={}] - Query filters or query callback
300
+ * @param {Object} [options={}] - Export options
301
+ * @returns {Promise<Buffer>}
302
+ */
303
+ async function fromModel(repository, filterOrQuery = {}, options = {}) {
304
+ if (!repository || (typeof repository.find !== 'function' && typeof repository.query !== 'function')) {
305
+ throw new Error('fromModel requires a valid Webspresso Model Repository');
306
+ }
307
+
308
+ let records;
309
+ if (typeof filterOrQuery === 'function') {
310
+ if (typeof repository.query !== 'function') {
311
+ throw new Error('fromModel query callback requires a repository with query() method');
312
+ }
313
+ records = await filterOrQuery(repository.query());
314
+ } else {
315
+ records = typeof repository.find === 'function' ? await repository.find(filterOrQuery) : [];
316
+ }
317
+
318
+ let columns = options.columns;
319
+ if (!columns && repository.model?.schema?.shape) {
320
+ const hidden = new Set(repository.model.hidden || []);
321
+ columns = Object.keys(repository.model.schema.shape)
322
+ .filter((col) => !hidden.has(col))
323
+ .map((col) => ({
324
+ header: col,
325
+ key: col,
326
+ }));
327
+ }
328
+
329
+ return generate({
330
+ ...options,
331
+ columns,
332
+ rows: records,
333
+ });
334
+ }
335
+
336
+ /**
337
+ * Creates a high-performance streaming CSV writer for massive datasets
338
+ * @param {import('stream').Writable} outputStream
339
+ * @param {Object} [options]
340
+ * @param {string} [options.delimiter=',']
341
+ * @param {string} [options.eol='\r\n']
342
+ * @param {boolean} [options.bom=true]
343
+ * @param {boolean} [options.sanitizeFormulas=true]
344
+ * @returns {{ writeHeader: Function, writeRow: Function, writeRows: Function, end: Function }}
345
+ */
346
+ function createStreamWriter(outputStream, options = {}) {
347
+ const {
348
+ delimiter = ',',
349
+ eol = '\r\n',
350
+ bom = true,
351
+ sanitizeFormulas: shouldSanitize = true,
352
+ } = options;
353
+
354
+ let columns = options.columns || null;
355
+ let headerWritten = false;
356
+
357
+ if (bom) {
358
+ outputStream.write(UTF8_BOM, 'utf8');
359
+ }
360
+
361
+ function normalizeColumns(cols) {
362
+ if (!cols) return [];
363
+ return cols.map((c) => {
364
+ if (typeof c === 'string') return { header: c, key: c };
365
+ return {
366
+ header: c.header || c.name || c.key,
367
+ key: c.key || c.name || c.header,
368
+ };
369
+ });
370
+ }
371
+
372
+ if (columns) {
373
+ columns = normalizeColumns(columns);
374
+ }
375
+
376
+ function writeHeader(customColumns) {
377
+ if (customColumns) {
378
+ columns = normalizeColumns(customColumns);
379
+ }
380
+ if (!columns || columns.length === 0) return;
381
+ const headerLine =
382
+ columns
383
+ .map((col) =>
384
+ escapeCsvField(col.header, {
385
+ delimiter,
386
+ sanitizeFormulas: false,
387
+ })
388
+ )
389
+ .join(delimiter) + eol;
390
+ outputStream.write(headerLine, 'utf8');
391
+ headerWritten = true;
392
+ }
393
+
394
+ function writeRow(row) {
395
+ if (!row || typeof row !== 'object') return;
396
+
397
+ if (!headerWritten && !columns) {
398
+ columns = Object.keys(row).map((k) => ({ header: k, key: k }));
399
+ writeHeader();
400
+ } else if (!headerWritten && columns) {
401
+ writeHeader();
402
+ }
403
+
404
+ const rowLine =
405
+ columns
406
+ .map((col) => {
407
+ const val = row[col.key];
408
+ return escapeCsvField(val, {
409
+ delimiter,
410
+ sanitizeFormulas: shouldSanitize,
411
+ });
412
+ })
413
+ .join(delimiter) + eol;
414
+
415
+ outputStream.write(rowLine, 'utf8');
416
+ }
417
+
418
+ function writeRows(rows) {
419
+ if (!Array.isArray(rows)) return;
420
+ for (const row of rows) {
421
+ writeRow(row);
422
+ }
423
+ }
424
+
425
+ function end() {
426
+ outputStream.end();
427
+ }
428
+
429
+ return {
430
+ writeHeader,
431
+ writeRow,
432
+ writeRows,
433
+ end,
434
+ stream: outputStream,
435
+ };
436
+ }
437
+
438
+ module.exports = {
439
+ generate,
440
+ parse,
441
+ fromModel,
442
+ createStreamWriter,
443
+ sanitizeFormula,
444
+ escapeCsvField,
445
+ UTF8_BOM,
446
+ CSV_MIME_TYPE,
447
+ };
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CSV Plugin
5
+ * RFC 4180 compliant CSV generation, parsing, ORM export, streaming, and res.csv() integration
6
+ * @module plugins/csv
7
+ */
8
+
9
+ const engine = require('./engine');
10
+ const { createCsvMiddleware, CSV_MIME_TYPE } = require('./middleware');
11
+ const { createCsvServices } = require('./services');
12
+
13
+ /**
14
+ * Standalone CSV Toolkit Singleton
15
+ */
16
+ const csv = {
17
+ ...engine,
18
+ MIME_TYPE: CSV_MIME_TYPE,
19
+ createMiddleware: createCsvMiddleware,
20
+ createServices: createCsvServices,
21
+ };
22
+
23
+ /**
24
+ * CSV Plugin for Webspresso
25
+ * @param {Object} [options]
26
+ * @param {boolean} [options.enabled=true]
27
+ * @param {string} [options.delimiter=','] - Default CSV delimiter
28
+ * @param {boolean} [options.bom=true] - Prepend UTF-8 BOM for Excel compatibility
29
+ * @param {boolean} [options.sanitizeFormulas=true] - Escape Formula Injection attacks
30
+ * @param {Object} [options.db] - Optional database instance for model export services
31
+ * @returns {import('../../index').Plugin}
32
+ */
33
+ function csvPlugin(options = {}) {
34
+ const {
35
+ enabled = true,
36
+ delimiter = ',',
37
+ bom = true,
38
+ sanitizeFormulas = true,
39
+ db: dbOption,
40
+ } = options;
41
+
42
+ return {
43
+ name: 'csv',
44
+ version: '1.0.0',
45
+ description: 'RFC 4180 CSV spreadsheet engine, res.csv() response decorator, and ORM export tools',
46
+ enabled,
47
+
48
+ register(ctx) {
49
+ if (!enabled) return;
50
+
51
+ // 1. Expose on PluginContext & global app
52
+ ctx.csv = csv;
53
+ if (ctx.app) {
54
+ ctx.app.csv = csv;
55
+ }
56
+
57
+ // 2. Register built-in services (csv.generate, csv.parse, csv.exportModel)
58
+ if (ctx.app && ctx.app.serviceRegistry) {
59
+ const db = dbOption ?? ctx.db ?? ctx.app?.get?.('db') ?? null;
60
+ const services = createCsvServices({ db, ...options });
61
+ for (const [name, def] of Object.entries(services)) {
62
+ if (!ctx.app.serviceRegistry.has(name)) {
63
+ ctx.app.serviceRegistry.register(name, def);
64
+ }
65
+ }
66
+ }
67
+
68
+ // 3. Register named middleware & global decorator
69
+ const middleware = createCsvMiddleware({
70
+ delimiter,
71
+ bom,
72
+ sanitizeFormulas,
73
+ ...options,
74
+ });
75
+
76
+ if (ctx.middlewares) {
77
+ ctx.middlewares.csv = middleware;
78
+ }
79
+
80
+ if (ctx.app && typeof ctx.app.use === 'function') {
81
+ ctx.app.use(middleware);
82
+ }
83
+ },
84
+ };
85
+ }
86
+
87
+ module.exports = {
88
+ csvPlugin,
89
+ csv,
90
+ createCsvMiddleware,
91
+ createCsvServices,
92
+ CSV_MIME_TYPE,
93
+ ...engine,
94
+ };
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CSV Express Middleware
5
+ * Decorates res.csv() and req.parseCsv() helpers
6
+ * @module plugins/csv/middleware
7
+ */
8
+
9
+ const { generate, parse, fromModel, CSV_MIME_TYPE } = require('./engine');
10
+
11
+ /**
12
+ * Creates the CSV middleware
13
+ * @param {Object} [globalOptions]
14
+ * @returns {Function}
15
+ */
16
+ function createCsvMiddleware(globalOptions = {}) {
17
+ return (req, res, next) => {
18
+ // 1. Response Helper: res.csv(data, filename, options)
19
+ res.csv = async function (data, filename = 'export.csv', options = {}) {
20
+ try {
21
+ let buffer;
22
+
23
+ if (Buffer.isBuffer(data)) {
24
+ buffer = data;
25
+ } else if (typeof data === 'string') {
26
+ buffer = Buffer.from(data, 'utf8');
27
+ } else if (data && (typeof data.find === 'function' || typeof data.query === 'function') && data.model) {
28
+ // Model repository instance
29
+ buffer = await fromModel(data, options.filter || options.query || {}, {
30
+ ...globalOptions,
31
+ ...options,
32
+ });
33
+ } else if (Array.isArray(data)) {
34
+ buffer = generate({
35
+ ...globalOptions,
36
+ ...options,
37
+ rows: data,
38
+ });
39
+ } else if (data && typeof data === 'object') {
40
+ buffer = generate({
41
+ ...globalOptions,
42
+ ...options,
43
+ ...data,
44
+ });
45
+ } else {
46
+ throw new Error('res.csv() expects an Array of rows, a config object, a Model Repository, a Buffer, or a String');
47
+ }
48
+
49
+ const safeFilename = filename.endsWith('.csv') ? filename : `${filename}.csv`;
50
+
51
+ res.setHeader('Content-Type', CSV_MIME_TYPE);
52
+ res.setHeader(
53
+ 'Content-Disposition',
54
+ `attachment; filename="${encodeURIComponent(safeFilename)}"; filename*=UTF-8''${encodeURIComponent(safeFilename)}`
55
+ );
56
+ res.setHeader('Content-Length', buffer.length);
57
+
58
+ return res.end(buffer);
59
+ } catch (err) {
60
+ return next(err);
61
+ }
62
+ };
63
+
64
+ // 2. Request Helper: req.parseCsv(fileOrBufferOrString, options)
65
+ req.parseCsv = async function (fileOrBufferOrString, options = {}) {
66
+ const target = fileOrBufferOrString || req.file?.buffer || req.rawBody || req.body;
67
+ if (!target) {
68
+ throw new Error('req.parseCsv requires a CSV string, Buffer, or multipart req.file.buffer');
69
+ }
70
+ return parse(target, options);
71
+ };
72
+
73
+ next();
74
+ };
75
+ }
76
+
77
+ module.exports = {
78
+ createCsvMiddleware,
79
+ CSV_MIME_TYPE,
80
+ };