te.js 2.1.0 → 2.1.2
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/README.md +197 -196
- package/auto-docs/analysis/handler-analyzer.js +58 -58
- package/auto-docs/analysis/source-resolver.js +101 -101
- package/auto-docs/constants.js +37 -37
- package/auto-docs/docs-llm/index.js +7 -7
- package/auto-docs/docs-llm/prompts.js +222 -222
- package/auto-docs/docs-llm/provider.js +132 -132
- package/auto-docs/index.js +146 -146
- package/auto-docs/openapi/endpoint-processor.js +277 -277
- package/auto-docs/openapi/generator.js +107 -107
- package/auto-docs/openapi/level3.js +131 -131
- package/auto-docs/openapi/spec-builders.js +244 -244
- package/auto-docs/ui/docs-ui.js +186 -186
- package/auto-docs/utils/logger.js +17 -17
- package/auto-docs/utils/strip-usage.js +10 -10
- package/cli/docs-command.js +315 -315
- package/cli/fly-command.js +71 -71
- package/cli/index.js +56 -56
- package/cors/index.js +71 -0
- package/database/index.js +165 -165
- package/database/mongodb.js +146 -146
- package/database/redis.js +201 -201
- package/docs/README.md +36 -36
- package/docs/ammo.md +362 -362
- package/docs/api-reference.md +490 -490
- package/docs/auto-docs.md +216 -216
- package/docs/cli.md +152 -152
- package/docs/configuration.md +275 -275
- package/docs/database.md +390 -390
- package/docs/error-handling.md +438 -438
- package/docs/file-uploads.md +333 -333
- package/docs/getting-started.md +214 -214
- package/docs/middleware.md +355 -355
- package/docs/rate-limiting.md +393 -393
- package/docs/routing.md +302 -302
- package/lib/llm/client.js +73 -0
- package/lib/llm/index.js +7 -0
- package/lib/llm/parse.js +89 -0
- package/package.json +64 -62
- package/rate-limit/algorithms/fixed-window.js +141 -141
- package/rate-limit/algorithms/sliding-window.js +147 -147
- package/rate-limit/algorithms/token-bucket.js +115 -115
- package/rate-limit/base.js +165 -165
- package/rate-limit/index.js +147 -147
- package/rate-limit/storage/base.js +104 -104
- package/rate-limit/storage/memory.js +101 -101
- package/rate-limit/storage/redis.js +88 -88
- package/server/ammo/body-parser.js +220 -220
- package/server/ammo/dispatch-helper.js +103 -103
- package/server/ammo/enhancer.js +57 -57
- package/server/ammo.js +454 -415
- package/server/endpoint.js +97 -74
- package/server/error.js +9 -9
- package/server/errors/code-context.js +125 -125
- package/server/errors/llm-error-service.js +140 -140
- package/server/files/helper.js +33 -33
- package/server/files/uploader.js +143 -143
- package/server/handler.js +158 -119
- package/server/target.js +185 -175
- package/server/targets/middleware-validator.js +22 -22
- package/server/targets/path-validator.js +21 -21
- package/server/targets/registry.js +160 -160
- package/server/targets/shoot-validator.js +21 -21
- package/te.js +428 -402
- package/utils/auto-register.js +17 -17
- package/utils/configuration.js +64 -64
- package/utils/errors-llm-config.js +84 -84
- package/utils/request-logger.js +43 -43
- package/utils/status-codes.js +82 -82
- package/utils/tejas-entrypoint-html.js +18 -18
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LLM-based error inference: given code context (surrounding + upstream/downstream),
|
|
3
|
-
* returns statusCode and message (and optionally devInsight in non-production).
|
|
4
|
-
* Uses shared lib/llm with errors.llm config. Developers do not pass an error object;
|
|
5
|
-
* the LLM infers from the code where ammo.throw() was called.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { createProvider } from '../../lib/llm/index.js';
|
|
9
|
-
import { extractJSON } from '../../lib/llm/parse.js';
|
|
10
|
-
import { getErrorsLlmConfig } from '../../utils/errors-llm-config.js';
|
|
11
|
-
|
|
12
|
-
const DEFAULT_STATUS = 500;
|
|
13
|
-
const DEFAULT_MESSAGE = 'Internal Server Error';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Build prompt text from code context (and optional error) for the LLM.
|
|
17
|
-
* @param {object} context
|
|
18
|
-
* @param {{ snippets: Array<{ file: string, line: number, snippet: string }> }} context.codeContext - Source snippets with line numbers (first = throw site, rest = upstream).
|
|
19
|
-
* @param {string} [context.method] - HTTP method.
|
|
20
|
-
* @param {string} [context.path] - Request path.
|
|
21
|
-
* @param {boolean} [context.includeDevInsight] - If true, ask for devInsight.
|
|
22
|
-
* @param {'endUser'|'developer'} [context.messageType] - Message tone.
|
|
23
|
-
* @param {string|Error|undefined} [context.error] - Optional error if one was passed (secondary signal).
|
|
24
|
-
* @returns {string}
|
|
25
|
-
*/
|
|
26
|
-
function buildPrompt(context) {
|
|
27
|
-
const { codeContext, method, path, includeDevInsight, messageType, error } = context;
|
|
28
|
-
const forDeveloper = messageType === 'developer';
|
|
29
|
-
|
|
30
|
-
const requestPart = [method, path].filter(Boolean).length
|
|
31
|
-
? `Request: ${[method, path].filter(Boolean).join(' ')}`
|
|
32
|
-
: '';
|
|
33
|
-
|
|
34
|
-
let codePart = 'No code context was captured.';
|
|
35
|
-
if (codeContext?.snippets?.length) {
|
|
36
|
-
codePart = codeContext.snippets
|
|
37
|
-
.map((s, i) => {
|
|
38
|
-
const label = i === 0 ? 'Call site (where ammo.throw() was invoked)' : `Upstream caller ${i}`;
|
|
39
|
-
return `--- ${label}: ${s.file} (line ${s.line}) ---\n${s.snippet}`;
|
|
40
|
-
})
|
|
41
|
-
.join('\n\n');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
let errorPart = '';
|
|
45
|
-
if (error !== undefined && error !== null) {
|
|
46
|
-
if (error instanceof Error) {
|
|
47
|
-
errorPart = `\nOptional error message (may be empty): ${error.message}`;
|
|
48
|
-
} else {
|
|
49
|
-
errorPart = `\nOptional error/message: ${String(error)}`;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const devPart = includeDevInsight
|
|
54
|
-
? '\nAlso provide a short "devInsight" string (one or two sentences) for the developer: (a) Is this likely a bug in the code or an environment/setup issue? (b) If the developer can fix it, suggest the fix. Be concise.'
|
|
55
|
-
: '';
|
|
56
|
-
|
|
57
|
-
const messageInstruction = forDeveloper
|
|
58
|
-
? '- "message": string (short message for developers: may include technical detail, error type, or cause; do not include raw stack traces)'
|
|
59
|
-
: '- "message": string (short, end-user-facing message: safe for clients; do not expose stack traces, internal details, or technical jargon)';
|
|
60
|
-
|
|
61
|
-
return `You are helping map an application error to an HTTP response. The developer called ammo.throw() (or an error was thrown and caught) at the call site below. Use the surrounding code with line numbers and all upstream/downstream context to infer what went wrong and choose an appropriate HTTP status and message.
|
|
62
|
-
|
|
63
|
-
Consider:
|
|
64
|
-
- The code BEFORE the throw (upstream in the same function and in callers) — what led to this point.
|
|
65
|
-
- The code AFTER the throw line (downstream) — what would have run next; this shows intent and expected flow.
|
|
66
|
-
- The first snippet is the call site (line marked with →); later snippets are upstream callers.
|
|
67
|
-
|
|
68
|
-
${requestPart ? requestPart + '\n\n' : ''}Code context (with line numbers; → marks the throw line):
|
|
69
|
-
|
|
70
|
-
${codePart}${errorPart}
|
|
71
|
-
${devPart ? '\n' + devPart : ''}
|
|
72
|
-
|
|
73
|
-
Respond with only valid JSON, no markdown or explanation. Use this shape:
|
|
74
|
-
- "statusCode": number (HTTP status, typically 4xx or 5xx; use 500 for generic/server errors)
|
|
75
|
-
${messageInstruction}
|
|
76
|
-
${includeDevInsight ? '- "devInsight": string (brief note for the developer only)' : ''}
|
|
77
|
-
|
|
78
|
-
JSON:`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Infer HTTP statusCode and message (and optionally devInsight) from code context using the LLM.
|
|
83
|
-
* Uses errors.llm config (getErrorsLlmConfig). Call only when errors.llm.enabled is true and config is valid.
|
|
84
|
-
* The primary input is codeContext (surrounding + upstream/downstream snippets); error is optional.
|
|
85
|
-
*
|
|
86
|
-
* @param {object} context - Context for the prompt.
|
|
87
|
-
* @param {{ snippets: Array<{ file: string, line: number, snippet: string }> }} context.codeContext - Source snippets with line numbers (from captureCodeContext).
|
|
88
|
-
* @param {string} [context.method] - HTTP method.
|
|
89
|
-
* @param {string} [context.path] - Request path.
|
|
90
|
-
* @param {boolean} [context.includeDevInsight] - In non-production, dev insight is included by default; set to false to disable.
|
|
91
|
-
* @param {'endUser'|'developer'} [context.messageType] - Override config: 'endUser' or 'developer'. Default from errors.llm.messageType.
|
|
92
|
-
* @param {string|Error|undefined} [context.error] - Optional error if the caller passed one (secondary signal).
|
|
93
|
-
* @returns {Promise<{ statusCode: number, message: string, devInsight?: string }>}
|
|
94
|
-
*/
|
|
95
|
-
export async function inferErrorFromContext(context) {
|
|
96
|
-
const config = getErrorsLlmConfig();
|
|
97
|
-
const { baseURL, apiKey, model, messageType: configMessageType } = config;
|
|
98
|
-
const provider = createProvider({ baseURL, apiKey, model });
|
|
99
|
-
|
|
100
|
-
const isProduction = process.env.NODE_ENV === 'production';
|
|
101
|
-
const includeDevInsight = !isProduction && context.includeDevInsight !== false;
|
|
102
|
-
const messageType = context.messageType ?? configMessageType;
|
|
103
|
-
|
|
104
|
-
const prompt = buildPrompt({
|
|
105
|
-
codeContext: context.codeContext,
|
|
106
|
-
method: context.method,
|
|
107
|
-
path: context.path,
|
|
108
|
-
includeDevInsight,
|
|
109
|
-
messageType,
|
|
110
|
-
error: context.error,
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const { content } = await provider.analyze(prompt);
|
|
114
|
-
const parsed = extractJSON(content);
|
|
115
|
-
|
|
116
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
117
|
-
return {
|
|
118
|
-
statusCode: DEFAULT_STATUS,
|
|
119
|
-
message: DEFAULT_MESSAGE,
|
|
120
|
-
...(includeDevInsight && { devInsight: 'Could not parse LLM response.' }),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
let statusCode = Number(parsed.statusCode);
|
|
125
|
-
if (Number.isNaN(statusCode) || statusCode < 100 || statusCode > 599) {
|
|
126
|
-
statusCode = DEFAULT_STATUS;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const message =
|
|
130
|
-
typeof parsed.message === 'string' && parsed.message.trim()
|
|
131
|
-
? parsed.message.trim()
|
|
132
|
-
: DEFAULT_MESSAGE;
|
|
133
|
-
|
|
134
|
-
const result = { statusCode, message };
|
|
135
|
-
if (includeDevInsight && typeof parsed.devInsight === 'string' && parsed.devInsight.trim()) {
|
|
136
|
-
result.devInsight = parsed.devInsight.trim();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return result;
|
|
140
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* LLM-based error inference: given code context (surrounding + upstream/downstream),
|
|
3
|
+
* returns statusCode and message (and optionally devInsight in non-production).
|
|
4
|
+
* Uses shared lib/llm with errors.llm config. Developers do not pass an error object;
|
|
5
|
+
* the LLM infers from the code where ammo.throw() was called.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createProvider } from '../../lib/llm/index.js';
|
|
9
|
+
import { extractJSON } from '../../lib/llm/parse.js';
|
|
10
|
+
import { getErrorsLlmConfig } from '../../utils/errors-llm-config.js';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_STATUS = 500;
|
|
13
|
+
const DEFAULT_MESSAGE = 'Internal Server Error';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build prompt text from code context (and optional error) for the LLM.
|
|
17
|
+
* @param {object} context
|
|
18
|
+
* @param {{ snippets: Array<{ file: string, line: number, snippet: string }> }} context.codeContext - Source snippets with line numbers (first = throw site, rest = upstream).
|
|
19
|
+
* @param {string} [context.method] - HTTP method.
|
|
20
|
+
* @param {string} [context.path] - Request path.
|
|
21
|
+
* @param {boolean} [context.includeDevInsight] - If true, ask for devInsight.
|
|
22
|
+
* @param {'endUser'|'developer'} [context.messageType] - Message tone.
|
|
23
|
+
* @param {string|Error|undefined} [context.error] - Optional error if one was passed (secondary signal).
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function buildPrompt(context) {
|
|
27
|
+
const { codeContext, method, path, includeDevInsight, messageType, error } = context;
|
|
28
|
+
const forDeveloper = messageType === 'developer';
|
|
29
|
+
|
|
30
|
+
const requestPart = [method, path].filter(Boolean).length
|
|
31
|
+
? `Request: ${[method, path].filter(Boolean).join(' ')}`
|
|
32
|
+
: '';
|
|
33
|
+
|
|
34
|
+
let codePart = 'No code context was captured.';
|
|
35
|
+
if (codeContext?.snippets?.length) {
|
|
36
|
+
codePart = codeContext.snippets
|
|
37
|
+
.map((s, i) => {
|
|
38
|
+
const label = i === 0 ? 'Call site (where ammo.throw() was invoked)' : `Upstream caller ${i}`;
|
|
39
|
+
return `--- ${label}: ${s.file} (line ${s.line}) ---\n${s.snippet}`;
|
|
40
|
+
})
|
|
41
|
+
.join('\n\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let errorPart = '';
|
|
45
|
+
if (error !== undefined && error !== null) {
|
|
46
|
+
if (error instanceof Error) {
|
|
47
|
+
errorPart = `\nOptional error message (may be empty): ${error.message}`;
|
|
48
|
+
} else {
|
|
49
|
+
errorPart = `\nOptional error/message: ${String(error)}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const devPart = includeDevInsight
|
|
54
|
+
? '\nAlso provide a short "devInsight" string (one or two sentences) for the developer: (a) Is this likely a bug in the code or an environment/setup issue? (b) If the developer can fix it, suggest the fix. Be concise.'
|
|
55
|
+
: '';
|
|
56
|
+
|
|
57
|
+
const messageInstruction = forDeveloper
|
|
58
|
+
? '- "message": string (short message for developers: may include technical detail, error type, or cause; do not include raw stack traces)'
|
|
59
|
+
: '- "message": string (short, end-user-facing message: safe for clients; do not expose stack traces, internal details, or technical jargon)';
|
|
60
|
+
|
|
61
|
+
return `You are helping map an application error to an HTTP response. The developer called ammo.throw() (or an error was thrown and caught) at the call site below. Use the surrounding code with line numbers and all upstream/downstream context to infer what went wrong and choose an appropriate HTTP status and message.
|
|
62
|
+
|
|
63
|
+
Consider:
|
|
64
|
+
- The code BEFORE the throw (upstream in the same function and in callers) — what led to this point.
|
|
65
|
+
- The code AFTER the throw line (downstream) — what would have run next; this shows intent and expected flow.
|
|
66
|
+
- The first snippet is the call site (line marked with →); later snippets are upstream callers.
|
|
67
|
+
|
|
68
|
+
${requestPart ? requestPart + '\n\n' : ''}Code context (with line numbers; → marks the throw line):
|
|
69
|
+
|
|
70
|
+
${codePart}${errorPart}
|
|
71
|
+
${devPart ? '\n' + devPart : ''}
|
|
72
|
+
|
|
73
|
+
Respond with only valid JSON, no markdown or explanation. Use this shape:
|
|
74
|
+
- "statusCode": number (HTTP status, typically 4xx or 5xx; use 500 for generic/server errors)
|
|
75
|
+
${messageInstruction}
|
|
76
|
+
${includeDevInsight ? '- "devInsight": string (brief note for the developer only)' : ''}
|
|
77
|
+
|
|
78
|
+
JSON:`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Infer HTTP statusCode and message (and optionally devInsight) from code context using the LLM.
|
|
83
|
+
* Uses errors.llm config (getErrorsLlmConfig). Call only when errors.llm.enabled is true and config is valid.
|
|
84
|
+
* The primary input is codeContext (surrounding + upstream/downstream snippets); error is optional.
|
|
85
|
+
*
|
|
86
|
+
* @param {object} context - Context for the prompt.
|
|
87
|
+
* @param {{ snippets: Array<{ file: string, line: number, snippet: string }> }} context.codeContext - Source snippets with line numbers (from captureCodeContext).
|
|
88
|
+
* @param {string} [context.method] - HTTP method.
|
|
89
|
+
* @param {string} [context.path] - Request path.
|
|
90
|
+
* @param {boolean} [context.includeDevInsight] - In non-production, dev insight is included by default; set to false to disable.
|
|
91
|
+
* @param {'endUser'|'developer'} [context.messageType] - Override config: 'endUser' or 'developer'. Default from errors.llm.messageType.
|
|
92
|
+
* @param {string|Error|undefined} [context.error] - Optional error if the caller passed one (secondary signal).
|
|
93
|
+
* @returns {Promise<{ statusCode: number, message: string, devInsight?: string }>}
|
|
94
|
+
*/
|
|
95
|
+
export async function inferErrorFromContext(context) {
|
|
96
|
+
const config = getErrorsLlmConfig();
|
|
97
|
+
const { baseURL, apiKey, model, messageType: configMessageType } = config;
|
|
98
|
+
const provider = createProvider({ baseURL, apiKey, model });
|
|
99
|
+
|
|
100
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
101
|
+
const includeDevInsight = !isProduction && context.includeDevInsight !== false;
|
|
102
|
+
const messageType = context.messageType ?? configMessageType;
|
|
103
|
+
|
|
104
|
+
const prompt = buildPrompt({
|
|
105
|
+
codeContext: context.codeContext,
|
|
106
|
+
method: context.method,
|
|
107
|
+
path: context.path,
|
|
108
|
+
includeDevInsight,
|
|
109
|
+
messageType,
|
|
110
|
+
error: context.error,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const { content } = await provider.analyze(prompt);
|
|
114
|
+
const parsed = extractJSON(content);
|
|
115
|
+
|
|
116
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
117
|
+
return {
|
|
118
|
+
statusCode: DEFAULT_STATUS,
|
|
119
|
+
message: DEFAULT_MESSAGE,
|
|
120
|
+
...(includeDevInsight && { devInsight: 'Could not parse LLM response.' }),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let statusCode = Number(parsed.statusCode);
|
|
125
|
+
if (Number.isNaN(statusCode) || statusCode < 100 || statusCode > 599) {
|
|
126
|
+
statusCode = DEFAULT_STATUS;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const message =
|
|
130
|
+
typeof parsed.message === 'string' && parsed.message.trim()
|
|
131
|
+
? parsed.message.trim()
|
|
132
|
+
: DEFAULT_MESSAGE;
|
|
133
|
+
|
|
134
|
+
const result = { statusCode, message };
|
|
135
|
+
if (includeDevInsight && typeof parsed.devInsight === 'string' && parsed.devInsight.trim()) {
|
|
136
|
+
result.devInsight = parsed.devInsight.trim();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result;
|
|
140
|
+
}
|
package/server/files/helper.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import mime from 'mime';
|
|
2
|
-
|
|
3
|
-
const paths = (destination, filename) => {
|
|
4
|
-
const dir = `${process.cwd()}\\${destination}`;
|
|
5
|
-
const path = `${dir}\\${filename}`;
|
|
6
|
-
|
|
7
|
-
const absolute = path;
|
|
8
|
-
const relative = path.replace(process.cwd(), '');
|
|
9
|
-
|
|
10
|
-
return { dir, absolute, relative };
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const extAndType = (obj) => {
|
|
14
|
-
const contentType = obj.headers['content-type'];
|
|
15
|
-
const ext = mime.getExtension(contentType);
|
|
16
|
-
const type = mime.getType(ext);
|
|
17
|
-
return {
|
|
18
|
-
ext,
|
|
19
|
-
type,
|
|
20
|
-
};
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const extract = (contentDisposition, key) => {
|
|
24
|
-
if (!contentDisposition) {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const parts = contentDisposition.split(';').map((part) => part.trim());
|
|
29
|
-
const part = parts.find((part) => part.startsWith(key));
|
|
30
|
-
return part ? part?.split('=')[1]?.trim()?.replace(/"/g, '') : undefined;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export { extAndType, extract, paths };
|
|
1
|
+
import mime from 'mime';
|
|
2
|
+
|
|
3
|
+
const paths = (destination, filename) => {
|
|
4
|
+
const dir = `${process.cwd()}\\${destination}`;
|
|
5
|
+
const path = `${dir}\\${filename}`;
|
|
6
|
+
|
|
7
|
+
const absolute = path;
|
|
8
|
+
const relative = path.replace(process.cwd(), '');
|
|
9
|
+
|
|
10
|
+
return { dir, absolute, relative };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const extAndType = (obj) => {
|
|
14
|
+
const contentType = obj.headers['content-type'];
|
|
15
|
+
const ext = mime.getExtension(contentType);
|
|
16
|
+
const type = mime.getType(ext);
|
|
17
|
+
return {
|
|
18
|
+
ext,
|
|
19
|
+
type,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const extract = (contentDisposition, key) => {
|
|
24
|
+
if (!contentDisposition) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parts = contentDisposition.split(';').map((part) => part.trim());
|
|
29
|
+
const part = parts.find((part) => part.startsWith(key));
|
|
30
|
+
return part ? part?.split('=')[1]?.trim()?.replace(/"/g, '') : undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export { extAndType, extract, paths };
|
package/server/files/uploader.js
CHANGED
|
@@ -1,143 +1,143 @@
|
|
|
1
|
-
import { filesize } from 'filesize';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import TejError from './../error.js';
|
|
4
|
-
import { extAndType, extract, paths } from './helper.js';
|
|
5
|
-
|
|
6
|
-
class TejFileUploader {
|
|
7
|
-
/*
|
|
8
|
-
* @param {Object} options
|
|
9
|
-
* @param {string} options.destination - Destination to upload file to
|
|
10
|
-
* @param {string} options.name - Name of the file
|
|
11
|
-
* @param {number} options.maxFileSize - Maximum file size in bytes
|
|
12
|
-
*/
|
|
13
|
-
constructor(options = {}) {
|
|
14
|
-
this.destination = options.destination;
|
|
15
|
-
this.name = options.name;
|
|
16
|
-
this.maxFileSize = options.maxFileSize;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
file() {
|
|
20
|
-
const keys = [...arguments];
|
|
21
|
-
return async (ammo, next) => {
|
|
22
|
-
if (!ammo.headers['content-type'].startsWith('multipart/form-data'))
|
|
23
|
-
return next();
|
|
24
|
-
|
|
25
|
-
const payload = ammo.payload;
|
|
26
|
-
const updatedPayload = {};
|
|
27
|
-
|
|
28
|
-
for (const part in payload) {
|
|
29
|
-
const obj = payload[part];
|
|
30
|
-
const contentDisposition = obj.headers['content-disposition'];
|
|
31
|
-
|
|
32
|
-
const { ext, type } = extAndType(obj);
|
|
33
|
-
|
|
34
|
-
const key = extract(contentDisposition, 'name');
|
|
35
|
-
if (!key) continue;
|
|
36
|
-
|
|
37
|
-
if (!ext || ext === 'txt') {
|
|
38
|
-
updatedPayload[key] = obj.value;
|
|
39
|
-
} else {
|
|
40
|
-
if (!keys.includes(key)) continue;
|
|
41
|
-
|
|
42
|
-
const filename = extract(contentDisposition, 'filename');
|
|
43
|
-
if (!filename) continue;
|
|
44
|
-
|
|
45
|
-
const { dir, absolute, relative } = paths(this.destination, filename);
|
|
46
|
-
const size = filesize(obj.value.length,
|
|
47
|
-
{ output: 'object', round: 0 });
|
|
48
|
-
const maxSize = filesize(this.maxFileSize,
|
|
49
|
-
{ output: 'object', round: 0 });
|
|
50
|
-
if (this.maxFileSize && obj.value.length > this.maxFileSize)
|
|
51
|
-
throw new TejError(413,
|
|
52
|
-
`File size exceeds ${maxSize.value} ${maxSize.symbol}`);
|
|
53
|
-
|
|
54
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
55
|
-
fs.writeFileSync(absolute, obj.value, 'binary');
|
|
56
|
-
|
|
57
|
-
updatedPayload[key] = {
|
|
58
|
-
filename,
|
|
59
|
-
extension: ext,
|
|
60
|
-
path: {
|
|
61
|
-
absolute: absolute,
|
|
62
|
-
relative: relative
|
|
63
|
-
},
|
|
64
|
-
mimetype: type,
|
|
65
|
-
size
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
ammo.payload = updatedPayload;
|
|
71
|
-
next();
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
files() {
|
|
76
|
-
const keys = [...arguments];
|
|
77
|
-
return async (ammo, next) => {
|
|
78
|
-
if (!ammo.headers['content-type'].startsWith('multipart/form-data'))
|
|
79
|
-
return next();
|
|
80
|
-
|
|
81
|
-
const payload = ammo.payload;
|
|
82
|
-
const updatedPayload = {};
|
|
83
|
-
const files = [];
|
|
84
|
-
|
|
85
|
-
for (const part in payload) {
|
|
86
|
-
const obj = payload[part];
|
|
87
|
-
const contentDisposition = obj.headers['content-disposition'];
|
|
88
|
-
|
|
89
|
-
const { ext, type } = extAndType(obj);
|
|
90
|
-
if (!ext) continue;
|
|
91
|
-
|
|
92
|
-
const key = extract(contentDisposition, 'name');
|
|
93
|
-
if (ext === 'txt') {
|
|
94
|
-
updatedPayload[key] = obj.value;
|
|
95
|
-
} else {
|
|
96
|
-
if (!keys.includes(key)) continue;
|
|
97
|
-
|
|
98
|
-
const filename = extract(contentDisposition, 'filename');
|
|
99
|
-
if (!filename) continue;
|
|
100
|
-
|
|
101
|
-
const { dir, absolute, relative } = paths(this.destination, filename);
|
|
102
|
-
const size = filesize(obj.value.length,
|
|
103
|
-
{ output: 'object', round: 0 });
|
|
104
|
-
const maxSize = filesize(this.maxFileSize,
|
|
105
|
-
{ output: 'object', round: 0 });
|
|
106
|
-
if (this.maxFileSize && obj.value.length > this.maxFileSize) {
|
|
107
|
-
throw new TejError(413,
|
|
108
|
-
`File size exceeds ${maxSize.value} ${maxSize.symbol}`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
112
|
-
fs.writeFileSync(absolute, obj.value, 'binary');
|
|
113
|
-
|
|
114
|
-
files.push({
|
|
115
|
-
key,
|
|
116
|
-
filename,
|
|
117
|
-
path: {
|
|
118
|
-
absolute: absolute,
|
|
119
|
-
relative: relative
|
|
120
|
-
},
|
|
121
|
-
mimetype: type,
|
|
122
|
-
size
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const groupedFilesByKey = files.reduce((acc, file) => {
|
|
128
|
-
if (!acc[file.key]) acc[file.key] = [];
|
|
129
|
-
acc[file.key].push(file);
|
|
130
|
-
return acc;
|
|
131
|
-
}, {});
|
|
132
|
-
|
|
133
|
-
for (const key in groupedFilesByKey) {
|
|
134
|
-
updatedPayload[key] = groupedFilesByKey[key];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
ammo.payload = updatedPayload;
|
|
138
|
-
next();
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export default TejFileUploader;
|
|
1
|
+
import { filesize } from 'filesize';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import TejError from './../error.js';
|
|
4
|
+
import { extAndType, extract, paths } from './helper.js';
|
|
5
|
+
|
|
6
|
+
class TejFileUploader {
|
|
7
|
+
/*
|
|
8
|
+
* @param {Object} options
|
|
9
|
+
* @param {string} options.destination - Destination to upload file to
|
|
10
|
+
* @param {string} options.name - Name of the file
|
|
11
|
+
* @param {number} options.maxFileSize - Maximum file size in bytes
|
|
12
|
+
*/
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.destination = options.destination;
|
|
15
|
+
this.name = options.name;
|
|
16
|
+
this.maxFileSize = options.maxFileSize;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
file() {
|
|
20
|
+
const keys = [...arguments];
|
|
21
|
+
return async (ammo, next) => {
|
|
22
|
+
if (!ammo.headers['content-type'].startsWith('multipart/form-data'))
|
|
23
|
+
return next();
|
|
24
|
+
|
|
25
|
+
const payload = ammo.payload;
|
|
26
|
+
const updatedPayload = {};
|
|
27
|
+
|
|
28
|
+
for (const part in payload) {
|
|
29
|
+
const obj = payload[part];
|
|
30
|
+
const contentDisposition = obj.headers['content-disposition'];
|
|
31
|
+
|
|
32
|
+
const { ext, type } = extAndType(obj);
|
|
33
|
+
|
|
34
|
+
const key = extract(contentDisposition, 'name');
|
|
35
|
+
if (!key) continue;
|
|
36
|
+
|
|
37
|
+
if (!ext || ext === 'txt') {
|
|
38
|
+
updatedPayload[key] = obj.value;
|
|
39
|
+
} else {
|
|
40
|
+
if (!keys.includes(key)) continue;
|
|
41
|
+
|
|
42
|
+
const filename = extract(contentDisposition, 'filename');
|
|
43
|
+
if (!filename) continue;
|
|
44
|
+
|
|
45
|
+
const { dir, absolute, relative } = paths(this.destination, filename);
|
|
46
|
+
const size = filesize(obj.value.length,
|
|
47
|
+
{ output: 'object', round: 0 });
|
|
48
|
+
const maxSize = filesize(this.maxFileSize,
|
|
49
|
+
{ output: 'object', round: 0 });
|
|
50
|
+
if (this.maxFileSize && obj.value.length > this.maxFileSize)
|
|
51
|
+
throw new TejError(413,
|
|
52
|
+
`File size exceeds ${maxSize.value} ${maxSize.symbol}`);
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
55
|
+
fs.writeFileSync(absolute, obj.value, 'binary');
|
|
56
|
+
|
|
57
|
+
updatedPayload[key] = {
|
|
58
|
+
filename,
|
|
59
|
+
extension: ext,
|
|
60
|
+
path: {
|
|
61
|
+
absolute: absolute,
|
|
62
|
+
relative: relative
|
|
63
|
+
},
|
|
64
|
+
mimetype: type,
|
|
65
|
+
size
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
ammo.payload = updatedPayload;
|
|
71
|
+
next();
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
files() {
|
|
76
|
+
const keys = [...arguments];
|
|
77
|
+
return async (ammo, next) => {
|
|
78
|
+
if (!ammo.headers['content-type'].startsWith('multipart/form-data'))
|
|
79
|
+
return next();
|
|
80
|
+
|
|
81
|
+
const payload = ammo.payload;
|
|
82
|
+
const updatedPayload = {};
|
|
83
|
+
const files = [];
|
|
84
|
+
|
|
85
|
+
for (const part in payload) {
|
|
86
|
+
const obj = payload[part];
|
|
87
|
+
const contentDisposition = obj.headers['content-disposition'];
|
|
88
|
+
|
|
89
|
+
const { ext, type } = extAndType(obj);
|
|
90
|
+
if (!ext) continue;
|
|
91
|
+
|
|
92
|
+
const key = extract(contentDisposition, 'name');
|
|
93
|
+
if (ext === 'txt') {
|
|
94
|
+
updatedPayload[key] = obj.value;
|
|
95
|
+
} else {
|
|
96
|
+
if (!keys.includes(key)) continue;
|
|
97
|
+
|
|
98
|
+
const filename = extract(contentDisposition, 'filename');
|
|
99
|
+
if (!filename) continue;
|
|
100
|
+
|
|
101
|
+
const { dir, absolute, relative } = paths(this.destination, filename);
|
|
102
|
+
const size = filesize(obj.value.length,
|
|
103
|
+
{ output: 'object', round: 0 });
|
|
104
|
+
const maxSize = filesize(this.maxFileSize,
|
|
105
|
+
{ output: 'object', round: 0 });
|
|
106
|
+
if (this.maxFileSize && obj.value.length > this.maxFileSize) {
|
|
107
|
+
throw new TejError(413,
|
|
108
|
+
`File size exceeds ${maxSize.value} ${maxSize.symbol}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
112
|
+
fs.writeFileSync(absolute, obj.value, 'binary');
|
|
113
|
+
|
|
114
|
+
files.push({
|
|
115
|
+
key,
|
|
116
|
+
filename,
|
|
117
|
+
path: {
|
|
118
|
+
absolute: absolute,
|
|
119
|
+
relative: relative
|
|
120
|
+
},
|
|
121
|
+
mimetype: type,
|
|
122
|
+
size
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const groupedFilesByKey = files.reduce((acc, file) => {
|
|
128
|
+
if (!acc[file.key]) acc[file.key] = [];
|
|
129
|
+
acc[file.key].push(file);
|
|
130
|
+
return acc;
|
|
131
|
+
}, {});
|
|
132
|
+
|
|
133
|
+
for (const key in groupedFilesByKey) {
|
|
134
|
+
updatedPayload[key] = groupedFilesByKey[key];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
ammo.payload = updatedPayload;
|
|
138
|
+
next();
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export default TejFileUploader;
|