webspresso 0.0.95 → 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.
- package/.agents/ADMIN.md +225 -0
- package/.agents/AGENTS.md +72 -0
- package/.agents/ARCHITECTURE.md +119 -0
- package/.agents/AUTH.md +103 -0
- package/.agents/CLI.md +115 -0
- package/.agents/COMPRESSION.md +59 -0
- package/.agents/CONVENTIONS.md +46 -0
- package/.agents/EMAIL.md +141 -0
- package/.agents/ERRORS.md +201 -0
- package/.agents/MCP.md +103 -0
- package/.agents/ORM.md +230 -0
- package/.agents/PLUGINS.md +419 -0
- package/.agents/QUEUE.md +113 -0
- package/.agents/REALTIME.md +213 -0
- package/.agents/ROUTING.md +292 -0
- package/.agents/SERVICES.md +341 -0
- package/.agents/SHUTDOWN.md +67 -0
- package/.agents/STREAMING.md +73 -0
- package/.agents/WORKFLOW.md +27 -0
- package/.agents/internal/DOCUMENTATION_REVIEW.md +485 -0
- package/.agents/internal/TECHNICAL_REVIEW.md +428 -0
- package/.cursorrules +14 -0
- package/AGENTS.md +175 -0
- package/CLAUDE.md +54 -0
- package/README.md +18 -14
- package/bin/commands/agents-init.js +169 -0
- package/bin/commands/api.js +114 -48
- package/bin/commands/doctor.js +35 -0
- package/bin/commands/new.js +13 -2
- package/bin/commands/page.js +75 -89
- package/bin/commands/service.js +77 -0
- package/bin/webspresso.js +2 -2
- package/core/compileSchema.js +1 -1
- package/core/errors/middleware.js +22 -2
- package/core/errors/normalize.js +29 -2
- package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
- package/index.d.ts +306 -0
- package/index.js +35 -1
- package/package.json +5 -3
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
- package/plugins/csrf/index.js +3 -2
- package/plugins/csv/engine.js +447 -0
- package/plugins/csv/index.js +94 -0
- package/plugins/csv/middleware.js +80 -0
- package/plugins/csv/services.js +138 -0
- package/plugins/file-manager/admin-component.js +613 -0
- package/plugins/file-manager/api-handlers.js +261 -0
- package/plugins/file-manager/core.js +601 -0
- package/plugins/file-manager/index.js +139 -0
- package/plugins/file-manager/security.js +207 -0
- package/plugins/index.js +8 -0
- package/plugins/xlsx/engine.js +397 -0
- package/plugins/xlsx/index.js +91 -0
- package/plugins/xlsx/middleware.js +87 -0
- package/plugins/xlsx/services.js +118 -0
- package/src/api/api-loader.js +233 -0
- package/src/api/define-api.js +35 -0
- package/src/discovery/file-route-parser.js +201 -0
- package/src/discovery/scan-routes.js +265 -0
- package/src/file-router.js +6 -0
- package/src/modules/define-module.js +99 -0
- package/src/modules/module-discovery.js +117 -0
- package/src/pages/define-page.js +36 -0
- package/src/pages/page-loader.js +214 -0
- package/src/routing/mount-discovered-routes.js +78 -0
- package/src/routing/route-table.js +191 -0
- package/src/server.js +112 -15
- package/src/services/builtins/file-manager.js +178 -0
- package/src/services/index.js +2 -0
- package/bin/commands/skill.js +0 -187
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
- package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
- 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
|
+
};
|