multipart
This commit is contained in:
@@ -1,5 +1,78 @@
|
||||
import multer from 'multer';
|
||||
import * as db from '../../db/delivery.js';
|
||||
|
||||
// Shared memory storage for delivery attachments (max 50 MB)
|
||||
const multerUpload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 50 * 1024 * 1024 },
|
||||
fileFilter: (_req, file, cb) => {
|
||||
const name = file.originalname?.toLowerCase() || '';
|
||||
const isPdf = file.mimetype === 'application/pdf' || name.endsWith('.pdf');
|
||||
const isText = file.mimetype.startsWith('text/') || /\.(txt|text|md|csv)$/.test(name);
|
||||
if (isPdf || isText) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only PDF or text files are accepted as attachments'));
|
||||
}
|
||||
},
|
||||
}).array('files');
|
||||
|
||||
function uploadDeliveryFiles(req, res, next) {
|
||||
multerUpload(req, res, (err) => {
|
||||
if (err) {
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(413).json({ error: 'File too large (max 50 MB)' });
|
||||
}
|
||||
return res.status(400).json({ error: err.message });
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to normalize payload from either multipart/form-data or application/json
|
||||
*/
|
||||
function parseDeliveryPayload(req) {
|
||||
let payload = req.body ?? {};
|
||||
// If sent as multipart/form-data with a JSON `data` field:
|
||||
if (typeof payload.data === 'string') {
|
||||
try {
|
||||
payload = JSON.parse(payload.data);
|
||||
} catch {
|
||||
throw new Error('Invalid JSON in "data" field');
|
||||
}
|
||||
}
|
||||
if (typeof payload !== 'object' || payload === null) {
|
||||
throw new Error('Invalid delivery');
|
||||
}
|
||||
|
||||
const { externalNumber, date, supplierId, lines } = payload;
|
||||
let files = payload.files;
|
||||
// If binary files were uploaded via multer:
|
||||
if (Array.isArray(req.files) && req.files.length > 0) {
|
||||
const filesMeta = Array.isArray(payload.filesMeta)
|
||||
? payload.filesMeta
|
||||
: (Array.isArray(payload.files) ? payload.files : []);
|
||||
const metaByName = new Map(
|
||||
filesMeta.filter((m) => m?.name).map((m) => [m.name, m])
|
||||
);
|
||||
files = req.files.map((f, idx) => {
|
||||
const metaObj = metaByName.get(f.originalname) || filesMeta[idx] || {};
|
||||
const name = f.originalname || '';
|
||||
const isPdf = f.mimetype === 'application/pdf' || name.toLowerCase().endsWith('.pdf');
|
||||
const mimetype = isPdf ? 'application/pdf' : 'text/plain';
|
||||
return {
|
||||
mimetype,
|
||||
name: f.originalname,
|
||||
content: f.buffer, // Raw Buffer directly accepted by libdashstore/attachments.js
|
||||
size: f.size,
|
||||
meta: metaObj.meta ?? null,
|
||||
};
|
||||
});
|
||||
}
|
||||
return { payload, externalNumber, date, supplierId, lines, files };
|
||||
}
|
||||
|
||||
function requireSelectedCompany(req) {
|
||||
const companyId = req.user.selectedCompanyId;
|
||||
if (companyId == null) {
|
||||
@@ -17,6 +90,9 @@ function deliveryErrorStatus(error) {
|
||||
if (error.message === 'Article not found') return 404;
|
||||
if (error.message === 'Invalid delivery') return 400;
|
||||
if (error.message === 'Invalid lines') return 400;
|
||||
if (error.message === 'Invalid file') return 400;
|
||||
if (error.message === 'Invalid JSON in "data" field') return 400;
|
||||
if (error.message === 'Only PDF or text files are accepted as attachments') return 400;
|
||||
return 500;
|
||||
}
|
||||
|
||||
@@ -112,6 +188,7 @@ export function register(app, basePath = '') {
|
||||
* description: |
|
||||
* Attached documents (pdf or txt) stored with the delivery
|
||||
* and returned in `files` on every delivery response.
|
||||
* Thumbnails (AVIF) are generated server-side.
|
||||
* items:
|
||||
* $ref: '#/components/schemas/DeliveryFileInput'
|
||||
* example:
|
||||
@@ -129,6 +206,21 @@ export function register(app, basePath = '') {
|
||||
* - mimetype: application/pdf
|
||||
* name: delivery-note.pdf
|
||||
* content: '<base64 encoded content>'
|
||||
* multipart/form-data:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [data]
|
||||
* properties:
|
||||
* data:
|
||||
* type: string
|
||||
* description: |
|
||||
* JSON string containing manifest header and line items ({ externalNumber, date, supplierId, lines, filesMeta? }).
|
||||
* files:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: Raw binary PDF or text files attached natively via standard HTTP multipart upload.
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Created delivery
|
||||
@@ -180,15 +272,14 @@ export function register(app, basePath = '') {
|
||||
}
|
||||
});
|
||||
|
||||
app.post(`${basePath}/delivery`, async (req, res) => {
|
||||
app.post(`${basePath}/delivery`, uploadDeliveryFiles, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const { externalNumber, date, supplierId, lines, files } = req.body ?? {};
|
||||
|
||||
try {
|
||||
const { externalNumber, date, supplierId, lines, files } = parseDeliveryPayload(req);
|
||||
const delivery = await db.create(req.user.id, {
|
||||
externalNumber,
|
||||
date,
|
||||
@@ -297,11 +388,26 @@ export function register(app, basePath = '') {
|
||||
* description: |
|
||||
* Appends attachments (pdf or txt) to the delivery.
|
||||
* Omit to leave existing files unchanged.
|
||||
* Thumbnails (AVIF) are generated server-side.
|
||||
* items:
|
||||
* $ref: '#/components/schemas/DeliveryFileInput'
|
||||
* example:
|
||||
* externalNumber: DHL-124
|
||||
* lines: []
|
||||
* multipart/form-data:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* data:
|
||||
* type: string
|
||||
* description: |
|
||||
* JSON string containing patch fields ({ externalNumber, date, supplierId, lines, filesMeta? }).
|
||||
* files:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: Raw binary PDF or text files to append to the delivery.
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Updated delivery
|
||||
@@ -399,7 +505,7 @@ export function register(app, basePath = '') {
|
||||
}
|
||||
});
|
||||
|
||||
app.patch(`${basePath}/delivery/:id`, async (req, res) => {
|
||||
app.patch(`${basePath}/delivery/:id`, uploadDeliveryFiles, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
@@ -410,25 +516,25 @@ export function register(app, basePath = '') {
|
||||
return res.status(400).json({ error: 'id is required' });
|
||||
}
|
||||
|
||||
const body = req.body ?? {};
|
||||
const patch = {};
|
||||
if (Object.prototype.hasOwnProperty.call(body, 'externalNumber')) {
|
||||
patch.externalNumber = body.externalNumber;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(body, 'date')) {
|
||||
patch.date = body.date;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(body, 'supplierId')) {
|
||||
patch.supplierId = body.supplierId;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(body, 'lines')) {
|
||||
patch.lines = body.lines;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(body, 'files')) {
|
||||
patch.files = body.files;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = parseDeliveryPayload(req);
|
||||
const patch = {};
|
||||
if (Object.prototype.hasOwnProperty.call(parsed.payload, 'externalNumber')) {
|
||||
patch.externalNumber = parsed.externalNumber;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(parsed.payload, 'date')) {
|
||||
patch.date = parsed.date;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(parsed.payload, 'supplierId')) {
|
||||
patch.supplierId = parsed.supplierId;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(parsed.payload, 'lines')) {
|
||||
patch.lines = parsed.lines;
|
||||
}
|
||||
if (parsed.files !== undefined) {
|
||||
patch.files = parsed.files;
|
||||
}
|
||||
|
||||
const delivery = await db.update(req.user.id, id, patch);
|
||||
res.status(200).json({ delivery });
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user