@payloadcms/plugin-cloud-storage 3.91.0-internal.c1569dc → 4.0.0-beta.1
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/LICENSE.md +1 -1
- package/README.md +4 -4
- package/dist/client/createClientUploadHandler.d.ts +8 -7
- package/dist/client/createClientUploadHandler.d.ts.map +1 -1
- package/dist/client/createClientUploadHandler.js +26 -22
- package/dist/client/createClientUploadHandler.js.map +1 -1
- package/dist/exports/client.d.ts +1 -1
- package/dist/exports/client.d.ts.map +1 -1
- package/dist/exports/client.js +1 -1
- package/dist/exports/client.js.map +1 -1
- package/dist/exports/utilities.d.ts +2 -4
- package/dist/exports/utilities.d.ts.map +1 -1
- package/dist/exports/utilities.js +2 -4
- package/dist/exports/utilities.js.map +1 -1
- package/dist/fields/getFields.d.ts +1 -5
- package/dist/fields/getFields.d.ts.map +1 -1
- package/dist/fields/getFields.js +102 -35
- package/dist/fields/getFields.js.map +1 -1
- package/dist/hooks/afterChange.d.ts +3 -1
- package/dist/hooks/afterChange.d.ts.map +1 -1
- package/dist/hooks/afterChange.js +93 -27
- package/dist/hooks/afterChange.js.map +1 -1
- package/dist/hooks/afterDelete.d.ts.map +1 -1
- package/dist/hooks/afterDelete.js +10 -2
- package/dist/hooks/afterDelete.js.map +1 -1
- package/dist/hooks/afterRead.d.ts +6 -1
- package/dist/hooks/afterRead.d.ts.map +1 -1
- package/dist/hooks/afterRead.js +27 -11
- package/dist/hooks/afterRead.js.map +1 -1
- package/dist/hooks/beforeChange.d.ts +2 -1
- package/dist/hooks/beforeChange.d.ts.map +1 -1
- package/dist/hooks/beforeChange.js +14 -9
- package/dist/hooks/beforeChange.js.map +1 -1
- package/dist/hooks/publicOriginalURL.d.ts +8 -0
- package/dist/hooks/publicOriginalURL.d.ts.map +1 -0
- package/dist/hooks/publicOriginalURL.js +34 -0
- package/dist/hooks/publicOriginalURL.js.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +198 -133
- package/dist/plugin.js.map +1 -1
- package/dist/types.d.ts +48 -31
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utilities/buildStoragePathData.d.ts +0 -6
- package/dist/utilities/buildStoragePathData.d.ts.map +1 -1
- package/dist/utilities/buildStoragePathData.js +0 -2
- package/dist/utilities/buildStoragePathData.js.map +1 -1
- package/dist/utilities/createFileOperations.d.ts +13 -0
- package/dist/utilities/createFileOperations.d.ts.map +1 -0
- package/dist/utilities/createFileOperations.js +258 -0
- package/dist/utilities/createFileOperations.js.map +1 -0
- package/dist/utilities/getFilePrefix.d.ts +9 -20
- package/dist/utilities/getFilePrefix.d.ts.map +1 -1
- package/dist/utilities/getFilePrefix.js +42 -31
- package/dist/utilities/getFilePrefix.js.map +1 -1
- package/dist/utilities/getIncomingFiles.js +4 -4
- package/dist/utilities/getIncomingFiles.js.map +1 -1
- package/dist/utilities/getStorageLocationSelect.d.ts +9 -0
- package/dist/utilities/getStorageLocationSelect.d.ts.map +1 -0
- package/dist/utilities/getStorageLocationSelect.js +54 -0
- package/dist/utilities/getStorageLocationSelect.js.map +1 -0
- package/dist/utilities/resolveSignedURLKey.d.ts +10 -10
- package/dist/utilities/resolveSignedURLKey.d.ts.map +1 -1
- package/dist/utilities/resolveSignedURLKey.js +33 -26
- package/dist/utilities/resolveSignedURLKey.js.map +1 -1
- package/dist/utilities/sanitizePrefix.d.ts +1 -9
- package/dist/utilities/sanitizePrefix.d.ts.map +1 -1
- package/dist/utilities/sanitizePrefix.js +1 -21
- package/dist/utilities/sanitizePrefix.js.map +1 -1
- package/package.json +6 -6
- package/dist/admin/fields/getFields.d.ts +0 -17
- package/dist/admin/fields/getFields.d.ts.map +0 -1
- package/dist/admin/fields/getFields.js +0 -98
- package/dist/admin/fields/getFields.js.map +0 -1
- package/dist/admin/index.d.ts +0 -4
- package/dist/admin/index.d.ts.map +0 -1
- package/dist/admin/index.js +0 -34
- package/dist/admin/index.js.map +0 -1
- package/dist/hooks/afterChange.spec.js +0 -150
- package/dist/hooks/afterChange.spec.js.map +0 -1
- package/dist/hooks/normalizeUploadPrefix.spec.js +0 -154
- package/dist/hooks/normalizeUploadPrefix.spec.js.map +0 -1
- package/dist/plugin.spec.js +0 -186
- package/dist/plugin.spec.js.map +0 -1
- package/dist/utilities/assertReceiptMatchesFileKey.d.ts +0 -15
- package/dist/utilities/assertReceiptMatchesFileKey.d.ts.map +0 -1
- package/dist/utilities/assertReceiptMatchesFileKey.js +0 -31
- package/dist/utilities/assertReceiptMatchesFileKey.js.map +0 -1
- package/dist/utilities/buildStoragePathData.spec.js +0 -254
- package/dist/utilities/buildStoragePathData.spec.js.map +0 -1
- package/dist/utilities/getFilePrefix.spec.js +0 -291
- package/dist/utilities/getFilePrefix.spec.js.map +0 -1
- package/dist/utilities/initClientUploads.d.ts +0 -13
- package/dist/utilities/initClientUploads.d.ts.map +0 -1
- package/dist/utilities/initClientUploads.js +0 -66
- package/dist/utilities/initClientUploads.js.map +0 -1
- package/dist/utilities/initClientUploads.spec.js +0 -31
- package/dist/utilities/initClientUploads.spec.js.map +0 -1
- package/dist/utilities/resolveSignedURLKey.spec.js +0 -56
- package/dist/utilities/resolveSignedURLKey.spec.js.map +0 -1
- package/dist/utilities/verifyClientUploadReceiptForFileKey.d.ts +0 -20
- package/dist/utilities/verifyClientUploadReceiptForFileKey.d.ts.map +0 -1
- package/dist/utilities/verifyClientUploadReceiptForFileKey.js +0 -24
- package/dist/utilities/verifyClientUploadReceiptForFileKey.js.map +0 -1
- package/dist/utilities/verifyClientUploadReceiptForFileKey.spec.js +0 -108
- package/dist/utilities/verifyClientUploadReceiptForFileKey.spec.js.map +0 -1
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import { buildStoragePathData, buildUploadStoragePathData, isStoragePathWithinCollectionPrefix } from './buildStoragePathData.js';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
describe('buildStoragePathData', ()=>{
|
|
4
|
-
describe('non-composite mode (useCompositePrefixes: false)', ()=>{
|
|
5
|
-
it('should preserve the key of a legacy document outside the collection prefix', ()=>{
|
|
6
|
-
const result = buildStoragePathData({
|
|
7
|
-
collectionPrefix: 'media/',
|
|
8
|
-
docPrefix: 'media-archive',
|
|
9
|
-
filename: 'test.png',
|
|
10
|
-
useCompositePrefixes: false
|
|
11
|
-
});
|
|
12
|
-
expect(result).toEqual({
|
|
13
|
-
fileKey: 'media-archive/test.png',
|
|
14
|
-
sanitizedCollectionPrefix: 'media',
|
|
15
|
-
sanitizedDocPrefix: 'media-archive',
|
|
16
|
-
sanitizedFilename: 'test.png',
|
|
17
|
-
storageFilePath: 'media-archive/test.png'
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
it('should fallback to collectionPrefix when docPrefix is empty', ()=>{
|
|
21
|
-
const result = buildStoragePathData({
|
|
22
|
-
collectionPrefix: 'collection',
|
|
23
|
-
docPrefix: '',
|
|
24
|
-
filename: 'test.png',
|
|
25
|
-
useCompositePrefixes: false
|
|
26
|
-
});
|
|
27
|
-
expect(result).toEqual({
|
|
28
|
-
fileKey: 'collection/test.png',
|
|
29
|
-
sanitizedCollectionPrefix: 'collection',
|
|
30
|
-
sanitizedDocPrefix: '',
|
|
31
|
-
sanitizedFilename: 'test.png',
|
|
32
|
-
storageFilePath: 'collection/test.png'
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
it('should fallback to collectionPrefix when docPrefix is undefined', ()=>{
|
|
36
|
-
const result = buildStoragePathData({
|
|
37
|
-
collectionPrefix: 'collection',
|
|
38
|
-
filename: 'test.png',
|
|
39
|
-
useCompositePrefixes: false
|
|
40
|
-
});
|
|
41
|
-
expect(result).toEqual({
|
|
42
|
-
fileKey: 'collection/test.png',
|
|
43
|
-
sanitizedCollectionPrefix: 'collection',
|
|
44
|
-
sanitizedDocPrefix: '',
|
|
45
|
-
sanitizedFilename: 'test.png',
|
|
46
|
-
storageFilePath: 'collection/test.png'
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
it('should return only filename when both prefixes are empty', ()=>{
|
|
50
|
-
const result = buildStoragePathData({
|
|
51
|
-
filename: 'test.png',
|
|
52
|
-
useCompositePrefixes: false
|
|
53
|
-
});
|
|
54
|
-
expect(result).toEqual({
|
|
55
|
-
fileKey: 'test.png',
|
|
56
|
-
sanitizedCollectionPrefix: '',
|
|
57
|
-
sanitizedDocPrefix: '',
|
|
58
|
-
sanitizedFilename: 'test.png',
|
|
59
|
-
storageFilePath: 'test.png'
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
describe('composite mode (useCompositePrefixes: true)', ()=>{
|
|
64
|
-
it('should combine collectionPrefix and docPrefix', ()=>{
|
|
65
|
-
const result = buildStoragePathData({
|
|
66
|
-
collectionPrefix: 'collection',
|
|
67
|
-
docPrefix: 'document',
|
|
68
|
-
filename: 'test.png',
|
|
69
|
-
useCompositePrefixes: true
|
|
70
|
-
});
|
|
71
|
-
expect(result).toEqual({
|
|
72
|
-
fileKey: 'collection/document/test.png',
|
|
73
|
-
sanitizedCollectionPrefix: 'collection',
|
|
74
|
-
sanitizedDocPrefix: 'document',
|
|
75
|
-
sanitizedFilename: 'test.png',
|
|
76
|
-
storageFilePath: 'collection/document/test.png'
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
it('should work with only collectionPrefix', ()=>{
|
|
80
|
-
const result = buildStoragePathData({
|
|
81
|
-
collectionPrefix: 'collection',
|
|
82
|
-
filename: 'test.png',
|
|
83
|
-
useCompositePrefixes: true
|
|
84
|
-
});
|
|
85
|
-
expect(result).toEqual({
|
|
86
|
-
fileKey: 'collection/test.png',
|
|
87
|
-
sanitizedCollectionPrefix: 'collection',
|
|
88
|
-
sanitizedDocPrefix: '',
|
|
89
|
-
sanitizedFilename: 'test.png',
|
|
90
|
-
storageFilePath: 'collection/test.png'
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
it('should work with only docPrefix', ()=>{
|
|
94
|
-
const result = buildStoragePathData({
|
|
95
|
-
docPrefix: 'document',
|
|
96
|
-
filename: 'test.png',
|
|
97
|
-
useCompositePrefixes: true
|
|
98
|
-
});
|
|
99
|
-
expect(result).toEqual({
|
|
100
|
-
fileKey: 'document/test.png',
|
|
101
|
-
sanitizedCollectionPrefix: '',
|
|
102
|
-
sanitizedDocPrefix: 'document',
|
|
103
|
-
sanitizedFilename: 'test.png',
|
|
104
|
-
storageFilePath: 'document/test.png'
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
it('should return only filename when both prefixes are empty', ()=>{
|
|
108
|
-
const result = buildStoragePathData({
|
|
109
|
-
filename: 'test.png',
|
|
110
|
-
useCompositePrefixes: true
|
|
111
|
-
});
|
|
112
|
-
expect(result).toEqual({
|
|
113
|
-
fileKey: 'test.png',
|
|
114
|
-
sanitizedCollectionPrefix: '',
|
|
115
|
-
sanitizedDocPrefix: '',
|
|
116
|
-
sanitizedFilename: 'test.png',
|
|
117
|
-
storageFilePath: 'test.png'
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
describe('collection prefix boundaries', ()=>{
|
|
122
|
-
it('should accept a file key within a slash-terminated collection prefix', ()=>{
|
|
123
|
-
expect(isStoragePathWithinCollectionPrefix({
|
|
124
|
-
collectionPrefix: 'media/',
|
|
125
|
-
docPrefix: 'media/documents/test.png'
|
|
126
|
-
})).toBe(true);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
describe('sanitization', ()=>{
|
|
130
|
-
it.each([
|
|
131
|
-
'./media/./images',
|
|
132
|
-
'media\\images',
|
|
133
|
-
'/media//images'
|
|
134
|
-
])('should canonicalize equivalent prefix paths', (docPrefix)=>{
|
|
135
|
-
const result = buildStoragePathData({
|
|
136
|
-
docPrefix,
|
|
137
|
-
filename: 'test.png'
|
|
138
|
-
});
|
|
139
|
-
expect(result).toEqual({
|
|
140
|
-
fileKey: 'media/images/test.png',
|
|
141
|
-
sanitizedCollectionPrefix: '',
|
|
142
|
-
sanitizedDocPrefix: 'media/images',
|
|
143
|
-
sanitizedFilename: 'test.png',
|
|
144
|
-
storageFilePath: 'media/images/test.png'
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
it('should remove path traversal segments from collectionPrefix', ()=>{
|
|
148
|
-
const result = buildStoragePathData({
|
|
149
|
-
collectionPrefix: '../../../etc',
|
|
150
|
-
filename: 'test.png',
|
|
151
|
-
useCompositePrefixes: false
|
|
152
|
-
});
|
|
153
|
-
expect(result).toEqual({
|
|
154
|
-
fileKey: 'etc/test.png',
|
|
155
|
-
sanitizedCollectionPrefix: 'etc',
|
|
156
|
-
sanitizedDocPrefix: '',
|
|
157
|
-
sanitizedFilename: 'test.png',
|
|
158
|
-
storageFilePath: 'etc/test.png'
|
|
159
|
-
});
|
|
160
|
-
expect(result.fileKey).not.toContain('..');
|
|
161
|
-
});
|
|
162
|
-
it('should remove path traversal segments from docPrefix', ()=>{
|
|
163
|
-
const result = buildStoragePathData({
|
|
164
|
-
docPrefix: 'a/../../outside',
|
|
165
|
-
filename: 'test.png',
|
|
166
|
-
useCompositePrefixes: false
|
|
167
|
-
});
|
|
168
|
-
expect(result).toEqual({
|
|
169
|
-
fileKey: 'a/outside/test.png',
|
|
170
|
-
sanitizedCollectionPrefix: '',
|
|
171
|
-
sanitizedDocPrefix: 'a/outside',
|
|
172
|
-
sanitizedFilename: 'test.png',
|
|
173
|
-
storageFilePath: 'a/outside/test.png'
|
|
174
|
-
});
|
|
175
|
-
expect(result.fileKey).not.toContain('..');
|
|
176
|
-
});
|
|
177
|
-
it('should remove control characters from prefixes', ()=>{
|
|
178
|
-
const result = buildStoragePathData({
|
|
179
|
-
collectionPrefix: 'test\x00\x1fprefix',
|
|
180
|
-
filename: 'test.png',
|
|
181
|
-
useCompositePrefixes: false
|
|
182
|
-
});
|
|
183
|
-
expect(result).toEqual({
|
|
184
|
-
fileKey: 'testprefix/test.png',
|
|
185
|
-
sanitizedCollectionPrefix: 'testprefix',
|
|
186
|
-
sanitizedDocPrefix: '',
|
|
187
|
-
sanitizedFilename: 'test.png',
|
|
188
|
-
storageFilePath: 'testprefix/test.png'
|
|
189
|
-
});
|
|
190
|
-
expect(result.fileKey).not.toMatch(/[\x00-\x1f]/);
|
|
191
|
-
});
|
|
192
|
-
it('should normalize both prefixes after removing control characters in composite mode', ()=>{
|
|
193
|
-
const result = buildStoragePathData({
|
|
194
|
-
collectionPrefix: '.\x00./collection',
|
|
195
|
-
docPrefix: '.\x00./doc',
|
|
196
|
-
filename: 'test.png',
|
|
197
|
-
useCompositePrefixes: true
|
|
198
|
-
});
|
|
199
|
-
expect(result).toEqual({
|
|
200
|
-
fileKey: 'collection/doc/test.png',
|
|
201
|
-
sanitizedCollectionPrefix: 'collection',
|
|
202
|
-
sanitizedDocPrefix: 'doc',
|
|
203
|
-
sanitizedFilename: 'test.png',
|
|
204
|
-
storageFilePath: 'collection/doc/test.png'
|
|
205
|
-
});
|
|
206
|
-
expect(result.fileKey).not.toContain('..');
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
describe('buildUploadStoragePathData', ()=>{
|
|
211
|
-
it.each([
|
|
212
|
-
[
|
|
213
|
-
'invoices',
|
|
214
|
-
false,
|
|
215
|
-
'media/invoices/file.png',
|
|
216
|
-
'media/invoices'
|
|
217
|
-
],
|
|
218
|
-
[
|
|
219
|
-
'media/invoices',
|
|
220
|
-
false,
|
|
221
|
-
'media/invoices/file.png',
|
|
222
|
-
'media/invoices'
|
|
223
|
-
],
|
|
224
|
-
[
|
|
225
|
-
'',
|
|
226
|
-
false,
|
|
227
|
-
'media/file.png',
|
|
228
|
-
''
|
|
229
|
-
],
|
|
230
|
-
[
|
|
231
|
-
'invoices',
|
|
232
|
-
true,
|
|
233
|
-
'media/invoices/file.png',
|
|
234
|
-
'invoices'
|
|
235
|
-
]
|
|
236
|
-
])('should persist a readable destination for %s (composite %s)', (docPrefix, useCompositePrefixes, fileKey, storedPrefix)=>{
|
|
237
|
-
const result = buildUploadStoragePathData({
|
|
238
|
-
collectionPrefix: 'media',
|
|
239
|
-
docPrefix,
|
|
240
|
-
filename: 'file.png',
|
|
241
|
-
useCompositePrefixes
|
|
242
|
-
});
|
|
243
|
-
expect(result.storageFilePath).toBe(fileKey);
|
|
244
|
-
expect(result.sanitizedDocPrefix).toBe(storedPrefix);
|
|
245
|
-
expect(buildStoragePathData({
|
|
246
|
-
collectionPrefix: 'media',
|
|
247
|
-
docPrefix: result.sanitizedDocPrefix,
|
|
248
|
-
filename: 'file.png',
|
|
249
|
-
useCompositePrefixes
|
|
250
|
-
}).fileKey).toBe(fileKey);
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
//# sourceMappingURL=buildStoragePathData.spec.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/buildStoragePathData.spec.ts"],"sourcesContent":["import {\n buildStoragePathData,\n buildUploadStoragePathData,\n isStoragePathWithinCollectionPrefix,\n} from './buildStoragePathData.js'\nimport { describe, expect, it } from 'vitest'\n\ndescribe('buildStoragePathData', () => {\n describe('non-composite mode (useCompositePrefixes: false)', () => {\n it('should preserve the key of a legacy document outside the collection prefix', () => {\n const result = buildStoragePathData({\n collectionPrefix: 'media/',\n docPrefix: 'media-archive',\n filename: 'test.png',\n useCompositePrefixes: false,\n })\n\n expect(result).toEqual({\n fileKey: 'media-archive/test.png',\n sanitizedCollectionPrefix: 'media',\n sanitizedDocPrefix: 'media-archive',\n sanitizedFilename: 'test.png',\n storageFilePath: 'media-archive/test.png',\n })\n })\n\n it('should fallback to collectionPrefix when docPrefix is empty', () => {\n const result = buildStoragePathData({\n collectionPrefix: 'collection',\n docPrefix: '',\n filename: 'test.png',\n useCompositePrefixes: false,\n })\n expect(result).toEqual({\n fileKey: 'collection/test.png',\n sanitizedCollectionPrefix: 'collection',\n sanitizedDocPrefix: '',\n sanitizedFilename: 'test.png',\n storageFilePath: 'collection/test.png',\n })\n })\n\n it('should fallback to collectionPrefix when docPrefix is undefined', () => {\n const result = buildStoragePathData({\n collectionPrefix: 'collection',\n filename: 'test.png',\n useCompositePrefixes: false,\n })\n expect(result).toEqual({\n fileKey: 'collection/test.png',\n sanitizedCollectionPrefix: 'collection',\n sanitizedDocPrefix: '',\n sanitizedFilename: 'test.png',\n storageFilePath: 'collection/test.png',\n })\n })\n\n it('should return only filename when both prefixes are empty', () => {\n const result = buildStoragePathData({\n filename: 'test.png',\n useCompositePrefixes: false,\n })\n expect(result).toEqual({\n fileKey: 'test.png',\n sanitizedCollectionPrefix: '',\n sanitizedDocPrefix: '',\n sanitizedFilename: 'test.png',\n storageFilePath: 'test.png',\n })\n })\n })\n\n describe('composite mode (useCompositePrefixes: true)', () => {\n it('should combine collectionPrefix and docPrefix', () => {\n const result = buildStoragePathData({\n collectionPrefix: 'collection',\n docPrefix: 'document',\n filename: 'test.png',\n useCompositePrefixes: true,\n })\n expect(result).toEqual({\n fileKey: 'collection/document/test.png',\n sanitizedCollectionPrefix: 'collection',\n sanitizedDocPrefix: 'document',\n sanitizedFilename: 'test.png',\n storageFilePath: 'collection/document/test.png',\n })\n })\n\n it('should work with only collectionPrefix', () => {\n const result = buildStoragePathData({\n collectionPrefix: 'collection',\n filename: 'test.png',\n useCompositePrefixes: true,\n })\n expect(result).toEqual({\n fileKey: 'collection/test.png',\n sanitizedCollectionPrefix: 'collection',\n sanitizedDocPrefix: '',\n sanitizedFilename: 'test.png',\n storageFilePath: 'collection/test.png',\n })\n })\n\n it('should work with only docPrefix', () => {\n const result = buildStoragePathData({\n docPrefix: 'document',\n filename: 'test.png',\n useCompositePrefixes: true,\n })\n expect(result).toEqual({\n fileKey: 'document/test.png',\n sanitizedCollectionPrefix: '',\n sanitizedDocPrefix: 'document',\n sanitizedFilename: 'test.png',\n storageFilePath: 'document/test.png',\n })\n })\n\n it('should return only filename when both prefixes are empty', () => {\n const result = buildStoragePathData({\n filename: 'test.png',\n useCompositePrefixes: true,\n })\n expect(result).toEqual({\n fileKey: 'test.png',\n sanitizedCollectionPrefix: '',\n sanitizedDocPrefix: '',\n sanitizedFilename: 'test.png',\n storageFilePath: 'test.png',\n })\n })\n })\n\n describe('collection prefix boundaries', () => {\n it('should accept a file key within a slash-terminated collection prefix', () => {\n expect(\n isStoragePathWithinCollectionPrefix({\n collectionPrefix: 'media/',\n docPrefix: 'media/documents/test.png',\n }),\n ).toBe(true)\n })\n })\n\n describe('sanitization', () => {\n it.each(['./media/./images', 'media\\\\images', '/media//images'])(\n 'should canonicalize equivalent prefix paths',\n (docPrefix) => {\n const result = buildStoragePathData({ docPrefix, filename: 'test.png' })\n\n expect(result).toEqual({\n fileKey: 'media/images/test.png',\n sanitizedCollectionPrefix: '',\n sanitizedDocPrefix: 'media/images',\n sanitizedFilename: 'test.png',\n storageFilePath: 'media/images/test.png',\n })\n },\n )\n\n it('should remove path traversal segments from collectionPrefix', () => {\n const result = buildStoragePathData({\n collectionPrefix: '../../../etc',\n filename: 'test.png',\n useCompositePrefixes: false,\n })\n expect(result).toEqual({\n fileKey: 'etc/test.png',\n sanitizedCollectionPrefix: 'etc',\n sanitizedDocPrefix: '',\n sanitizedFilename: 'test.png',\n storageFilePath: 'etc/test.png',\n })\n expect(result.fileKey).not.toContain('..')\n })\n\n it('should remove path traversal segments from docPrefix', () => {\n const result = buildStoragePathData({\n docPrefix: 'a/../../outside',\n filename: 'test.png',\n useCompositePrefixes: false,\n })\n expect(result).toEqual({\n fileKey: 'a/outside/test.png',\n sanitizedCollectionPrefix: '',\n sanitizedDocPrefix: 'a/outside',\n sanitizedFilename: 'test.png',\n storageFilePath: 'a/outside/test.png',\n })\n expect(result.fileKey).not.toContain('..')\n })\n\n it('should remove control characters from prefixes', () => {\n const result = buildStoragePathData({\n collectionPrefix: 'test\\x00\\x1fprefix',\n filename: 'test.png',\n useCompositePrefixes: false,\n })\n expect(result).toEqual({\n fileKey: 'testprefix/test.png',\n sanitizedCollectionPrefix: 'testprefix',\n sanitizedDocPrefix: '',\n sanitizedFilename: 'test.png',\n storageFilePath: 'testprefix/test.png',\n })\n expect(result.fileKey).not.toMatch(/[\\x00-\\x1f]/)\n })\n\n it('should normalize both prefixes after removing control characters in composite mode', () => {\n const result = buildStoragePathData({\n collectionPrefix: '.\\x00./collection',\n docPrefix: '.\\x00./doc',\n filename: 'test.png',\n useCompositePrefixes: true,\n })\n expect(result).toEqual({\n fileKey: 'collection/doc/test.png',\n sanitizedCollectionPrefix: 'collection',\n sanitizedDocPrefix: 'doc',\n sanitizedFilename: 'test.png',\n storageFilePath: 'collection/doc/test.png',\n })\n expect(result.fileKey).not.toContain('..')\n })\n })\n})\n\ndescribe('buildUploadStoragePathData', () => {\n it.each([\n ['invoices', false, 'media/invoices/file.png', 'media/invoices'],\n ['media/invoices', false, 'media/invoices/file.png', 'media/invoices'],\n ['', false, 'media/file.png', ''],\n ['invoices', true, 'media/invoices/file.png', 'invoices'],\n ] as const)(\n 'should persist a readable destination for %s (composite %s)',\n (docPrefix, useCompositePrefixes, fileKey, storedPrefix) => {\n const result = buildUploadStoragePathData({\n collectionPrefix: 'media',\n docPrefix,\n filename: 'file.png',\n useCompositePrefixes,\n })\n\n expect(result.storageFilePath).toBe(fileKey)\n expect(result.sanitizedDocPrefix).toBe(storedPrefix)\n expect(\n buildStoragePathData({\n collectionPrefix: 'media',\n docPrefix: result.sanitizedDocPrefix,\n filename: 'file.png',\n useCompositePrefixes,\n }).fileKey,\n ).toBe(fileKey)\n },\n )\n})\n"],"names":["buildStoragePathData","buildUploadStoragePathData","isStoragePathWithinCollectionPrefix","describe","expect","it","result","collectionPrefix","docPrefix","filename","useCompositePrefixes","toEqual","fileKey","sanitizedCollectionPrefix","sanitizedDocPrefix","sanitizedFilename","storageFilePath","toBe","each","not","toContain","toMatch","storedPrefix"],"mappings":"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"}
|
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { getFilePrefix } from './getFilePrefix.js';
|
|
3
|
-
const makeReq = ({ docs = [] } = {})=>({
|
|
4
|
-
payload: {
|
|
5
|
-
find: vi.fn().mockResolvedValue({
|
|
6
|
-
docs
|
|
7
|
-
})
|
|
8
|
-
}
|
|
9
|
-
});
|
|
10
|
-
const makeCollection = ({ hasPrefixField = false } = {})=>({
|
|
11
|
-
slug: 'media',
|
|
12
|
-
fields: hasPrefixField ? [
|
|
13
|
-
{
|
|
14
|
-
name: 'prefix',
|
|
15
|
-
type: 'text'
|
|
16
|
-
}
|
|
17
|
-
] : [],
|
|
18
|
-
upload: {}
|
|
19
|
-
});
|
|
20
|
-
const whereOf = (req)=>JSON.stringify(req.payload.find.mock.calls[0][0].where);
|
|
21
|
-
describe('getFilePrefix', ()=>{
|
|
22
|
-
it('prefers an already-authorized document over a client upload context or query prefix', async ()=>{
|
|
23
|
-
const req = makeReq({
|
|
24
|
-
docs: [
|
|
25
|
-
{
|
|
26
|
-
prefix: 'db-prefix'
|
|
27
|
-
}
|
|
28
|
-
]
|
|
29
|
-
});
|
|
30
|
-
const doc = {
|
|
31
|
-
id: 1,
|
|
32
|
-
prefix: 'invoices'
|
|
33
|
-
};
|
|
34
|
-
const result = await getFilePrefix({
|
|
35
|
-
clientUploadContext: {
|
|
36
|
-
prefix: 'reference-prefix'
|
|
37
|
-
},
|
|
38
|
-
collection: makeCollection(),
|
|
39
|
-
collectionPrefix: 'media',
|
|
40
|
-
doc,
|
|
41
|
-
filename: 'logo.png',
|
|
42
|
-
prefixQueryParam: 'query-prefix',
|
|
43
|
-
req
|
|
44
|
-
});
|
|
45
|
-
expect(result).toBe('invoices');
|
|
46
|
-
expect(req.payload.find).not.toHaveBeenCalled();
|
|
47
|
-
});
|
|
48
|
-
it('does not fall back to a lookup when the checked document has no prefix', async ()=>{
|
|
49
|
-
const req = makeReq({
|
|
50
|
-
docs: [
|
|
51
|
-
{
|
|
52
|
-
prefix: 'db-prefix'
|
|
53
|
-
}
|
|
54
|
-
]
|
|
55
|
-
});
|
|
56
|
-
const result = await getFilePrefix({
|
|
57
|
-
collection: makeCollection(),
|
|
58
|
-
collectionPrefix: 'media',
|
|
59
|
-
doc: {
|
|
60
|
-
id: 1
|
|
61
|
-
},
|
|
62
|
-
filename: 'logo.png',
|
|
63
|
-
req
|
|
64
|
-
});
|
|
65
|
-
expect(result).toBe('');
|
|
66
|
-
expect(req.payload.find).not.toHaveBeenCalled();
|
|
67
|
-
});
|
|
68
|
-
it('contains a client upload context prefix beneath the collection prefix', async ()=>{
|
|
69
|
-
const req = makeReq({
|
|
70
|
-
docs: [
|
|
71
|
-
{
|
|
72
|
-
prefix: 'invoices'
|
|
73
|
-
}
|
|
74
|
-
]
|
|
75
|
-
});
|
|
76
|
-
const result = await getFilePrefix({
|
|
77
|
-
clientUploadContext: {
|
|
78
|
-
prefix: 'invoices'
|
|
79
|
-
},
|
|
80
|
-
collection: makeCollection(),
|
|
81
|
-
collectionPrefix: 'media',
|
|
82
|
-
filename: 'file.png',
|
|
83
|
-
req
|
|
84
|
-
});
|
|
85
|
-
expect(result).toBe('media/invoices');
|
|
86
|
-
expect(req.payload.find).not.toHaveBeenCalled();
|
|
87
|
-
});
|
|
88
|
-
describe('authorized document (trusted)', ()=>{
|
|
89
|
-
it('folds the object key into the document prefix and skips the database', async ()=>{
|
|
90
|
-
const req = makeReq({
|
|
91
|
-
docs: [
|
|
92
|
-
{
|
|
93
|
-
prefix: 'db-prefix'
|
|
94
|
-
}
|
|
95
|
-
]
|
|
96
|
-
});
|
|
97
|
-
const result = await getFilePrefix({
|
|
98
|
-
collection: makeCollection(),
|
|
99
|
-
doc: {
|
|
100
|
-
id: 1,
|
|
101
|
-
_objectKey: 'abc123',
|
|
102
|
-
prefix: 'doc-prefix'
|
|
103
|
-
},
|
|
104
|
-
filename: 'logo.png',
|
|
105
|
-
req
|
|
106
|
-
});
|
|
107
|
-
expect(result).toBe('doc-prefix/abc123');
|
|
108
|
-
expect(req.payload.find).not.toHaveBeenCalled();
|
|
109
|
-
});
|
|
110
|
-
it('returns the document prefix alone when it has no object key', async ()=>{
|
|
111
|
-
const req = makeReq();
|
|
112
|
-
const result = await getFilePrefix({
|
|
113
|
-
collection: makeCollection(),
|
|
114
|
-
doc: {
|
|
115
|
-
id: 1,
|
|
116
|
-
prefix: 'doc-prefix'
|
|
117
|
-
},
|
|
118
|
-
filename: 'logo.png',
|
|
119
|
-
req
|
|
120
|
-
});
|
|
121
|
-
expect(result).toBe('doc-prefix');
|
|
122
|
-
});
|
|
123
|
-
it('sanitizes the document prefix', async ()=>{
|
|
124
|
-
const req = makeReq();
|
|
125
|
-
const result = await getFilePrefix({
|
|
126
|
-
collection: makeCollection(),
|
|
127
|
-
doc: {
|
|
128
|
-
id: 1,
|
|
129
|
-
prefix: '../secret'
|
|
130
|
-
},
|
|
131
|
-
filename: 'logo.png',
|
|
132
|
-
req
|
|
133
|
-
});
|
|
134
|
-
expect(result).toBe('secret');
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
describe('verified clientUploadContext (trusted)', ()=>{
|
|
138
|
-
it('folds the server-owned object key into the prefix and skips the database', async ()=>{
|
|
139
|
-
const req = makeReq();
|
|
140
|
-
const result = await getFilePrefix({
|
|
141
|
-
clientUploadContext: {
|
|
142
|
-
_objectKey: 'abc123',
|
|
143
|
-
prefix: 'documents'
|
|
144
|
-
},
|
|
145
|
-
collection: makeCollection(),
|
|
146
|
-
filename: 'logo.png',
|
|
147
|
-
req
|
|
148
|
-
});
|
|
149
|
-
expect(result).toBe('documents/abc123');
|
|
150
|
-
expect(req.payload.find).not.toHaveBeenCalled();
|
|
151
|
-
});
|
|
152
|
-
it('returns the prefix alone when the context has no object key', async ()=>{
|
|
153
|
-
const req = makeReq();
|
|
154
|
-
const result = await getFilePrefix({
|
|
155
|
-
clientUploadContext: {
|
|
156
|
-
prefix: 'documents'
|
|
157
|
-
},
|
|
158
|
-
collection: makeCollection(),
|
|
159
|
-
filename: 'logo.png',
|
|
160
|
-
req
|
|
161
|
-
});
|
|
162
|
-
expect(result).toBe('documents');
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
describe('access-controlled database resolution', ()=>{
|
|
166
|
-
it('does NOT short-circuit on a client-supplied prefix — the access-controlled query still runs', async ()=>{
|
|
167
|
-
const req = makeReq({
|
|
168
|
-
docs: [
|
|
169
|
-
{
|
|
170
|
-
_objectKey: 'abc123',
|
|
171
|
-
prefix: 'documents'
|
|
172
|
-
}
|
|
173
|
-
]
|
|
174
|
-
});
|
|
175
|
-
await getFilePrefix({
|
|
176
|
-
collection: makeCollection({
|
|
177
|
-
hasPrefixField: true
|
|
178
|
-
}),
|
|
179
|
-
filename: 'logo.png',
|
|
180
|
-
prefixQueryParam: 'documents',
|
|
181
|
-
req
|
|
182
|
-
});
|
|
183
|
-
// A client-supplied prefix must not bypass the access-controlled lookup.
|
|
184
|
-
expect(req.payload.find).toHaveBeenCalledOnce();
|
|
185
|
-
});
|
|
186
|
-
it('queries with access control enabled', async ()=>{
|
|
187
|
-
const req = makeReq({
|
|
188
|
-
docs: [
|
|
189
|
-
{
|
|
190
|
-
prefix: 'db-prefix'
|
|
191
|
-
}
|
|
192
|
-
]
|
|
193
|
-
});
|
|
194
|
-
await getFilePrefix({
|
|
195
|
-
collection: makeCollection(),
|
|
196
|
-
filename: 'logo.png',
|
|
197
|
-
req
|
|
198
|
-
});
|
|
199
|
-
expect(req.payload.find).toHaveBeenCalledWith(expect.objectContaining({
|
|
200
|
-
overrideAccess: false,
|
|
201
|
-
req
|
|
202
|
-
}));
|
|
203
|
-
});
|
|
204
|
-
it('folds the stored object key into the resolved prefix', async ()=>{
|
|
205
|
-
const req = makeReq({
|
|
206
|
-
docs: [
|
|
207
|
-
{
|
|
208
|
-
_objectKey: 'abc123',
|
|
209
|
-
prefix: 'db-prefix'
|
|
210
|
-
}
|
|
211
|
-
]
|
|
212
|
-
});
|
|
213
|
-
const result = await getFilePrefix({
|
|
214
|
-
collection: makeCollection(),
|
|
215
|
-
filename: 'logo.png',
|
|
216
|
-
req
|
|
217
|
-
});
|
|
218
|
-
expect(result).toBe('db-prefix/abc123');
|
|
219
|
-
});
|
|
220
|
-
it('returns an empty string when no document is found', async ()=>{
|
|
221
|
-
const req = makeReq({
|
|
222
|
-
docs: []
|
|
223
|
-
});
|
|
224
|
-
const result = await getFilePrefix({
|
|
225
|
-
collection: makeCollection(),
|
|
226
|
-
filename: 'logo.png',
|
|
227
|
-
req
|
|
228
|
-
});
|
|
229
|
-
expect(result).toBe('');
|
|
230
|
-
});
|
|
231
|
-
it('prioritizes a verified clientUploadContext over the database query', async ()=>{
|
|
232
|
-
const req = makeReq({
|
|
233
|
-
docs: [
|
|
234
|
-
{
|
|
235
|
-
prefix: 'db-prefix'
|
|
236
|
-
}
|
|
237
|
-
]
|
|
238
|
-
});
|
|
239
|
-
const result = await getFilePrefix({
|
|
240
|
-
clientUploadContext: {
|
|
241
|
-
prefix: 'context-prefix'
|
|
242
|
-
},
|
|
243
|
-
collection: makeCollection(),
|
|
244
|
-
filename: 'logo.png',
|
|
245
|
-
req
|
|
246
|
-
});
|
|
247
|
-
expect(result).toBe('context-prefix');
|
|
248
|
-
expect(req.payload.find).not.toHaveBeenCalled();
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
describe('client prefix as a disambiguation filter only', ()=>{
|
|
252
|
-
it('adds the prefix filter when the collection persists a prefix field', async ()=>{
|
|
253
|
-
const req = makeReq({
|
|
254
|
-
docs: [
|
|
255
|
-
{
|
|
256
|
-
prefix: 'documents'
|
|
257
|
-
}
|
|
258
|
-
]
|
|
259
|
-
});
|
|
260
|
-
await getFilePrefix({
|
|
261
|
-
collection: makeCollection({
|
|
262
|
-
hasPrefixField: true
|
|
263
|
-
}),
|
|
264
|
-
filename: 'logo.png',
|
|
265
|
-
prefixQueryParam: 'documents',
|
|
266
|
-
req
|
|
267
|
-
});
|
|
268
|
-
expect(whereOf(req)).toContain('prefix');
|
|
269
|
-
});
|
|
270
|
-
it('omits the prefix filter when the collection has no prefix field (avoids querying a missing field)', async ()=>{
|
|
271
|
-
const req = makeReq({
|
|
272
|
-
docs: [
|
|
273
|
-
{
|
|
274
|
-
prefix: ''
|
|
275
|
-
}
|
|
276
|
-
]
|
|
277
|
-
});
|
|
278
|
-
await getFilePrefix({
|
|
279
|
-
collection: makeCollection({
|
|
280
|
-
hasPrefixField: false
|
|
281
|
-
}),
|
|
282
|
-
filename: 'logo.png',
|
|
283
|
-
prefixQueryParam: 'anything',
|
|
284
|
-
req
|
|
285
|
-
});
|
|
286
|
-
expect(whereOf(req)).not.toContain('prefix');
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
//# sourceMappingURL=getFilePrefix.spec.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/getFilePrefix.spec.ts"],"sourcesContent":["import type { CollectionConfig, PayloadRequest } from 'payload'\n\nimport { describe, expect, it, vi } from 'vitest'\n\nimport { getFilePrefix } from './getFilePrefix.js'\n\nconst makeReq = ({ docs = [] }: { docs?: unknown[] } = {}) =>\n ({\n payload: {\n find: vi.fn().mockResolvedValue({ docs }),\n },\n }) as unknown as PayloadRequest\n\nconst makeCollection = ({\n hasPrefixField = false,\n}: { hasPrefixField?: boolean } = {}): CollectionConfig =>\n ({\n slug: 'media',\n fields: hasPrefixField ? [{ name: 'prefix', type: 'text' }] : [],\n upload: {},\n }) as unknown as CollectionConfig\n\nconst whereOf = (req: PayloadRequest) =>\n JSON.stringify(\n (req.payload.find as unknown as { mock: { calls: [{ where: unknown }][] } }).mock.calls[0][0]\n .where,\n )\n\ndescribe('getFilePrefix', () => {\n it('prefers an already-authorized document over a client upload context or query prefix', async () => {\n const req = makeReq({ docs: [{ prefix: 'db-prefix' }] })\n const doc = { id: 1, prefix: 'invoices' }\n\n const result = await getFilePrefix({\n clientUploadContext: { prefix: 'reference-prefix' },\n collection: makeCollection(),\n collectionPrefix: 'media',\n doc,\n filename: 'logo.png',\n prefixQueryParam: 'query-prefix',\n req,\n })\n\n expect(result).toBe('invoices')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n\n it('does not fall back to a lookup when the checked document has no prefix', async () => {\n const req = makeReq({ docs: [{ prefix: 'db-prefix' }] })\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n collectionPrefix: 'media',\n doc: { id: 1 },\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n\n it('contains a client upload context prefix beneath the collection prefix', async () => {\n const req = makeReq({ docs: [{ prefix: 'invoices' }] })\n\n const result = await getFilePrefix({\n clientUploadContext: { prefix: 'invoices' },\n collection: makeCollection(),\n collectionPrefix: 'media',\n filename: 'file.png',\n req,\n })\n\n expect(result).toBe('media/invoices')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n\n describe('authorized document (trusted)', () => {\n it('folds the object key into the document prefix and skips the database', async () => {\n const req = makeReq({ docs: [{ prefix: 'db-prefix' }] })\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n doc: { id: 1, _objectKey: 'abc123', prefix: 'doc-prefix' },\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('doc-prefix/abc123')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n\n it('returns the document prefix alone when it has no object key', async () => {\n const req = makeReq()\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n doc: { id: 1, prefix: 'doc-prefix' },\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('doc-prefix')\n })\n\n it('sanitizes the document prefix', async () => {\n const req = makeReq()\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n doc: { id: 1, prefix: '../secret' },\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('secret')\n })\n })\n\n describe('verified clientUploadContext (trusted)', () => {\n it('folds the server-owned object key into the prefix and skips the database', async () => {\n const req = makeReq()\n\n const result = await getFilePrefix({\n clientUploadContext: { _objectKey: 'abc123', prefix: 'documents' },\n collection: makeCollection(),\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('documents/abc123')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n\n it('returns the prefix alone when the context has no object key', async () => {\n const req = makeReq()\n\n const result = await getFilePrefix({\n clientUploadContext: { prefix: 'documents' },\n collection: makeCollection(),\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('documents')\n })\n })\n\n describe('access-controlled database resolution', () => {\n it('does NOT short-circuit on a client-supplied prefix — the access-controlled query still runs', async () => {\n const req = makeReq({ docs: [{ _objectKey: 'abc123', prefix: 'documents' }] })\n\n await getFilePrefix({\n collection: makeCollection({ hasPrefixField: true }),\n filename: 'logo.png',\n prefixQueryParam: 'documents',\n req,\n })\n\n // A client-supplied prefix must not bypass the access-controlled lookup.\n expect(req.payload.find).toHaveBeenCalledOnce()\n })\n\n it('queries with access control enabled', async () => {\n const req = makeReq({ docs: [{ prefix: 'db-prefix' }] })\n\n await getFilePrefix({\n collection: makeCollection(),\n filename: 'logo.png',\n req,\n })\n\n expect(req.payload.find).toHaveBeenCalledWith(\n expect.objectContaining({ overrideAccess: false, req }),\n )\n })\n\n it('folds the stored object key into the resolved prefix', async () => {\n const req = makeReq({ docs: [{ _objectKey: 'abc123', prefix: 'db-prefix' }] })\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('db-prefix/abc123')\n })\n\n it('returns an empty string when no document is found', async () => {\n const req = makeReq({ docs: [] })\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('')\n })\n\n it('prioritizes a verified clientUploadContext over the database query', async () => {\n const req = makeReq({ docs: [{ prefix: 'db-prefix' }] })\n\n const result = await getFilePrefix({\n clientUploadContext: { prefix: 'context-prefix' },\n collection: makeCollection(),\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('context-prefix')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n })\n\n describe('client prefix as a disambiguation filter only', () => {\n it('adds the prefix filter when the collection persists a prefix field', async () => {\n const req = makeReq({ docs: [{ prefix: 'documents' }] })\n\n await getFilePrefix({\n collection: makeCollection({ hasPrefixField: true }),\n filename: 'logo.png',\n prefixQueryParam: 'documents',\n req,\n })\n\n expect(whereOf(req)).toContain('prefix')\n })\n\n it('omits the prefix filter when the collection has no prefix field (avoids querying a missing field)', async () => {\n const req = makeReq({ docs: [{ prefix: '' }] })\n\n await getFilePrefix({\n collection: makeCollection({ hasPrefixField: false }),\n filename: 'logo.png',\n prefixQueryParam: 'anything',\n req,\n })\n\n expect(whereOf(req)).not.toContain('prefix')\n })\n })\n})\n"],"names":["describe","expect","it","vi","getFilePrefix","makeReq","docs","payload","find","fn","mockResolvedValue","makeCollection","hasPrefixField","slug","fields","name","type","upload","whereOf","req","JSON","stringify","mock","calls","where","prefix","doc","id","result","clientUploadContext","collection","collectionPrefix","filename","prefixQueryParam","toBe","not","toHaveBeenCalled","_objectKey","toHaveBeenCalledOnce","toHaveBeenCalledWith","objectContaining","overrideAccess","toContain"],"mappings":"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"}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { Config, PayloadHandler } from 'payload';
|
|
2
|
-
export declare const initClientUploads: <ExtraProps extends Record<string, unknown>, T>({ clientHandler, collections, config, enabled, extraClientHandlerProps, serverHandler, serverHandlerPath, }: {
|
|
3
|
-
/** Path to clientHandler component */
|
|
4
|
-
clientHandler: string;
|
|
5
|
-
collections: Record<string, T>;
|
|
6
|
-
config: Config;
|
|
7
|
-
enabled: boolean;
|
|
8
|
-
/** extra props to pass to the client handler */
|
|
9
|
-
extraClientHandlerProps?: (collection: T) => ExtraProps;
|
|
10
|
-
serverHandler: PayloadHandler;
|
|
11
|
-
serverHandlerPath: string;
|
|
12
|
-
}) => void;
|
|
13
|
-
//# sourceMappingURL=initClientUploads.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"initClientUploads.d.ts","sourceRoot":"","sources":["../../src/utilities/initClientUploads.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAErD,eAAO,MAAM,iBAAiB,GAAI,UAAU,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,+GAQ5E;IACD,sCAAsC;IACtC,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;IAChB,gDAAgD;IAChD,uBAAuB,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,KAAK,UAAU,CAAA;IACvD,aAAa,EAAE,cAAc,CAAA;IAC7B,iBAAiB,EAAE,MAAM,CAAA;CAC1B,SAkFA,CAAA"}
|