Skip to content

Commit e31cfa6

Browse files
committed
feat: add support of multipart/form-data for the adminforth api wrapper
1 parent 6111f40 commit e31cfa6

5 files changed

Lines changed: 176 additions & 5 deletions

File tree

adminforth/package-lock.json

Lines changed: 86 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

adminforth/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"jsonwebtoken": "^9.0.2",
113113
"listr2": "^8.2.5",
114114
"mongodb": "6.6",
115+
"multer": "^2.1.1",
115116
"mysql2": "^3.14.2",
116117
"node-fetch": "^3.3.2",
117118
"pg": "^8.11.5",

adminforth/pnpm-lock.yaml

Lines changed: 54 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

adminforth/servers/express.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { randomUUID } from 'crypto';
1515
import { listify } from '../modules/utils.js';
1616
import { afLogger } from '../modules/logger.js';
1717
import * as z from 'zod';
18+
import multer from 'multer';
1819

1920
function replaceAtStart(string, substring) {
2021
if (string.startsWith(substring)) {
@@ -62,6 +63,7 @@ const EXPRESS_REGEXP_PARAM_CAPTURE_RE = /\(\?:\(\[\^\\\/]\+\?\)\)/g;
6263
const EXPRESS_REGEXP_ESCAPED_SLASH_RE = /\\\//g;
6364
const EXPRESS_REGEXP_TRAILING_DOLLAR_RE = /\$$/;
6465
const EXPRESS_REGEXP_LEADING_CARET_RE = /^\^/;
66+
type MulterParser = (req: any, res: any, callback: (error?: unknown) => void) => void;
6567

6668
type RegisteredExpressRouteSchema = IAdminForthExpressRouteSchema & {
6769
request?: AnySchemaObject;
@@ -154,9 +156,13 @@ class ExpressServer implements IExpressHttpServer {
154156
adminforth: IAdminForth;
155157
server: http.Server;
156158
schemaAwareRouteRegistrationPatched = false;
159+
uploadParser: MulterParser;
157160

158161
constructor(adminforth: IAdminForth) {
159162
this.adminforth = adminforth;
163+
this.uploadParser = multer({
164+
storage: multer.memoryStorage(),
165+
}).any();
160166
}
161167

162168
setupSpaServer() {
@@ -543,6 +549,7 @@ class ExpressServer implements IExpressHttpServer {
543549
request_schema,
544550
response_schema,
545551
responce_schema,
552+
target='json'
546553
} = options;
547554
if (!path.startsWith('/')) {
548555
throw new Error(`Path must start with /, got: ${path}`);
@@ -572,17 +579,39 @@ class ExpressServer implements IExpressHttpServer {
572579
// AdminForth API endpoints accept only application/json for POST, PUT, PATCH, DELETE
573580
// If you need other content types, use a custom server endpoint.
574581
const method = (req.method || '').toUpperCase();
582+
const contentTypeHeader = (req.headers?.['content-type'] || '').toString();
575583
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
576-
const contentTypeHeader = (req.headers?.['content-type'] || '').toString();
577-
const isJson = contentTypeHeader.toLowerCase().startsWith('application/json');
578-
if (!isJson) {
584+
const expectedContentType = target === 'upload' ? 'multipart/form-data' : 'application/json';
585+
const hasExpectedContentType = contentTypeHeader.toLowerCase().startsWith(expectedContentType);
586+
if (!hasExpectedContentType) {
579587
const passed = contentTypeHeader || 'undefined';
580-
res.status(415).send(`AdminForth API endpoints support only requests with Content/Type: application/json, when you passed: ${passed}. Please use custom server endpoint if you really need this content type`);
588+
res.status(415).send(`AdminForth API endpoint supports only requests with Content-Type: ${expectedContentType}, when you passed: ${passed}. Please use custom server endpoint if you really need this content type`);
589+
return;
590+
}
591+
}
592+
if (target === 'upload') {
593+
try {
594+
await new Promise<void>((resolve, reject) => {
595+
this.uploadParser(req, res, (error?: unknown) => {
596+
if (error) {
597+
reject(error);
598+
return;
599+
}
600+
601+
resolve();
602+
});
603+
});
604+
if (!(req as any).file && Array.isArray((req as any).files) && (req as any).files.length) {
605+
(req as any).file = (req as any).files[0];
606+
}
607+
} catch (error) {
608+
afLogger.error(`Failed to parse multipart form-data body, ${error}`);
609+
res.status(400).send('Invalid multipart/form-data body');
581610
return;
582611
}
583612
}
584613
let body = req.body || {};
585-
if (typeof body === 'string') {
614+
if (typeof body === 'string' && target === 'json') {
586615
try {
587616
body = JSON.parse(body);
588617
} catch (e) {

adminforth/types/Back.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export interface IAdminForthEndpointOptions {
6363
response_schema?: AnySchemaObject,
6464
responce_schema?: AnySchemaObject,
6565
meta?: Record<string, unknown>,
66+
target?: 'json' | 'upload',
6667
handler: (input: IAdminForthEndpointHandlerInput) => void | Promise<any>,
6768
}
6869

0 commit comments

Comments
 (0)