Skip to content

Commit 9089d32

Browse files
fix(plugin-cloud-storage): fixes an issue with generateFileURL not being respected (#15565)
Fixes #15554 When `generateFileURL` is defined at the collection level, the beforeChange hook was not using the collection level `generateFileURL` but instead would fallback to `adapter.generateURL`. The user did not have an endpoint set on their cloud storage plugin, so this resulted in the url being generated and stored with a prefix of `undefined/`.
1 parent 2b3061a commit 9089d32

8 files changed

Lines changed: 159 additions & 42 deletions

File tree

packages/plugin-cloud-storage/src/fields/getFields.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,12 @@ export const getFields = ({
7575
...(existingURLField?.hooks?.afterRead || []),
7676
],
7777
beforeChange: [
78-
getBeforeChangeHook({ adapter, collection, disablePayloadAccessControl }),
78+
getBeforeChangeHook({
79+
adapter,
80+
collection,
81+
disablePayloadAccessControl,
82+
generateFileURL,
83+
}),
7984
...(existingURLField?.hooks?.beforeChange || []),
8085
],
8186
},
@@ -143,6 +148,7 @@ export const getFields = ({
143148
adapter,
144149
collection,
145150
disablePayloadAccessControl,
151+
generateFileURL,
146152
size,
147153
}),
148154
...((typeof existingSizeURLField === 'object' &&

packages/plugin-cloud-storage/src/hooks/afterRead.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,21 @@ export const getAfterReadHook =
1818
let url = value
1919

2020
if (disablePayloadAccessControl && filename) {
21-
url = await adapter.generateURL?.({
22-
collection,
23-
data,
24-
filename,
25-
prefix,
26-
})
27-
}
28-
29-
if (generateFileURL) {
30-
url = await generateFileURL({
31-
collection,
32-
filename,
33-
prefix,
34-
size,
35-
})
21+
if (generateFileURL) {
22+
url = await generateFileURL({
23+
collection,
24+
filename,
25+
prefix,
26+
size,
27+
})
28+
} else if (adapter.generateURL) {
29+
url = await adapter.generateURL({
30+
collection,
31+
data,
32+
filename,
33+
prefix,
34+
})
35+
}
3636
}
3737

3838
return url
Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,47 @@
11
import type { CollectionConfig, FieldHook, ImageSize } from 'payload'
22

3-
import type { GeneratedAdapter } from '../types.js'
3+
import type { GeneratedAdapter, GenerateFileURL } from '../types.js'
44

55
interface Args {
66
adapter: GeneratedAdapter
77
collection: CollectionConfig
88
disablePayloadAccessControl?: boolean
9+
generateFileURL?: GenerateFileURL
910
size?: ImageSize
1011
}
1112

1213
export const getBeforeChangeHook =
13-
({ adapter, collection, disablePayloadAccessControl, size }: Args): FieldHook =>
14+
({ adapter, collection, disablePayloadAccessControl, generateFileURL, size }: Args): FieldHook =>
1415
async ({ data, originalDoc, value }) => {
15-
// Only handle the disablePayloadAccessControl: true case here
16-
// When false, let the core beforeChange hook from getBaseFields handle it
1716
if (!disablePayloadAccessControl) {
1817
return value
18+
} else {
19+
const newFilename = size ? data?.sizes?.[size.name]?.filename : data?.filename
20+
const originalFilename = size
21+
? originalDoc?.sizes?.[size.name]?.filename
22+
: originalDoc?.filename
23+
const filename = newFilename || originalFilename
24+
const prefix = data?.prefix
25+
let url = value
26+
27+
// Store the full URL in the database so files can be accessed directly
28+
// from the storage provider without going through Payload's API
29+
if (generateFileURL) {
30+
url = await generateFileURL({
31+
collection,
32+
filename,
33+
prefix,
34+
size,
35+
})
36+
} else if (adapter.generateURL) {
37+
url = await adapter.generateURL({
38+
collection,
39+
data: data || originalDoc,
40+
filename,
41+
prefix,
42+
})
43+
}
44+
45+
return url
1946
}
20-
21-
const filename = size ? data?.sizes?.[size.name]?.filename : data?.filename
22-
23-
if (!filename) {
24-
return value
25-
}
26-
27-
const prefix = data?.prefix
28-
29-
// Store the full URL in the database so files can be accessed directly
30-
// from the storage provider without going through Payload's API
31-
if (adapter.generateURL) {
32-
return await adapter.generateURL({
33-
collection,
34-
data: data || originalDoc,
35-
filename,
36-
prefix,
37-
})
38-
}
39-
40-
return value
4147
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
export const MediaWithCustomURL: CollectionConfig = {
4+
slug: 'media-with-custom-url',
5+
upload: {
6+
disableLocalStorage: true,
7+
},
8+
fields: [],
9+
}

test/plugin-cloud-storage/config.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { S3StorageOptions } from '@payloadcms/storage-s3'
12
import type { Plugin } from 'payload'
23

34
import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'
@@ -11,12 +12,14 @@ import path from 'path'
1112
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
1213
import { devUser } from '../credentials.js'
1314
import { Media } from './collections/Media.js'
15+
import { MediaWithCustomURL } from './collections/MediaWithCustomURL.js'
1416
import { MediaWithPrefix } from './collections/MediaWithPrefix.js'
1517
import { RestrictedMedia } from './collections/RestrictedMedia.js'
1618
import { TestMetadata } from './collections/TestMetadata.js'
1719
import { Users } from './collections/Users.js'
1820
import {
1921
mediaSlug,
22+
mediaWithCustomURLSlug,
2023
mediaWithPrefixSlug,
2124
prefix,
2225
restrictedMediaSlug,
@@ -83,6 +86,14 @@ if (
8386
[mediaWithPrefixSlug]: {
8487
prefix,
8588
},
89+
[mediaWithCustomURLSlug]: {
90+
prefix,
91+
disablePayloadAccessControl: true,
92+
generateFileURL: ({ filename, prefix }) =>
93+
filename
94+
? `https://test-cdn.example.com/${prefix}/${encodeURIComponent(filename)}`
95+
: null,
96+
} as S3StorageOptions['collections'][keyof S3StorageOptions['collections']],
8697
[restrictedMediaSlug]: true,
8798
},
8899
bucket: process.env.S3_BUCKET ?? '',
@@ -150,7 +161,7 @@ export default buildConfigWithDefaults({
150161
baseDir: path.resolve(dirname),
151162
},
152163
},
153-
collections: [Media, MediaWithPrefix, RestrictedMedia, TestMetadata, Users],
164+
collections: [Media, MediaWithCustomURL, MediaWithPrefix, RestrictedMedia, TestMetadata, Users],
154165
onInit: async (payload) => {
155166
/*const client = new AWS.S3({
156167
endpoint: process.env.S3_ENDPOINT,

test/plugin-cloud-storage/int.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { Config } from './payload-types.js'
1212
import { initPayloadInt } from '../__helpers/shared/initPayloadInt.js'
1313
import {
1414
mediaSlug,
15+
mediaWithCustomURLSlug,
1516
mediaWithPrefixSlug,
1617
prefix,
1718
restrictedMediaSlug,
@@ -226,6 +227,46 @@ describe('@payloadcms/plugin-cloud-storage', () => {
226227
expect(dbRecord.url).toEqual(`/api/${mediaWithPrefixSlug}/file/${upload.filename}`)
227228
expect((rawDbData as any)?.sizes).toBeFalsy()
228229
})
230+
231+
it('should use custom generateFileURL in beforeChange when disablePayloadAccessControl is true', async () => {
232+
// This test verifies that custom generateFileURL is used in beforeChange hook
233+
// when disablePayloadAccessControl: true, preventing "undefined" from appearing in URLs
234+
const upload = await payload.create({
235+
collection: mediaWithCustomURLSlug,
236+
data: {},
237+
filePath: path.resolve(dirname, '../uploads/image.png'),
238+
})
239+
240+
expect(upload.id).toBeTruthy()
241+
expect(upload.filename).toBeTruthy()
242+
243+
// Get the raw DB data
244+
const rawDbData = await payload.db.findOne({
245+
collection: mediaWithCustomURLSlug,
246+
where: { id: { equals: upload.id } },
247+
})
248+
249+
const dbRecord = rawDbData as unknown as {
250+
filename: string
251+
prefix: string
252+
url: string
253+
}
254+
255+
// The custom generateFileURL should be used, resulting in test-cdn.example.com URL
256+
expect(dbRecord.url).toContain('test-cdn.example.com')
257+
expect(dbRecord.url).toContain(prefix)
258+
expect(dbRecord.url).toContain(encodeURIComponent(dbRecord.filename))
259+
expect(dbRecord.url).toMatch(/^https:\/\//)
260+
261+
// Verify the API response also returns the custom URL
262+
const apiResponse = await payload.findByID({
263+
collection: mediaWithCustomURLSlug,
264+
id: upload.id,
265+
})
266+
267+
expect(apiResponse.url).toContain('test-cdn.example.com')
268+
expect(apiResponse.url).toContain(prefix)
269+
})
229270
})
230271
})
231272

test/plugin-cloud-storage/payload-types.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export interface Config {
6868
blocks: {};
6969
collections: {
7070
media: Media;
71+
'media-with-custom-url': MediaWithCustomUrl;
7172
'media-with-prefix': MediaWithPrefix;
7273
'restricted-media': RestrictedMedia;
7374
'test-metadata': TestMetadatum;
@@ -80,6 +81,7 @@ export interface Config {
8081
collectionsJoins: {};
8182
collectionsSelect: {
8283
media: MediaSelect<false> | MediaSelect<true>;
84+
'media-with-custom-url': MediaWithCustomUrlSelect<false> | MediaWithCustomUrlSelect<true>;
8385
'media-with-prefix': MediaWithPrefixSelect<false> | MediaWithPrefixSelect<true>;
8486
'restricted-media': RestrictedMediaSelect<false> | RestrictedMediaSelect<true>;
8587
'test-metadata': TestMetadataSelect<false> | TestMetadataSelect<true>;
@@ -157,6 +159,25 @@ export interface Media {
157159
};
158160
};
159161
}
162+
/**
163+
* This interface was referenced by `Config`'s JSON-Schema
164+
* via the `definition` "media-with-custom-url".
165+
*/
166+
export interface MediaWithCustomUrl {
167+
id: string;
168+
prefix?: string | null;
169+
updatedAt: string;
170+
createdAt: string;
171+
url?: string | null;
172+
thumbnailURL?: string | null;
173+
filename?: string | null;
174+
mimeType?: string | null;
175+
filesize?: number | null;
176+
width?: number | null;
177+
height?: number | null;
178+
focalX?: number | null;
179+
focalY?: number | null;
180+
}
160181
/**
161182
* This interface was referenced by `Config`'s JSON-Schema
162183
* via the `definition` "media-with-prefix".
@@ -280,6 +301,10 @@ export interface PayloadLockedDocument {
280301
relationTo: 'media';
281302
value: string | Media;
282303
} | null)
304+
| ({
305+
relationTo: 'media-with-custom-url';
306+
value: string | MediaWithCustomUrl;
307+
} | null)
283308
| ({
284309
relationTo: 'media-with-prefix';
285310
value: string | MediaWithPrefix;
@@ -380,6 +405,24 @@ export interface MediaSelect<T extends boolean = true> {
380405
};
381406
};
382407
}
408+
/**
409+
* This interface was referenced by `Config`'s JSON-Schema
410+
* via the `definition` "media-with-custom-url_select".
411+
*/
412+
export interface MediaWithCustomUrlSelect<T extends boolean = true> {
413+
prefix?: T;
414+
updatedAt?: T;
415+
createdAt?: T;
416+
url?: T;
417+
thumbnailURL?: T;
418+
filename?: T;
419+
mimeType?: T;
420+
filesize?: T;
421+
width?: T;
422+
height?: T;
423+
focalX?: T;
424+
focalY?: T;
425+
}
383426
/**
384427
* This interface was referenced by `Config`'s JSON-Schema
385428
* via the `definition` "media-with-prefix_select".
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const mediaSlug = 'media'
22
export const mediaWithPrefixSlug = 'media-with-prefix'
3+
export const mediaWithCustomURLSlug = 'media-with-custom-url'
34
export const restrictedMediaSlug = 'restricted-media'
45
export const testMetadataSlug = 'test-metadata'
56
export const prefix = 'test-prefix'

0 commit comments

Comments
 (0)