payload 3.88.0 → 3.89.0-internal.95c9cba
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/dist/collections/operations/create.d.ts.map +1 -1
- package/dist/collections/operations/create.js +10 -0
- package/dist/collections/operations/create.js.map +1 -1
- package/dist/collections/operations/update.d.ts.map +1 -1
- package/dist/collections/operations/update.js +10 -0
- package/dist/collections/operations/update.js.map +1 -1
- package/dist/collections/operations/updateByID.d.ts.map +1 -1
- package/dist/collections/operations/updateByID.js +10 -0
- package/dist/collections/operations/updateByID.js.map +1 -1
- package/dist/index.bundled.d.ts +10 -0
- package/dist/uploads/cropImage.d.ts.map +1 -1
- package/dist/uploads/cropImage.js +2 -5
- package/dist/uploads/cropImage.js.map +1 -1
- package/dist/uploads/generateFileData.d.ts +8 -0
- package/dist/uploads/generateFileData.d.ts.map +1 -1
- package/dist/uploads/generateFileData.js +55 -29
- package/dist/uploads/generateFileData.js.map +1 -1
- package/dist/uploads/generateFileData.spec.js +144 -0
- package/dist/uploads/generateFileData.spec.js.map +1 -0
- package/dist/uploads/generateFileData.tempFileBuffering.spec.js +81 -0
- package/dist/uploads/generateFileData.tempFileBuffering.spec.js.map +1 -0
- package/dist/uploads/getFileContentRequirement.d.ts +9 -0
- package/dist/uploads/getFileContentRequirement.d.ts.map +1 -0
- package/dist/uploads/getFileContentRequirement.js +24 -0
- package/dist/uploads/getFileContentRequirement.js.map +1 -0
- package/dist/uploads/getFileContentRequirement.spec.js +95 -0
- package/dist/uploads/getFileContentRequirement.spec.js.map +1 -0
- package/dist/uploads/getFileFromClientUpload.d.ts +20 -0
- package/dist/uploads/getFileFromClientUpload.d.ts.map +1 -0
- package/dist/uploads/getFileFromClientUpload.js +199 -0
- package/dist/uploads/getFileFromClientUpload.js.map +1 -0
- package/dist/uploads/getFileFromClientUpload.spec.js +528 -0
- package/dist/uploads/getFileFromClientUpload.spec.js.map +1 -0
- package/dist/uploads/image-resizing/createImageSizes.d.ts.map +1 -1
- package/dist/uploads/image-resizing/createImageSizes.js +2 -5
- package/dist/uploads/image-resizing/createImageSizes.js.map +1 -1
- package/dist/uploads/isAnimatedImage.d.ts +6 -0
- package/dist/uploads/isAnimatedImage.d.ts.map +1 -0
- package/dist/uploads/isAnimatedImage.js +19 -0
- package/dist/uploads/isAnimatedImage.js.map +1 -0
- package/dist/uploads/isAnimatedImage.spec.js +19 -0
- package/dist/uploads/isAnimatedImage.spec.js.map +1 -0
- package/dist/uploads/types.d.ts +10 -0
- package/dist/uploads/types.d.ts.map +1 -1
- package/dist/uploads/types.js.map +1 -1
- package/dist/uploads/unlinkTempFiles.d.ts.map +1 -1
- package/dist/uploads/unlinkTempFiles.js +12 -2
- package/dist/uploads/unlinkTempFiles.js.map +1 -1
- package/dist/uploads/unlinkTempFiles.spec.js +157 -0
- package/dist/uploads/unlinkTempFiles.spec.js.map +1 -0
- package/dist/uploads/uploadFiles.d.ts.map +1 -1
- package/dist/uploads/uploadFiles.js +7 -2
- package/dist/uploads/uploadFiles.js.map +1 -1
- package/dist/uploads/uploadFiles.spec.js +44 -0
- package/dist/uploads/uploadFiles.spec.js.map +1 -0
- package/dist/utilities/addDataAndFileToRequest.d.ts.map +1 -1
- package/dist/utilities/addDataAndFileToRequest.js +7 -46
- package/dist/utilities/addDataAndFileToRequest.js.map +1 -1
- package/dist/utilities/addDataAndFileToRequest.spec.js +131 -1
- package/dist/utilities/addDataAndFileToRequest.spec.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { APIError } from '../errors/APIError.js';
|
|
2
2
|
import { processMultipartFormdata } from '../uploads/fetchAPI-multipart/index.js';
|
|
3
|
+
import { getFileFromClientUpload } from '../uploads/getFileFromClientUpload.js';
|
|
3
4
|
/**
|
|
4
5
|
* Mutates the Request, appending 'data' and 'file' if found
|
|
5
6
|
*/ export const addDataAndFileToRequest = async (req)=>{
|
|
@@ -50,56 +51,16 @@ import { processMultipartFormdata } from '../uploads/fetchAPI-multipart/index.js
|
|
|
50
51
|
req.data = JSON.parse(fields._payload);
|
|
51
52
|
}
|
|
52
53
|
if (!req.file && fields?.file && typeof fields?.file === 'string') {
|
|
53
|
-
let
|
|
54
|
+
let clientUploadFile;
|
|
54
55
|
try {
|
|
55
|
-
;
|
|
56
|
-
({ clientUploadContext, collectionSlug, filename, mimeType, size } = JSON.parse(fields.file));
|
|
56
|
+
clientUploadFile = JSON.parse(fields.file);
|
|
57
57
|
} catch {
|
|
58
58
|
throw new APIError('A file name is required.', 400);
|
|
59
59
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
let response = null;
|
|
65
|
-
let error;
|
|
66
|
-
for (const handler of uploadConfig.handlers){
|
|
67
|
-
try {
|
|
68
|
-
const result = await handler(req, {
|
|
69
|
-
doc: null,
|
|
70
|
-
params: {
|
|
71
|
-
clientUploadContext,
|
|
72
|
-
collection: collectionSlug,
|
|
73
|
-
filename
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
if (result) {
|
|
77
|
-
response = result;
|
|
78
|
-
}
|
|
79
|
-
// If we couldn't get the file from that handler, save the error and try other.
|
|
80
|
-
} catch (err) {
|
|
81
|
-
error = err;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (!response) {
|
|
85
|
-
if (error) {
|
|
86
|
-
payload.logger.error(error);
|
|
87
|
-
}
|
|
88
|
-
throw new APIError('Expected response from the upload handler.');
|
|
89
|
-
}
|
|
90
|
-
if (response.status >= 300 && response.status < 400) {
|
|
91
|
-
const redirectUrl = response.headers.get('Location');
|
|
92
|
-
if (redirectUrl) {
|
|
93
|
-
response = await fetch(redirectUrl);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
req.file = {
|
|
97
|
-
name: filename,
|
|
98
|
-
clientUploadContext,
|
|
99
|
-
data: Buffer.from(await response.arrayBuffer()),
|
|
100
|
-
mimetype: response.headers.get('Content-Type') || mimeType,
|
|
101
|
-
size
|
|
102
|
-
};
|
|
60
|
+
req.file = await getFileFromClientUpload({
|
|
61
|
+
file: clientUploadFile,
|
|
62
|
+
req
|
|
63
|
+
});
|
|
103
64
|
}
|
|
104
65
|
}
|
|
105
66
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/addDataAndFileToRequest.ts"],"sourcesContent":["import type { PayloadRequest } from '../types/index.js'\n\nimport { APIError } from '../errors/APIError.js'\nimport { processMultipartFormdata } from '../uploads/fetchAPI-multipart/index.js'\n\ntype AddDataAndFileToRequest = (req: PayloadRequest) => Promise<void>\n\n/**\n * Mutates the Request, appending 'data' and 'file' if found\n */\nexport const addDataAndFileToRequest: AddDataAndFileToRequest = async (req) => {\n const { body, headers, method, payload } = req\n\n if (method && ['PATCH', 'POST', 'PUT'].includes(method.toUpperCase()) && body) {\n const [contentType] = (headers.get('Content-Type') || '').split(';', 1)\n const bodyByteSize = parseInt(req.headers.get('Content-Length') || '0', 10)\n const hasBodyStream = req.body !== null\n\n if (contentType === 'application/json') {\n try {\n const text = await req.text?.()\n const data = text ? JSON.parse(text) : {}\n req.data = data\n // @ts-expect-error attach json method to request\n req.json = () => Promise.resolve(data)\n } catch (error) {\n if (error instanceof SyntaxError) {\n throw new APIError('Invalid JSON', 400)\n }\n req.payload.logger.error(error)\n throw error\n }\n } else if ((bodyByteSize || hasBodyStream) && contentType?.includes('multipart/')) {\n const { error, fields, files } = await processMultipartFormdata({\n options: {\n ...(payload.config.bodyParser || {}),\n ...(payload.config.upload || {}),\n },\n request: req as Request,\n })\n\n if (error) {\n throw new APIError(error.message)\n }\n\n // Set all files on req.files for access by hooks\n if (files) {\n req.files = files\n // Backwards compatibility: set req.file for standard upload collections\n // Guard: if multiple files share the field name \"file\", files.file is an array — skip\n if (files.file && !Array.isArray(files.file)) {\n req.file = files.file\n }\n }\n\n if (fields?._payload && typeof fields._payload === 'string') {\n req.data = JSON.parse(fields._payload)\n }\n\n if (!req.file && fields?.file && typeof fields?.file === 'string') {\n let
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/addDataAndFileToRequest.ts"],"sourcesContent":["import type { PayloadRequest } from '../types/index.js'\nimport type { ClientUploadData } from '../uploads/getFileFromClientUpload.js'\n\nimport { APIError } from '../errors/APIError.js'\nimport { processMultipartFormdata } from '../uploads/fetchAPI-multipart/index.js'\nimport { getFileFromClientUpload } from '../uploads/getFileFromClientUpload.js'\n\ntype AddDataAndFileToRequest = (req: PayloadRequest) => Promise<void>\n\n/**\n * Mutates the Request, appending 'data' and 'file' if found\n */\nexport const addDataAndFileToRequest: AddDataAndFileToRequest = async (req) => {\n const { body, headers, method, payload } = req\n\n if (method && ['PATCH', 'POST', 'PUT'].includes(method.toUpperCase()) && body) {\n const [contentType] = (headers.get('Content-Type') || '').split(';', 1)\n const bodyByteSize = parseInt(req.headers.get('Content-Length') || '0', 10)\n const hasBodyStream = req.body !== null\n\n if (contentType === 'application/json') {\n try {\n const text = await req.text?.()\n const data = text ? JSON.parse(text) : {}\n req.data = data\n // @ts-expect-error attach json method to request\n req.json = () => Promise.resolve(data)\n } catch (error) {\n if (error instanceof SyntaxError) {\n throw new APIError('Invalid JSON', 400)\n }\n req.payload.logger.error(error)\n throw error\n }\n } else if ((bodyByteSize || hasBodyStream) && contentType?.includes('multipart/')) {\n const { error, fields, files } = await processMultipartFormdata({\n options: {\n ...(payload.config.bodyParser || {}),\n ...(payload.config.upload || {}),\n },\n request: req as Request,\n })\n\n if (error) {\n throw new APIError(error.message)\n }\n\n // Set all files on req.files for access by hooks\n if (files) {\n req.files = files\n // Backwards compatibility: set req.file for standard upload collections\n // Guard: if multiple files share the field name \"file\", files.file is an array — skip\n if (files.file && !Array.isArray(files.file)) {\n req.file = files.file\n }\n }\n\n if (fields?._payload && typeof fields._payload === 'string') {\n req.data = JSON.parse(fields._payload)\n }\n\n if (!req.file && fields?.file && typeof fields?.file === 'string') {\n let clientUploadFile: ClientUploadData\n\n try {\n clientUploadFile = JSON.parse(fields.file) as ClientUploadData\n } catch {\n throw new APIError('A file name is required.', 400)\n }\n\n req.file = await getFileFromClientUpload({\n file: clientUploadFile,\n req,\n })\n }\n }\n }\n}\n"],"names":["APIError","processMultipartFormdata","getFileFromClientUpload","addDataAndFileToRequest","req","body","headers","method","payload","includes","toUpperCase","contentType","get","split","bodyByteSize","parseInt","hasBodyStream","text","data","JSON","parse","json","Promise","resolve","error","SyntaxError","logger","fields","files","options","config","bodyParser","upload","request","message","file","Array","isArray","_payload","clientUploadFile"],"mappings":"AAGA,SAASA,QAAQ,QAAQ,wBAAuB;AAChD,SAASC,wBAAwB,QAAQ,yCAAwC;AACjF,SAASC,uBAAuB,QAAQ,wCAAuC;AAI/E;;CAEC,GACD,OAAO,MAAMC,0BAAmD,OAAOC;IACrE,MAAM,EAAEC,IAAI,EAAEC,OAAO,EAAEC,MAAM,EAAEC,OAAO,EAAE,GAAGJ;IAE3C,IAAIG,UAAU;QAAC;QAAS;QAAQ;KAAM,CAACE,QAAQ,CAACF,OAAOG,WAAW,OAAOL,MAAM;QAC7E,MAAM,CAACM,YAAY,GAAG,AAACL,CAAAA,QAAQM,GAAG,CAAC,mBAAmB,EAAC,EAAGC,KAAK,CAAC,KAAK;QACrE,MAAMC,eAAeC,SAASX,IAAIE,OAAO,CAACM,GAAG,CAAC,qBAAqB,KAAK;QACxE,MAAMI,gBAAgBZ,IAAIC,IAAI,KAAK;QAEnC,IAAIM,gBAAgB,oBAAoB;YACtC,IAAI;gBACF,MAAMM,OAAO,MAAMb,IAAIa,IAAI;gBAC3B,MAAMC,OAAOD,OAAOE,KAAKC,KAAK,CAACH,QAAQ,CAAC;gBACxCb,IAAIc,IAAI,GAAGA;gBACX,iDAAiD;gBACjDd,IAAIiB,IAAI,GAAG,IAAMC,QAAQC,OAAO,CAACL;YACnC,EAAE,OAAOM,OAAO;gBACd,IAAIA,iBAAiBC,aAAa;oBAChC,MAAM,IAAIzB,SAAS,gBAAgB;gBACrC;gBACAI,IAAII,OAAO,CAACkB,MAAM,CAACF,KAAK,CAACA;gBACzB,MAAMA;YACR;QACF,OAAO,IAAI,AAACV,CAAAA,gBAAgBE,aAAY,KAAML,aAAaF,SAAS,eAAe;YACjF,MAAM,EAAEe,KAAK,EAAEG,MAAM,EAAEC,KAAK,EAAE,GAAG,MAAM3B,yBAAyB;gBAC9D4B,SAAS;oBACP,GAAIrB,QAAQsB,MAAM,CAACC,UAAU,IAAI,CAAC,CAAC;oBACnC,GAAIvB,QAAQsB,MAAM,CAACE,MAAM,IAAI,CAAC,CAAC;gBACjC;gBACAC,SAAS7B;YACX;YAEA,IAAIoB,OAAO;gBACT,MAAM,IAAIxB,SAASwB,MAAMU,OAAO;YAClC;YAEA,iDAAiD;YACjD,IAAIN,OAAO;gBACTxB,IAAIwB,KAAK,GAAGA;gBACZ,wEAAwE;gBACxE,sFAAsF;gBACtF,IAAIA,MAAMO,IAAI,IAAI,CAACC,MAAMC,OAAO,CAACT,MAAMO,IAAI,GAAG;oBAC5C/B,IAAI+B,IAAI,GAAGP,MAAMO,IAAI;gBACvB;YACF;YAEA,IAAIR,QAAQW,YAAY,OAAOX,OAAOW,QAAQ,KAAK,UAAU;gBAC3DlC,IAAIc,IAAI,GAAGC,KAAKC,KAAK,CAACO,OAAOW,QAAQ;YACvC;YAEA,IAAI,CAAClC,IAAI+B,IAAI,IAAIR,QAAQQ,QAAQ,OAAOR,QAAQQ,SAAS,UAAU;gBACjE,IAAII;gBAEJ,IAAI;oBACFA,mBAAmBpB,KAAKC,KAAK,CAACO,OAAOQ,IAAI;gBAC3C,EAAE,OAAM;oBACN,MAAM,IAAInC,SAAS,4BAA4B;gBACjD;gBAEAI,IAAI+B,IAAI,GAAG,MAAMjC,wBAAwB;oBACvCiC,MAAMI;oBACNnC;gBACF;YACF;QACF;IACF;AACF,EAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
3
|
import { addDataAndFileToRequest } from './addDataAndFileToRequest.js';
|
|
3
4
|
const createReqWithMultipartBody = ()=>{
|
|
4
5
|
const formData = new FormData();
|
|
@@ -27,6 +28,41 @@ const createReqWithMultipartBody = ()=>{
|
|
|
27
28
|
}
|
|
28
29
|
};
|
|
29
30
|
};
|
|
31
|
+
const createClientUploadReq = ({ file, handler, upload })=>{
|
|
32
|
+
const formData = new FormData();
|
|
33
|
+
formData.append('file', JSON.stringify(file));
|
|
34
|
+
const request = new Request('http://localhost/api/media', {
|
|
35
|
+
body: formData,
|
|
36
|
+
method: 'POST'
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
body: request.body,
|
|
40
|
+
headers: request.headers,
|
|
41
|
+
method: request.method,
|
|
42
|
+
payload: {
|
|
43
|
+
collections: {
|
|
44
|
+
media: {
|
|
45
|
+
config: {
|
|
46
|
+
upload: {
|
|
47
|
+
disableLocalStorage: true,
|
|
48
|
+
handlers: [
|
|
49
|
+
handler
|
|
50
|
+
],
|
|
51
|
+
...upload
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
config: {
|
|
57
|
+
bodyParser: {},
|
|
58
|
+
upload: {}
|
|
59
|
+
},
|
|
60
|
+
logger: {
|
|
61
|
+
error: ()=>{}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
};
|
|
30
66
|
describe('addDataAndFileToRequest', ()=>{
|
|
31
67
|
it('should parse multipart form-data even when content-length is absent', async ()=>{
|
|
32
68
|
const req = createReqWithMultipartBody();
|
|
@@ -36,6 +72,100 @@ describe('addDataAndFileToRequest', ()=>{
|
|
|
36
72
|
expect(req.file?.name).toBe('hello.txt');
|
|
37
73
|
expect(req.file?.mimetype).toBe('text/plain');
|
|
38
74
|
});
|
|
75
|
+
describe('client uploads', ()=>{
|
|
76
|
+
const tempFilesToRemove = [];
|
|
77
|
+
afterEach(async ()=>{
|
|
78
|
+
for (const tempFilePath of tempFilesToRemove){
|
|
79
|
+
await fs.rm(tempFilePath, {
|
|
80
|
+
force: true
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
tempFilesToRemove.length = 0;
|
|
84
|
+
});
|
|
85
|
+
it('materializes client-upload metadata without buffering an unused cloud file', async ()=>{
|
|
86
|
+
const handler = vi.fn(()=>{
|
|
87
|
+
throw new Error('No-content handler was invoked');
|
|
88
|
+
});
|
|
89
|
+
const req = createClientUploadReq({
|
|
90
|
+
file: {
|
|
91
|
+
clientUploadContext: {
|
|
92
|
+
prefix: ''
|
|
93
|
+
},
|
|
94
|
+
collectionSlug: 'media',
|
|
95
|
+
filename: 'large.mp4',
|
|
96
|
+
mimeType: 'video/mp4',
|
|
97
|
+
size: 5_000_000_000
|
|
98
|
+
},
|
|
99
|
+
handler,
|
|
100
|
+
upload: {
|
|
101
|
+
disableLocalStorage: true
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
await addDataAndFileToRequest(req);
|
|
105
|
+
expect(handler).not.toHaveBeenCalled();
|
|
106
|
+
expect(req.file).toMatchObject({
|
|
107
|
+
clientUploadContext: {
|
|
108
|
+
prefix: ''
|
|
109
|
+
},
|
|
110
|
+
mimetype: 'video/mp4',
|
|
111
|
+
name: 'large.mp4',
|
|
112
|
+
size: 5_000_000_000
|
|
113
|
+
});
|
|
114
|
+
expect(req.file?.data.length).toBe(0);
|
|
115
|
+
});
|
|
116
|
+
it('streams a full-content client upload to a temp file without buffering the whole body', async ()=>{
|
|
117
|
+
const chunks = [
|
|
118
|
+
Buffer.from('chunk-one-'),
|
|
119
|
+
Buffer.from('chunk-two')
|
|
120
|
+
];
|
|
121
|
+
const stream = new ReadableStream({
|
|
122
|
+
start (controller) {
|
|
123
|
+
for (const chunk of chunks){
|
|
124
|
+
controller.enqueue(chunk);
|
|
125
|
+
}
|
|
126
|
+
controller.close();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
const response = new Response(stream, {
|
|
130
|
+
headers: {
|
|
131
|
+
'Content-Type': 'video/mp4'
|
|
132
|
+
},
|
|
133
|
+
status: 200
|
|
134
|
+
});
|
|
135
|
+
const arrayBufferTripwire = vi.fn(async ()=>{
|
|
136
|
+
throw new Error('Unexpected whole-body buffering');
|
|
137
|
+
});
|
|
138
|
+
Object.defineProperty(response, 'arrayBuffer', {
|
|
139
|
+
value: arrayBufferTripwire
|
|
140
|
+
});
|
|
141
|
+
const handler = vi.fn(async ()=>response);
|
|
142
|
+
const req = createClientUploadReq({
|
|
143
|
+
file: {
|
|
144
|
+
clientUploadContext: {
|
|
145
|
+
prefix: ''
|
|
146
|
+
},
|
|
147
|
+
collectionSlug: 'media',
|
|
148
|
+
filename: 'clip.mp4',
|
|
149
|
+
mimeType: 'video/mp4',
|
|
150
|
+
size: 20
|
|
151
|
+
},
|
|
152
|
+
handler,
|
|
153
|
+
upload: {
|
|
154
|
+
disableLocalStorage: true,
|
|
155
|
+
mimeTypes: [
|
|
156
|
+
'video/*'
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
await addDataAndFileToRequest(req);
|
|
161
|
+
tempFilesToRemove.push(req.file.tempFilePath);
|
|
162
|
+
expect(arrayBufferTripwire).not.toHaveBeenCalled();
|
|
163
|
+
expect(req.file?.data.length).toBe(0);
|
|
164
|
+
expect(req.file?.tempFilePath).toBeDefined();
|
|
165
|
+
const written = await fs.readFile(req.file.tempFilePath);
|
|
166
|
+
expect(written.toString()).toBe(chunks.map((chunk)=>chunk.toString()).join(''));
|
|
167
|
+
});
|
|
168
|
+
});
|
|
39
169
|
});
|
|
40
170
|
|
|
41
171
|
//# sourceMappingURL=addDataAndFileToRequest.spec.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/addDataAndFileToRequest.spec.ts"],"sourcesContent":["import type { PayloadRequest } from '../types/index.js'\n\nimport { describe, expect, it } from 'vitest'\n\nimport { addDataAndFileToRequest } from './addDataAndFileToRequest.js'\n\ntype MinimalReq = Pick<PayloadRequest, 'body' | 'headers' | 'method' | 'payload'> & {\n file?: PayloadRequest['file']\n}\n\nconst createReqWithMultipartBody = (): MinimalReq => {\n const formData = new FormData()\n formData.append('file', new Blob(['hello world'], { type: 'text/plain' }), 'hello.txt')\n\n const request = new Request('http://localhost/api/upload', {\n body: formData,\n method: 'POST',\n })\n\n return {\n body: request.body,\n headers: request.headers,\n method: request.method,\n payload: {\n collections: {},\n config: {\n bodyParser: {},\n upload: {},\n },\n logger: {\n error: () => {},\n },\n } as unknown as PayloadRequest['payload'],\n }\n}\n\ndescribe('addDataAndFileToRequest', () => {\n it('should parse multipart form-data even when content-length is absent', async () => {\n const req = createReqWithMultipartBody()\n\n expect(req.headers.get('content-length')).toBeNull()\n\n await addDataAndFileToRequest(req as PayloadRequest)\n\n expect(req.file).toBeDefined()\n expect(req.file?.name).toBe('hello.txt')\n expect(req.file?.mimetype).toBe('text/plain')\n })\n})\n"],"names":["describe","expect","it","addDataAndFileToRequest","createReqWithMultipartBody","formData","FormData","append","Blob","type","request","Request","body","method","headers","payload","collections","config","bodyParser","upload","logger","error","req","get","toBeNull","
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/addDataAndFileToRequest.spec.ts"],"sourcesContent":["import type { PayloadRequest } from '../types/index.js'\nimport type { ClientUploadData } from '../uploads/getFileFromClientUpload.js'\nimport type { SanitizedUploadConfig } from '../uploads/types.js'\n\nimport fs from 'fs/promises'\nimport { afterEach, describe, expect, it, vi } from 'vitest'\n\nimport { addDataAndFileToRequest } from './addDataAndFileToRequest.js'\n\ntype MinimalReq = Pick<PayloadRequest, 'body' | 'headers' | 'method' | 'payload'> & {\n file?: PayloadRequest['file']\n}\n\nconst createReqWithMultipartBody = (): MinimalReq => {\n const formData = new FormData()\n formData.append('file', new Blob(['hello world'], { type: 'text/plain' }), 'hello.txt')\n\n const request = new Request('http://localhost/api/upload', {\n body: formData,\n method: 'POST',\n })\n\n return {\n body: request.body,\n headers: request.headers,\n method: request.method,\n payload: {\n collections: {},\n config: {\n bodyParser: {},\n upload: {},\n },\n logger: {\n error: () => {},\n },\n } as unknown as PayloadRequest['payload'],\n }\n}\n\nconst createClientUploadReq = ({\n file,\n handler,\n upload,\n}: {\n file: ClientUploadData\n handler: NonNullable<SanitizedUploadConfig['handlers']>[number]\n upload?: Partial<SanitizedUploadConfig>\n}): MinimalReq => {\n const formData = new FormData()\n formData.append('file', JSON.stringify(file))\n\n const request = new Request('http://localhost/api/media', {\n body: formData,\n method: 'POST',\n })\n\n return {\n body: request.body,\n headers: request.headers,\n method: request.method,\n payload: {\n collections: {\n media: {\n config: {\n upload: {\n disableLocalStorage: true,\n handlers: [handler],\n ...upload,\n },\n },\n },\n },\n config: {\n bodyParser: {},\n upload: {},\n },\n logger: {\n error: () => {},\n },\n } as unknown as PayloadRequest['payload'],\n }\n}\n\ndescribe('addDataAndFileToRequest', () => {\n it('should parse multipart form-data even when content-length is absent', async () => {\n const req = createReqWithMultipartBody()\n\n expect(req.headers.get('content-length')).toBeNull()\n\n await addDataAndFileToRequest(req as PayloadRequest)\n\n expect(req.file).toBeDefined()\n expect(req.file?.name).toBe('hello.txt')\n expect(req.file?.mimetype).toBe('text/plain')\n })\n\n describe('client uploads', () => {\n const tempFilesToRemove: string[] = []\n\n afterEach(async () => {\n for (const tempFilePath of tempFilesToRemove) {\n await fs.rm(tempFilePath, { force: true })\n }\n tempFilesToRemove.length = 0\n })\n\n it('materializes client-upload metadata without buffering an unused cloud file', async () => {\n const handler = vi.fn(() => {\n throw new Error('No-content handler was invoked')\n })\n const req = createClientUploadReq({\n file: {\n clientUploadContext: { prefix: '' },\n collectionSlug: 'media',\n filename: 'large.mp4',\n mimeType: 'video/mp4',\n size: 5_000_000_000,\n },\n handler,\n upload: { disableLocalStorage: true },\n })\n\n await addDataAndFileToRequest(req as PayloadRequest)\n\n expect(handler).not.toHaveBeenCalled()\n expect(req.file).toMatchObject({\n clientUploadContext: { prefix: '' },\n mimetype: 'video/mp4',\n name: 'large.mp4',\n size: 5_000_000_000,\n })\n expect(req.file?.data.length).toBe(0)\n })\n\n it('streams a full-content client upload to a temp file without buffering the whole body', async () => {\n const chunks = [Buffer.from('chunk-one-'), Buffer.from('chunk-two')]\n const stream = new ReadableStream({\n start(controller) {\n for (const chunk of chunks) {\n controller.enqueue(chunk)\n }\n controller.close()\n },\n })\n const response = new Response(stream, {\n headers: { 'Content-Type': 'video/mp4' },\n status: 200,\n })\n const arrayBufferTripwire = vi.fn(async () => {\n throw new Error('Unexpected whole-body buffering')\n })\n Object.defineProperty(response, 'arrayBuffer', { value: arrayBufferTripwire })\n\n const handler = vi.fn(async () => response)\n const req = createClientUploadReq({\n file: {\n clientUploadContext: { prefix: '' },\n collectionSlug: 'media',\n filename: 'clip.mp4',\n mimeType: 'video/mp4',\n size: 20,\n },\n handler,\n upload: { disableLocalStorage: true, mimeTypes: ['video/*'] },\n })\n\n await addDataAndFileToRequest(req as PayloadRequest)\n tempFilesToRemove.push(req.file!.tempFilePath!)\n\n expect(arrayBufferTripwire).not.toHaveBeenCalled()\n expect(req.file?.data.length).toBe(0)\n expect(req.file?.tempFilePath).toBeDefined()\n const written = await fs.readFile(req.file!.tempFilePath!)\n expect(written.toString()).toBe(chunks.map((chunk) => chunk.toString()).join(''))\n })\n })\n})\n"],"names":["fs","afterEach","describe","expect","it","vi","addDataAndFileToRequest","createReqWithMultipartBody","formData","FormData","append","Blob","type","request","Request","body","method","headers","payload","collections","config","bodyParser","upload","logger","error","createClientUploadReq","file","handler","JSON","stringify","media","disableLocalStorage","handlers","req","get","toBeNull","toBeDefined","name","toBe","mimetype","tempFilesToRemove","tempFilePath","rm","force","length","fn","Error","clientUploadContext","prefix","collectionSlug","filename","mimeType","size","not","toHaveBeenCalled","toMatchObject","data","chunks","Buffer","from","stream","ReadableStream","start","controller","chunk","enqueue","close","response","Response","status","arrayBufferTripwire","Object","defineProperty","value","mimeTypes","push","written","readFile","toString","map","join"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payload",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.89.0-internal.95c9cba",
|
|
4
4
|
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"admin panel",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"undici": "7.29.0",
|
|
116
116
|
"uuid": "13.0.2",
|
|
117
117
|
"ws": "^8.16.0",
|
|
118
|
-
"@payloadcms/translations": "3.
|
|
118
|
+
"@payloadcms/translations": "3.89.0-internal.95c9cba"
|
|
119
119
|
},
|
|
120
120
|
"devDependencies": {
|
|
121
121
|
"@hyrious/esbuild-plugin-commonjs": "0.2.6",
|