@payloadcms/plugin-cloud-storage 4.0.0-canary.32 → 4.0.0-canary.34

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.
Files changed (75) hide show
  1. package/README.md +2 -0
  2. package/dist/exports/client.d.ts +1 -1
  3. package/dist/exports/client.d.ts.map +1 -1
  4. package/dist/exports/client.js +1 -1
  5. package/dist/exports/client.js.map +1 -1
  6. package/dist/exports/utilities.d.ts +1 -1
  7. package/dist/exports/utilities.d.ts.map +1 -1
  8. package/dist/exports/utilities.js +1 -1
  9. package/dist/exports/utilities.js.map +1 -1
  10. package/dist/fields/getFields.d.ts.map +1 -1
  11. package/dist/fields/getFields.js +25 -1
  12. package/dist/fields/getFields.js.map +1 -1
  13. package/dist/hooks/afterChange.d.ts +3 -1
  14. package/dist/hooks/afterChange.d.ts.map +1 -1
  15. package/dist/hooks/afterChange.js +51 -8
  16. package/dist/hooks/afterChange.js.map +1 -1
  17. package/dist/hooks/afterChange.spec.js +95 -0
  18. package/dist/hooks/afterChange.spec.js.map +1 -0
  19. package/dist/hooks/afterDelete.d.ts +3 -1
  20. package/dist/hooks/afterDelete.d.ts.map +1 -1
  21. package/dist/hooks/afterDelete.js +16 -2
  22. package/dist/hooks/afterDelete.js.map +1 -1
  23. package/dist/hooks/afterRead.d.ts.map +1 -1
  24. package/dist/hooks/afterRead.js +15 -2
  25. package/dist/hooks/afterRead.js.map +1 -1
  26. package/dist/hooks/beforeChange.d.ts.map +1 -1
  27. package/dist/hooks/beforeChange.js +12 -1
  28. package/dist/hooks/beforeChange.js.map +1 -1
  29. package/dist/hooks/normalizeUploadPrefix.d.ts +11 -0
  30. package/dist/hooks/normalizeUploadPrefix.d.ts.map +1 -0
  31. package/dist/hooks/normalizeUploadPrefix.js +42 -0
  32. package/dist/hooks/normalizeUploadPrefix.js.map +1 -0
  33. package/dist/hooks/normalizeUploadPrefix.spec.js +154 -0
  34. package/dist/hooks/normalizeUploadPrefix.spec.js.map +1 -0
  35. package/dist/plugin.d.ts.map +1 -1
  36. package/dist/plugin.js +14 -6
  37. package/dist/plugin.js.map +1 -1
  38. package/dist/plugin.spec.js +166 -0
  39. package/dist/plugin.spec.js.map +1 -0
  40. package/dist/types.d.ts +19 -1
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/types.js.map +1 -1
  43. package/dist/utilities/buildPrefixWithObjectKey.d.ts +8 -0
  44. package/dist/utilities/buildPrefixWithObjectKey.d.ts.map +1 -0
  45. package/dist/utilities/buildPrefixWithObjectKey.js +13 -0
  46. package/dist/utilities/buildPrefixWithObjectKey.js.map +1 -0
  47. package/dist/utilities/buildStoragePathData.d.ts +49 -0
  48. package/dist/utilities/buildStoragePathData.d.ts.map +1 -0
  49. package/dist/utilities/buildStoragePathData.js +67 -0
  50. package/dist/utilities/buildStoragePathData.js.map +1 -0
  51. package/dist/utilities/buildStoragePathData.spec.js +241 -0
  52. package/dist/utilities/buildStoragePathData.spec.js.map +1 -0
  53. package/dist/utilities/getFilePrefix.d.ts +18 -10
  54. package/dist/utilities/getFilePrefix.d.ts.map +1 -1
  55. package/dist/utilities/getFilePrefix.js +69 -29
  56. package/dist/utilities/getFilePrefix.js.map +1 -1
  57. package/dist/utilities/getFilePrefix.spec.js +174 -31
  58. package/dist/utilities/getFilePrefix.spec.js.map +1 -1
  59. package/dist/utilities/resolveSignedURLKey.d.ts +12 -1
  60. package/dist/utilities/resolveSignedURLKey.d.ts.map +1 -1
  61. package/dist/utilities/resolveSignedURLKey.js +42 -8
  62. package/dist/utilities/resolveSignedURLKey.js.map +1 -1
  63. package/dist/utilities/resolveSignedURLKey.spec.js +74 -0
  64. package/dist/utilities/resolveSignedURLKey.spec.js.map +1 -0
  65. package/dist/utilities/sanitizePrefix.d.ts +1 -9
  66. package/dist/utilities/sanitizePrefix.d.ts.map +1 -1
  67. package/dist/utilities/sanitizePrefix.js +1 -21
  68. package/dist/utilities/sanitizePrefix.js.map +1 -1
  69. package/package.json +4 -4
  70. package/dist/utilities/getFileKey.d.ts +0 -22
  71. package/dist/utilities/getFileKey.d.ts.map +0 -1
  72. package/dist/utilities/getFileKey.js +0 -23
  73. package/dist/utilities/getFileKey.js.map +0 -1
  74. package/dist/utilities/getFileKey.spec.js +0 -174
  75. package/dist/utilities/getFileKey.spec.js.map +0 -1
@@ -1,8 +1,19 @@
1
+ import { sanitizePrefix } from '../utilities/sanitizePrefix.js';
2
+ // The object's folder: semantic prefix + `_objectKey` segment.
3
+ const getObjectFolder = (data, originalDoc)=>{
4
+ const source = data ?? originalDoc ?? {};
5
+ const safePrefix = sanitizePrefix(typeof source.prefix === 'string' ? source.prefix : '');
6
+ const safeObjectKey = sanitizePrefix(typeof source._objectKey === 'string' ? source._objectKey : '');
7
+ if (safePrefix && safeObjectKey) {
8
+ return `${safePrefix}/${safeObjectKey}`;
9
+ }
10
+ return safePrefix || safeObjectKey;
11
+ };
1
12
  export const getBeforeChangeHook = ({ adapter, collection, disablePayloadAccessControl, generateFileURL, size })=>async ({ data, originalDoc, value })=>{
2
13
  const newFilename = size ? data?.sizes?.[size.name]?.filename : data?.filename;
3
14
  const originalFilename = size ? originalDoc?.sizes?.[size.name]?.filename : originalDoc?.filename;
4
15
  const filename = newFilename || originalFilename;
5
- const prefix = data?.prefix;
16
+ const prefix = getObjectFolder(data, originalDoc);
6
17
  let url = value;
7
18
  if (generateFileURL && filename) {
8
19
  url = await generateFileURL({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/beforeChange.ts"],"sourcesContent":["import type { CollectionConfig, FieldHook, ImageSize } from 'payload'\n\nimport type { GeneratedAdapter, GenerateFileURL } from '../types.js'\n\ninterface Args {\n adapter: GeneratedAdapter\n collection: CollectionConfig\n disablePayloadAccessControl?: boolean\n generateFileURL?: GenerateFileURL\n size?: ImageSize\n}\n\nexport const getBeforeChangeHook =\n ({ adapter, collection, disablePayloadAccessControl, generateFileURL, size }: Args): FieldHook =>\n async ({ data, originalDoc, value }) => {\n const newFilename = size ? data?.sizes?.[size.name]?.filename : data?.filename\n const originalFilename = size\n ? originalDoc?.sizes?.[size.name]?.filename\n : originalDoc?.filename\n const filename = newFilename || originalFilename\n const prefix = data?.prefix\n let url = value\n\n if (generateFileURL && filename) {\n url = await generateFileURL({\n collection,\n filename,\n prefix,\n size,\n })\n } else if (disablePayloadAccessControl && filename && adapter.generateURL) {\n url = await adapter.generateURL({\n collection,\n data: data || originalDoc,\n filename,\n prefix,\n })\n }\n\n return url\n }\n"],"names":["getBeforeChangeHook","adapter","collection","disablePayloadAccessControl","generateFileURL","size","data","originalDoc","value","newFilename","sizes","name","filename","originalFilename","prefix","url","generateURL"],"mappings":"AAYA,OAAO,MAAMA,sBACX,CAAC,EAAEC,OAAO,EAAEC,UAAU,EAAEC,2BAA2B,EAAEC,eAAe,EAAEC,IAAI,EAAQ,GAClF,OAAO,EAAEC,IAAI,EAAEC,WAAW,EAAEC,KAAK,EAAE;QACjC,MAAMC,cAAcJ,OAAOC,MAAMI,OAAO,CAACL,KAAKM,IAAI,CAAC,EAAEC,WAAWN,MAAMM;QACtE,MAAMC,mBAAmBR,OACrBE,aAAaG,OAAO,CAACL,KAAKM,IAAI,CAAC,EAAEC,WACjCL,aAAaK;QACjB,MAAMA,WAAWH,eAAeI;QAChC,MAAMC,SAASR,MAAMQ;QACrB,IAAIC,MAAMP;QAEV,IAAIJ,mBAAmBQ,UAAU;YAC/BG,MAAM,MAAMX,gBAAgB;gBAC1BF;gBACAU;gBACAE;gBACAT;YACF;QACF,OAAO,IAAIF,+BAA+BS,YAAYX,QAAQe,WAAW,EAAE;YACzED,MAAM,MAAMd,QAAQe,WAAW,CAAC;gBAC9Bd;gBACAI,MAAMA,QAAQC;gBACdK;gBACAE;YACF;QACF;QAEA,OAAOC;IACT,EAAC"}
1
+ {"version":3,"sources":["../../src/hooks/beforeChange.ts"],"sourcesContent":["import type { CollectionConfig, FieldHook, ImageSize } from 'payload'\n\nimport type { GeneratedAdapter, GenerateFileURL } from '../types.js'\n\nimport { sanitizePrefix } from '../utilities/sanitizePrefix.js'\n\ninterface Args {\n adapter: GeneratedAdapter\n collection: CollectionConfig\n disablePayloadAccessControl?: boolean\n generateFileURL?: GenerateFileURL\n size?: ImageSize\n}\n\n// The object's folder: semantic prefix + `_objectKey` segment.\nconst getObjectFolder = (data: unknown, originalDoc: unknown): string => {\n const source = (data ?? originalDoc ?? {}) as Record<string, unknown>\n const safePrefix = sanitizePrefix(typeof source.prefix === 'string' ? source.prefix : '')\n const safeObjectKey = sanitizePrefix(\n typeof source._objectKey === 'string' ? source._objectKey : '',\n )\n\n if (safePrefix && safeObjectKey) {\n return `${safePrefix}/${safeObjectKey}`\n }\n\n return safePrefix || safeObjectKey\n}\n\nexport const getBeforeChangeHook =\n ({ adapter, collection, disablePayloadAccessControl, generateFileURL, size }: Args): FieldHook =>\n async ({ data, originalDoc, value }) => {\n const newFilename = size ? data?.sizes?.[size.name]?.filename : data?.filename\n const originalFilename = size\n ? originalDoc?.sizes?.[size.name]?.filename\n : originalDoc?.filename\n const filename = newFilename || originalFilename\n const prefix = getObjectFolder(data, originalDoc)\n let url = value\n\n if (generateFileURL && filename) {\n url = await generateFileURL({\n collection,\n filename,\n prefix,\n size,\n })\n } else if (disablePayloadAccessControl && filename && adapter.generateURL) {\n url = await adapter.generateURL({\n collection,\n data: data || originalDoc,\n filename,\n prefix,\n })\n }\n\n return url\n }\n"],"names":["sanitizePrefix","getObjectFolder","data","originalDoc","source","safePrefix","prefix","safeObjectKey","_objectKey","getBeforeChangeHook","adapter","collection","disablePayloadAccessControl","generateFileURL","size","value","newFilename","sizes","name","filename","originalFilename","url","generateURL"],"mappings":"AAIA,SAASA,cAAc,QAAQ,iCAAgC;AAU/D,+DAA+D;AAC/D,MAAMC,kBAAkB,CAACC,MAAeC;IACtC,MAAMC,SAAUF,QAAQC,eAAe,CAAC;IACxC,MAAME,aAAaL,eAAe,OAAOI,OAAOE,MAAM,KAAK,WAAWF,OAAOE,MAAM,GAAG;IACtF,MAAMC,gBAAgBP,eACpB,OAAOI,OAAOI,UAAU,KAAK,WAAWJ,OAAOI,UAAU,GAAG;IAG9D,IAAIH,cAAcE,eAAe;QAC/B,OAAO,GAAGF,WAAW,CAAC,EAAEE,eAAe;IACzC;IAEA,OAAOF,cAAcE;AACvB;AAEA,OAAO,MAAME,sBACX,CAAC,EAAEC,OAAO,EAAEC,UAAU,EAAEC,2BAA2B,EAAEC,eAAe,EAAEC,IAAI,EAAQ,GAClF,OAAO,EAAEZ,IAAI,EAAEC,WAAW,EAAEY,KAAK,EAAE;QACjC,MAAMC,cAAcF,OAAOZ,MAAMe,OAAO,CAACH,KAAKI,IAAI,CAAC,EAAEC,WAAWjB,MAAMiB;QACtE,MAAMC,mBAAmBN,OACrBX,aAAac,OAAO,CAACH,KAAKI,IAAI,CAAC,EAAEC,WACjChB,aAAagB;QACjB,MAAMA,WAAWH,eAAeI;QAChC,MAAMd,SAASL,gBAAgBC,MAAMC;QACrC,IAAIkB,MAAMN;QAEV,IAAIF,mBAAmBM,UAAU;YAC/BE,MAAM,MAAMR,gBAAgB;gBAC1BF;gBACAQ;gBACAb;gBACAQ;YACF;QACF,OAAO,IAAIF,+BAA+BO,YAAYT,QAAQY,WAAW,EAAE;YACzED,MAAM,MAAMX,QAAQY,WAAW,CAAC;gBAC9BX;gBACAT,MAAMA,QAAQC;gBACdgB;gBACAb;YACF;QACF;QAEA,OAAOe;IACT,EAAC"}
@@ -0,0 +1,11 @@
1
+ import type { CollectionBeforeChangeHook, FieldHook } from 'payload';
2
+ type Args = {
3
+ collectionPrefix?: string;
4
+ useCompositePrefixes?: boolean;
5
+ };
6
+ /** Persist new upload destinations without reinterpreting metadata-only updates. */
7
+ export declare const getNormalizeUploadPrefixHook: ({ collectionPrefix, useCompositePrefixes }: Args) => CollectionBeforeChangeHook;
8
+ /** Run last on the prefix field so user hooks cannot leave stale storage coordinates. */
9
+ export declare const getNormalizeUploadPrefixFieldHook: ({ collectionPrefix, useCompositePrefixes }: Args) => FieldHook;
10
+ export {};
11
+ //# sourceMappingURL=normalizeUploadPrefix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizeUploadPrefix.d.ts","sourceRoot":"","sources":["../../src/hooks/normalizeUploadPrefix.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAIpE,KAAK,IAAI,GAAG;IACV,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAA;AAiBD,oFAAoF;AACpF,eAAO,MAAM,4BAA4B,+CACM,IAAI,KAAG,0BAiBnD,CAAA;AAEH,yFAAyF;AACzF,eAAO,MAAM,iCAAiC,+CACC,IAAI,KAAG,SAenD,CAAA"}
@@ -0,0 +1,42 @@
1
+ import { buildUploadPrefix } from '../utilities/buildStoragePathData.js';
2
+ /**
3
+ * A prefix the caller did not supply, or supplied unchanged, is left as stored so
4
+ * pre-upgrade files keep resolving to their original object keys. Anything the caller
5
+ * chooses is contained, because `prefix` is writable through the REST, GraphQL and
6
+ * Local APIs and the read path trusts whatever is persisted.
7
+ */ const isPrefixSupplied = ({ storedPrefix, value })=>typeof value === 'string' && value !== (typeof storedPrefix === 'string' ? storedPrefix : '');
8
+ /** Persist new upload destinations without reinterpreting metadata-only updates. */ export const getNormalizeUploadPrefixHook = ({ collectionPrefix, useCompositePrefixes })=>({ data, originalDoc, req })=>{
9
+ if (req.context?.skipCloudStorage) {
10
+ return data;
11
+ }
12
+ if (!req.file && !isPrefixSupplied({
13
+ storedPrefix: originalDoc?.prefix,
14
+ value: data.prefix
15
+ })) {
16
+ return data;
17
+ }
18
+ data.prefix = buildUploadPrefix({
19
+ collectionPrefix,
20
+ docPrefix: typeof data.prefix === 'string' ? data.prefix : '',
21
+ useCompositePrefixes
22
+ }).sanitizedDocPrefix;
23
+ return data;
24
+ };
25
+ /** Run last on the prefix field so user hooks cannot leave stale storage coordinates. */ export const getNormalizeUploadPrefixFieldHook = ({ collectionPrefix, useCompositePrefixes })=>({ previousValue, req, value })=>{
26
+ if (req.context?.skipCloudStorage) {
27
+ return value;
28
+ }
29
+ if (!req.file && !isPrefixSupplied({
30
+ storedPrefix: previousValue,
31
+ value
32
+ })) {
33
+ return value;
34
+ }
35
+ return buildUploadPrefix({
36
+ collectionPrefix,
37
+ docPrefix: typeof value === 'string' ? value : '',
38
+ useCompositePrefixes
39
+ }).sanitizedDocPrefix;
40
+ };
41
+
42
+ //# sourceMappingURL=normalizeUploadPrefix.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/normalizeUploadPrefix.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook, FieldHook } from 'payload'\n\nimport { buildUploadPrefix } from '../utilities/buildStoragePathData.js'\n\ntype Args = {\n collectionPrefix?: string\n useCompositePrefixes?: boolean\n}\n\n/**\n * A prefix the caller did not supply, or supplied unchanged, is left as stored so\n * pre-upgrade files keep resolving to their original object keys. Anything the caller\n * chooses is contained, because `prefix` is writable through the REST, GraphQL and\n * Local APIs and the read path trusts whatever is persisted.\n */\nconst isPrefixSupplied = ({\n storedPrefix,\n value,\n}: {\n storedPrefix: unknown\n value: unknown\n}): boolean =>\n typeof value === 'string' && value !== (typeof storedPrefix === 'string' ? storedPrefix : '')\n\n/** Persist new upload destinations without reinterpreting metadata-only updates. */\nexport const getNormalizeUploadPrefixHook =\n ({ collectionPrefix, useCompositePrefixes }: Args): CollectionBeforeChangeHook =>\n ({ data, originalDoc, req }) => {\n if (req.context?.skipCloudStorage) {\n return data\n }\n\n if (!req.file && !isPrefixSupplied({ storedPrefix: originalDoc?.prefix, value: data.prefix })) {\n return data\n }\n\n data.prefix = buildUploadPrefix({\n collectionPrefix,\n docPrefix: typeof data.prefix === 'string' ? data.prefix : '',\n useCompositePrefixes,\n }).sanitizedDocPrefix\n\n return data\n }\n\n/** Run last on the prefix field so user hooks cannot leave stale storage coordinates. */\nexport const getNormalizeUploadPrefixFieldHook =\n ({ collectionPrefix, useCompositePrefixes }: Args): FieldHook =>\n ({ previousValue, req, value }) => {\n if (req.context?.skipCloudStorage) {\n return value\n }\n\n if (!req.file && !isPrefixSupplied({ storedPrefix: previousValue, value })) {\n return value\n }\n\n return buildUploadPrefix({\n collectionPrefix,\n docPrefix: typeof value === 'string' ? value : '',\n useCompositePrefixes,\n }).sanitizedDocPrefix\n }\n"],"names":["buildUploadPrefix","isPrefixSupplied","storedPrefix","value","getNormalizeUploadPrefixHook","collectionPrefix","useCompositePrefixes","data","originalDoc","req","context","skipCloudStorage","file","prefix","docPrefix","sanitizedDocPrefix","getNormalizeUploadPrefixFieldHook","previousValue"],"mappings":"AAEA,SAASA,iBAAiB,QAAQ,uCAAsC;AAOxE;;;;;CAKC,GACD,MAAMC,mBAAmB,CAAC,EACxBC,YAAY,EACZC,KAAK,EAIN,GACC,OAAOA,UAAU,YAAYA,UAAW,CAAA,OAAOD,iBAAiB,WAAWA,eAAe,EAAC;AAE7F,kFAAkF,GAClF,OAAO,MAAME,+BACX,CAAC,EAAEC,gBAAgB,EAAEC,oBAAoB,EAAQ,GACjD,CAAC,EAAEC,IAAI,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACzB,IAAIA,IAAIC,OAAO,EAAEC,kBAAkB;YACjC,OAAOJ;QACT;QAEA,IAAI,CAACE,IAAIG,IAAI,IAAI,CAACX,iBAAiB;YAAEC,cAAcM,aAAaK;YAAQV,OAAOI,KAAKM,MAAM;QAAC,IAAI;YAC7F,OAAON;QACT;QAEAA,KAAKM,MAAM,GAAGb,kBAAkB;YAC9BK;YACAS,WAAW,OAAOP,KAAKM,MAAM,KAAK,WAAWN,KAAKM,MAAM,GAAG;YAC3DP;QACF,GAAGS,kBAAkB;QAErB,OAAOR;IACT,EAAC;AAEH,uFAAuF,GACvF,OAAO,MAAMS,oCACX,CAAC,EAAEX,gBAAgB,EAAEC,oBAAoB,EAAQ,GACjD,CAAC,EAAEW,aAAa,EAAER,GAAG,EAAEN,KAAK,EAAE;QAC5B,IAAIM,IAAIC,OAAO,EAAEC,kBAAkB;YACjC,OAAOR;QACT;QAEA,IAAI,CAACM,IAAIG,IAAI,IAAI,CAACX,iBAAiB;YAAEC,cAAce;YAAed;QAAM,IAAI;YAC1E,OAAOA;QACT;QAEA,OAAOH,kBAAkB;YACvBK;YACAS,WAAW,OAAOX,UAAU,WAAWA,QAAQ;YAC/CG;QACF,GAAGS,kBAAkB;IACvB,EAAC"}
@@ -0,0 +1,154 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getFields } from '../fields/getFields.js';
3
+ import { getNormalizeUploadPrefixFieldHook, getNormalizeUploadPrefixHook } from './normalizeUploadPrefix.js';
4
+ const runHook = ({ data, originalDoc, req = {
5
+ context: {},
6
+ file: {}
7
+ } })=>getNormalizeUploadPrefixHook({
8
+ collectionPrefix: 'media'
9
+ })({
10
+ data,
11
+ originalDoc,
12
+ req
13
+ });
14
+ describe('new upload prefixes', ()=>{
15
+ it('should normalize after custom prefix field hooks', async ()=>{
16
+ const fields = getFields({
17
+ collection: {
18
+ fields: [
19
+ {
20
+ name: 'prefix',
21
+ type: 'text',
22
+ hooks: {
23
+ beforeChange: [
24
+ ()=>'custom'
25
+ ]
26
+ }
27
+ }
28
+ ]
29
+ },
30
+ prefix: 'media'
31
+ });
32
+ const field = fields.find((field)=>'name' in field && field.name === 'prefix');
33
+ let value = 'original';
34
+ for (const hook of field.hooks?.beforeChange || []){
35
+ value = await hook({
36
+ value,
37
+ data: {
38
+ filename: 'file.png'
39
+ },
40
+ req: {
41
+ file: {},
42
+ context: {}
43
+ }
44
+ });
45
+ }
46
+ expect(value).toBe('media/custom');
47
+ });
48
+ it('should contain a prefix supplied with a file upload', async ()=>{
49
+ const result = await runHook({
50
+ data: {
51
+ filename: 'file.png',
52
+ prefix: 'legacy'
53
+ }
54
+ });
55
+ expect(result.prefix).toBe('media/legacy');
56
+ });
57
+ it('should leave internal cloud storage writes alone', async ()=>{
58
+ const result = await runHook({
59
+ data: {
60
+ filename: 'file.png',
61
+ prefix: 'legacy'
62
+ },
63
+ req: {
64
+ context: {
65
+ skipCloudStorage: true
66
+ },
67
+ file: {}
68
+ }
69
+ });
70
+ expect(result.prefix).toBe('legacy');
71
+ });
72
+ /**
73
+ * The `prefix` field is writable through the REST/GraphQL/Local API, so a request
74
+ * carrying no file must not be able to point a document at a foreign object.
75
+ */ it('should contain a caller-supplied prefix on a metadata-only update', async ()=>{
76
+ const result = await runHook({
77
+ data: {
78
+ prefix: 'tenant-b/private'
79
+ },
80
+ originalDoc: {
81
+ prefix: 'media/legacy'
82
+ },
83
+ req: {
84
+ context: {}
85
+ }
86
+ });
87
+ expect(result.prefix).toBe('media/tenant-b/private');
88
+ });
89
+ it('should contain a caller-supplied prefix on a create without a file', async ()=>{
90
+ const result = await runHook({
91
+ data: {
92
+ prefix: 'tenant-b/private'
93
+ },
94
+ req: {
95
+ context: {}
96
+ }
97
+ });
98
+ expect(result.prefix).toBe('media/tenant-b/private');
99
+ });
100
+ it('should leave a stored prefix alone when the request omits it', async ()=>{
101
+ const result = await runHook({
102
+ data: {
103
+ filename: 'legacy.png'
104
+ },
105
+ originalDoc: {
106
+ prefix: 'legacy-invoices'
107
+ },
108
+ req: {
109
+ context: {}
110
+ }
111
+ });
112
+ expect(result.prefix).toBeUndefined();
113
+ });
114
+ it('should leave a stored prefix alone when the request repeats it unchanged', async ()=>{
115
+ const result = await runHook({
116
+ data: {
117
+ prefix: 'legacy-invoices'
118
+ },
119
+ originalDoc: {
120
+ prefix: 'legacy-invoices'
121
+ },
122
+ req: {
123
+ context: {}
124
+ }
125
+ });
126
+ expect(result.prefix).toBe('legacy-invoices');
127
+ });
128
+ describe('prefix field hook', ()=>{
129
+ const runFieldHook = ({ previousValue, req = {
130
+ context: {}
131
+ }, value })=>getNormalizeUploadPrefixFieldHook({
132
+ collectionPrefix: 'media'
133
+ })({
134
+ data: {},
135
+ previousValue,
136
+ req,
137
+ value
138
+ });
139
+ it('should contain a caller-supplied value without a file', async ()=>{
140
+ expect(await runFieldHook({
141
+ previousValue: 'media/legacy',
142
+ value: 'tenant-b/private'
143
+ })).toBe('media/tenant-b/private');
144
+ });
145
+ it('should leave an unchanged value alone', async ()=>{
146
+ expect(await runFieldHook({
147
+ previousValue: 'legacy-invoices',
148
+ value: 'legacy-invoices'
149
+ })).toBe('legacy-invoices');
150
+ });
151
+ });
152
+ });
153
+
154
+ //# sourceMappingURL=normalizeUploadPrefix.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/normalizeUploadPrefix.spec.ts"],"sourcesContent":["import type { CollectionConfig, TextField } from 'payload'\n\nimport { describe, expect, it } from 'vitest'\n\nimport { getFields } from '../fields/getFields.js'\nimport {\n getNormalizeUploadPrefixFieldHook,\n getNormalizeUploadPrefixHook,\n} from './normalizeUploadPrefix.js'\n\nconst runHook = ({\n data,\n originalDoc,\n req = { context: {}, file: {} },\n}: {\n data: Record<string, unknown>\n originalDoc?: Record<string, unknown>\n req?: Record<string, unknown>\n}) =>\n getNormalizeUploadPrefixHook({ collectionPrefix: 'media' })({\n data,\n originalDoc,\n req,\n } as never)\n\ndescribe('new upload prefixes', () => {\n it('should normalize after custom prefix field hooks', async () => {\n const fields = getFields({\n collection: {\n fields: [{ name: 'prefix', type: 'text', hooks: { beforeChange: [() => 'custom'] } }],\n } as CollectionConfig,\n prefix: 'media',\n })\n const field = fields.find((field) => 'name' in field && field.name === 'prefix') as TextField\n let value = 'original'\n\n for (const hook of field.hooks?.beforeChange || []) {\n value = await hook({\n value,\n data: { filename: 'file.png' },\n req: { file: {}, context: {} },\n } as never)\n }\n\n expect(value).toBe('media/custom')\n })\n\n it('should contain a prefix supplied with a file upload', async () => {\n const result = await runHook({ data: { filename: 'file.png', prefix: 'legacy' } })\n\n expect(result.prefix).toBe('media/legacy')\n })\n\n it('should leave internal cloud storage writes alone', async () => {\n const result = await runHook({\n data: { filename: 'file.png', prefix: 'legacy' },\n req: { context: { skipCloudStorage: true }, file: {} },\n })\n\n expect(result.prefix).toBe('legacy')\n })\n\n /**\n * The `prefix` field is writable through the REST/GraphQL/Local API, so a request\n * carrying no file must not be able to point a document at a foreign object.\n */\n it('should contain a caller-supplied prefix on a metadata-only update', async () => {\n const result = await runHook({\n data: { prefix: 'tenant-b/private' },\n originalDoc: { prefix: 'media/legacy' },\n req: { context: {} },\n })\n\n expect(result.prefix).toBe('media/tenant-b/private')\n })\n\n it('should contain a caller-supplied prefix on a create without a file', async () => {\n const result = await runHook({\n data: { prefix: 'tenant-b/private' },\n req: { context: {} },\n })\n\n expect(result.prefix).toBe('media/tenant-b/private')\n })\n\n it('should leave a stored prefix alone when the request omits it', async () => {\n const result = await runHook({\n data: { filename: 'legacy.png' },\n originalDoc: { prefix: 'legacy-invoices' },\n req: { context: {} },\n })\n\n expect(result.prefix).toBeUndefined()\n })\n\n it('should leave a stored prefix alone when the request repeats it unchanged', async () => {\n const result = await runHook({\n data: { prefix: 'legacy-invoices' },\n originalDoc: { prefix: 'legacy-invoices' },\n req: { context: {} },\n })\n\n expect(result.prefix).toBe('legacy-invoices')\n })\n\n describe('prefix field hook', () => {\n const runFieldHook = ({\n previousValue,\n req = { context: {} },\n value,\n }: {\n previousValue?: string\n req?: Record<string, unknown>\n value: unknown\n }) =>\n getNormalizeUploadPrefixFieldHook({ collectionPrefix: 'media' })({\n data: {},\n previousValue,\n req,\n value,\n } as never)\n\n it('should contain a caller-supplied value without a file', async () => {\n expect(await runFieldHook({ previousValue: 'media/legacy', value: 'tenant-b/private' })).toBe(\n 'media/tenant-b/private',\n )\n })\n\n it('should leave an unchanged value alone', async () => {\n expect(\n await runFieldHook({ previousValue: 'legacy-invoices', value: 'legacy-invoices' }),\n ).toBe('legacy-invoices')\n })\n })\n})\n"],"names":["describe","expect","it","getFields","getNormalizeUploadPrefixFieldHook","getNormalizeUploadPrefixHook","runHook","data","originalDoc","req","context","file","collectionPrefix","fields","collection","name","type","hooks","beforeChange","prefix","field","find","value","hook","filename","toBe","result","skipCloudStorage","toBeUndefined","runFieldHook","previousValue"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAErC,OAAO,KAAK,EAAsB,aAAa,EAAE,MAAM,YAAY,CAAA;AAgBnE,eAAO,MAAM,kBAAkB,kBACb,aAAa,sBACZ,MAAM,KAAG,MAoOzB,CAAA"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAErC,OAAO,KAAK,EAAsB,aAAa,EAAE,MAAM,YAAY,CAAA;AAiBnE,eAAO,MAAM,kBAAkB,kBACb,aAAa,sBACZ,MAAM,KAAG,MAiPzB,CAAA"}
package/dist/plugin.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { getFields } from './fields/getFields.js';
2
2
  import { getAfterChangeHook } from './hooks/afterChange.js';
3
3
  import { getAfterDeleteHook } from './hooks/afterDelete.js';
4
+ import { getNormalizeUploadPrefixHook } from './hooks/normalizeUploadPrefix.js';
4
5
  import { getPreserveFileDataHook } from './hooks/preserveFileData.js';
5
6
  // This plugin extends all targeted collections by offloading uploaded files
6
7
  // to cloud storage instead of solely storing files locally.
@@ -121,9 +122,6 @@ export const cloudStoragePlugin = (pluginOptions)=>(incomingConfig)=>{
121
122
  });
122
123
  }
123
124
  const getSkipSafeFetchSetting = ()=>{
124
- if (options.disablePayloadAccessControl) {
125
- return true;
126
- }
127
125
  const isBooleanTrueSkipSafeFetch = typeof existingCollection.upload === 'object' && existingCollection.upload.skipSafeFetch === true;
128
126
  const isAllowListSkipSafeFetch = typeof existingCollection.upload === 'object' && Array.isArray(existingCollection.upload.skipSafeFetch);
129
127
  if (isBooleanTrueSkipSafeFetch) {
@@ -162,18 +160,26 @@ export const cloudStoragePlugin = (pluginOptions)=>(incomingConfig)=>{
162
160
  ...existingCollection.hooks?.afterChange || [],
163
161
  getAfterChangeHook({
164
162
  adapter,
165
- collection: existingCollection
163
+ collection: existingCollection,
164
+ collectionPrefix: options.prefix,
165
+ useCompositePrefixes
166
166
  })
167
167
  ],
168
168
  afterDelete: [
169
169
  ...existingCollection.hooks?.afterDelete || [],
170
170
  getAfterDeleteHook({
171
171
  adapter,
172
- collection: existingCollection
172
+ collection: existingCollection,
173
+ collectionPrefix: options.prefix,
174
+ useCompositePrefixes
173
175
  })
174
176
  ],
175
177
  beforeChange: [
176
178
  ...existingCollection.hooks?.beforeChange || [],
179
+ getNormalizeUploadPrefixHook({
180
+ collectionPrefix: options.prefix,
181
+ useCompositePrefixes
182
+ }),
177
183
  getPreserveFileDataHook()
178
184
  ]
179
185
  },
@@ -195,7 +201,9 @@ export const cloudStoragePlugin = (pluginOptions)=>(incomingConfig)=>{
195
201
  ...config,
196
202
  collections,
197
203
  onInit: async (payload)=>{
198
- initFunctions.forEach((fn)=>fn());
204
+ // Await each init so a provisioning failure fails Payload startup
205
+ // instead of becoming an unhandled rejection.
206
+ await Promise.all(initFunctions.map((fn)=>fn()));
199
207
  if (config.onInit) {
200
208
  await config.onInit(payload);
201
209
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { Config } from 'payload'\n\nimport type { Adapter, AllowList, PluginOptions } from './types.js'\n\nimport { getFields } from './fields/getFields.js'\nimport { getAfterChangeHook } from './hooks/afterChange.js'\nimport { getAfterDeleteHook } from './hooks/afterDelete.js'\nimport { getPreserveFileDataHook } from './hooks/preserveFileData.js'\n\n// This plugin extends all targeted collections by offloading uploaded files\n// to cloud storage instead of solely storing files locally.\n\n// It is based on an adapter approach, where adapters can be written for any cloud provider.\n// Adapters are responsible for providing four actions that this plugin will use:\n// 1. handleUpload, 2. handleDelete, 3. generateURL, 4. staticHandler\n\n// Optionally, the adapter can specify any Webpack config overrides if they are necessary.\n\nexport const cloudStoragePlugin =\n (pluginOptions: PluginOptions) =>\n (incomingConfig: Config): Config => {\n const {\n alwaysInsertFields,\n collections: allCollectionOptions,\n enabled,\n useCompositePrefixes,\n } = pluginOptions\n const config = { ...incomingConfig }\n\n // If disabled but alwaysInsertFields is true, only insert fields without full plugin functionality\n if (enabled === false) {\n if (alwaysInsertFields) {\n return {\n ...config,\n collections: (config.collections || []).map((existingCollection) => {\n const options = allCollectionOptions[existingCollection.slug]\n\n if (options) {\n // If adapter is provided, use it to get fields\n const adapter = options.adapter\n ? options.adapter({\n collection: existingCollection,\n prefix: options.prefix,\n })\n : undefined\n\n const fields = getFields({\n adapter,\n alwaysInsertFields: true,\n collection: existingCollection,\n disablePayloadAccessControl: options.disablePayloadAccessControl,\n generateFileURL: options.generateFileURL,\n prefix: options.prefix,\n useCompositePrefixes,\n })\n\n return {\n ...existingCollection,\n fields,\n }\n }\n\n return existingCollection\n }),\n }\n }\n\n return config\n }\n\n const initFunctions: Array<() => void> = []\n const endpointPaths = new Map<Adapter, string>()\n\n const collections = (config.collections || []).map((existingCollection) => {\n const options = allCollectionOptions[existingCollection.slug]\n\n if (options?.adapter) {\n const adapter = options.adapter({\n collection: existingCollection,\n prefix: options.prefix,\n })\n\n if (adapter.onInit) {\n initFunctions.push(adapter.onInit)\n }\n\n let uploadInstructions\n\n if (adapter.uploadInstructions) {\n const { adminHandler, enabled, endpoint, ...instructions } = adapter.uploadInstructions\n let endpointPath = endpointPaths.get(options.adapter)\n\n if (enabled && endpoint && !endpointPath) {\n const endpointCount =\n config.endpoints?.filter(({ path }) => path?.startsWith(endpoint.path)).length || 0\n\n endpointPath = endpointCount ? `${endpoint.path}-${endpointCount}` : endpoint.path\n config.endpoints ??= []\n config.endpoints.push({\n handler: endpoint.handler,\n method: 'post',\n path: endpointPath,\n })\n endpointPaths.set(options.adapter, endpointPath)\n }\n\n if (enabled && adminHandler) {\n config.admin ??= {}\n config.admin.components ??= {}\n config.admin.components.providers ??= []\n config.admin.components.providers.push({\n clientProps: {\n collectionSlug: existingCollection.slug,\n endpointPath,\n prefix: options.prefix,\n props: adminHandler.props || {},\n },\n path: adminHandler.path,\n })\n }\n\n if (enabled) {\n uploadInstructions = instructions\n } else if (adminHandler) {\n /** Avoid importMap.js differences when client uploads vary between dev and production. */\n config.admin ??= {}\n config.admin.dependencies ??= {}\n config.admin.dependencies[adminHandler.path] = {\n type: 'component',\n path: adminHandler.path,\n }\n }\n }\n\n const fields = getFields({\n adapter,\n collection: existingCollection,\n disablePayloadAccessControl: options.disablePayloadAccessControl,\n generateFileURL: options.generateFileURL,\n prefix: options.prefix,\n useCompositePrefixes,\n })\n\n const handlers = [\n ...(typeof existingCollection.upload === 'object' &&\n Array.isArray(existingCollection.upload.handlers)\n ? existingCollection.upload.handlers\n : []),\n ]\n\n if (!options.disablePayloadAccessControl) {\n handlers.push(adapter.staticHandler)\n // Else if disablePayloadAccessControl: true and upload instructions are used\n // Build the \"proxied\" handler that responds only when addDataAndFileToRequest fetches the uploaded file\n } else if (uploadInstructions) {\n handlers.push((req, args) => {\n if ('uploadReference' in args.params) {\n return adapter.staticHandler(req, args)\n }\n })\n }\n\n const getSkipSafeFetchSetting = (): AllowList | boolean => {\n if (options.disablePayloadAccessControl) {\n return true\n }\n const isBooleanTrueSkipSafeFetch =\n typeof existingCollection.upload === 'object' &&\n existingCollection.upload.skipSafeFetch === true\n\n const isAllowListSkipSafeFetch =\n typeof existingCollection.upload === 'object' &&\n Array.isArray(existingCollection.upload.skipSafeFetch)\n\n if (isBooleanTrueSkipSafeFetch) {\n return true\n } else if (isAllowListSkipSafeFetch) {\n const existingSkipSafeFetch =\n typeof existingCollection.upload === 'object' &&\n Array.isArray(existingCollection.upload.skipSafeFetch)\n ? existingCollection.upload.skipSafeFetch\n : []\n\n const hasExactLocalhostMatch = existingSkipSafeFetch.some((entry) => {\n const entryKeys = Object.keys(entry)\n return entryKeys.length === 1 && entry.hostname === 'localhost'\n })\n\n const localhostEntry =\n process.env.NODE_ENV !== 'production' && !hasExactLocalhostMatch\n ? [{ hostname: 'localhost' }]\n : []\n\n return [...existingSkipSafeFetch, ...localhostEntry]\n }\n\n if (process.env.NODE_ENV !== 'production') {\n return [{ hostname: 'localhost' }]\n }\n\n return false\n }\n\n return {\n ...existingCollection,\n fields,\n hooks: {\n ...(existingCollection.hooks || {}),\n afterChange: [\n ...(existingCollection.hooks?.afterChange || []),\n getAfterChangeHook({ adapter, collection: existingCollection }),\n ],\n afterDelete: [\n ...(existingCollection.hooks?.afterDelete || []),\n getAfterDeleteHook({ adapter, collection: existingCollection }),\n ],\n beforeChange: [\n ...(existingCollection.hooks?.beforeChange || []),\n getPreserveFileDataHook(),\n ],\n },\n upload: {\n ...(typeof existingCollection.upload === 'object' ? existingCollection.upload : {}),\n adapter: adapter.name,\n ...(uploadInstructions && {\n uploadInstructions,\n }),\n disableLocalStorage:\n typeof options.disableLocalStorage === 'boolean' ? options.disableLocalStorage : true,\n handlers,\n skipSafeFetch: getSkipSafeFetchSetting(),\n },\n }\n }\n\n return existingCollection\n })\n\n return {\n ...config,\n collections,\n onInit: async (payload) => {\n initFunctions.forEach((fn) => fn())\n if (config.onInit) {\n await config.onInit(payload)\n }\n },\n }\n }\n"],"names":["getFields","getAfterChangeHook","getAfterDeleteHook","getPreserveFileDataHook","cloudStoragePlugin","pluginOptions","incomingConfig","alwaysInsertFields","collections","allCollectionOptions","enabled","useCompositePrefixes","config","map","existingCollection","options","slug","adapter","collection","prefix","undefined","fields","disablePayloadAccessControl","generateFileURL","initFunctions","endpointPaths","Map","onInit","push","uploadInstructions","adminHandler","endpoint","instructions","endpointPath","get","endpointCount","endpoints","filter","path","startsWith","length","handler","method","set","admin","components","providers","clientProps","collectionSlug","props","dependencies","type","handlers","upload","Array","isArray","staticHandler","req","args","params","getSkipSafeFetchSetting","isBooleanTrueSkipSafeFetch","skipSafeFetch","isAllowListSkipSafeFetch","existingSkipSafeFetch","hasExactLocalhostMatch","some","entry","entryKeys","Object","keys","hostname","localhostEntry","process","env","NODE_ENV","hooks","afterChange","afterDelete","beforeChange","name","disableLocalStorage","payload","forEach","fn"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { Config } from 'payload'\n\nimport type { Adapter, AllowList, PluginOptions } from './types.js'\n\nimport { getFields } from './fields/getFields.js'\nimport { getAfterChangeHook } from './hooks/afterChange.js'\nimport { getAfterDeleteHook } from './hooks/afterDelete.js'\nimport { getNormalizeUploadPrefixHook } from './hooks/normalizeUploadPrefix.js'\nimport { getPreserveFileDataHook } from './hooks/preserveFileData.js'\n\n// This plugin extends all targeted collections by offloading uploaded files\n// to cloud storage instead of solely storing files locally.\n\n// It is based on an adapter approach, where adapters can be written for any cloud provider.\n// Adapters are responsible for providing four actions that this plugin will use:\n// 1. handleUpload, 2. handleDelete, 3. generateURL, 4. staticHandler\n\n// Optionally, the adapter can specify any Webpack config overrides if they are necessary.\n\nexport const cloudStoragePlugin =\n (pluginOptions: PluginOptions) =>\n (incomingConfig: Config): Config => {\n const {\n alwaysInsertFields,\n collections: allCollectionOptions,\n enabled,\n useCompositePrefixes,\n } = pluginOptions\n const config = { ...incomingConfig }\n\n // If disabled but alwaysInsertFields is true, only insert fields without full plugin functionality\n if (enabled === false) {\n if (alwaysInsertFields) {\n return {\n ...config,\n collections: (config.collections || []).map((existingCollection) => {\n const options = allCollectionOptions[existingCollection.slug]\n\n if (options) {\n // If adapter is provided, use it to get fields\n const adapter = options.adapter\n ? options.adapter({\n collection: existingCollection,\n prefix: options.prefix,\n })\n : undefined\n\n const fields = getFields({\n adapter,\n alwaysInsertFields: true,\n collection: existingCollection,\n disablePayloadAccessControl: options.disablePayloadAccessControl,\n generateFileURL: options.generateFileURL,\n prefix: options.prefix,\n useCompositePrefixes,\n })\n\n return {\n ...existingCollection,\n fields,\n }\n }\n\n return existingCollection\n }),\n }\n }\n\n return config\n }\n\n const initFunctions: Array<() => Promise<void> | void> = []\n const endpointPaths = new Map<Adapter, string>()\n\n const collections = (config.collections || []).map((existingCollection) => {\n const options = allCollectionOptions[existingCollection.slug]\n\n if (options?.adapter) {\n const adapter = options.adapter({\n collection: existingCollection,\n prefix: options.prefix,\n })\n\n if (adapter.onInit) {\n initFunctions.push(adapter.onInit)\n }\n\n let uploadInstructions\n\n if (adapter.uploadInstructions) {\n const { adminHandler, enabled, endpoint, ...instructions } = adapter.uploadInstructions\n let endpointPath = endpointPaths.get(options.adapter)\n\n if (enabled && endpoint && !endpointPath) {\n const endpointCount =\n config.endpoints?.filter(({ path }) => path?.startsWith(endpoint.path)).length || 0\n\n endpointPath = endpointCount ? `${endpoint.path}-${endpointCount}` : endpoint.path\n config.endpoints ??= []\n config.endpoints.push({\n handler: endpoint.handler,\n method: 'post',\n path: endpointPath,\n })\n endpointPaths.set(options.adapter, endpointPath)\n }\n\n if (enabled && adminHandler) {\n config.admin ??= {}\n config.admin.components ??= {}\n config.admin.components.providers ??= []\n config.admin.components.providers.push({\n clientProps: {\n collectionSlug: existingCollection.slug,\n endpointPath,\n prefix: options.prefix,\n props: adminHandler.props || {},\n },\n path: adminHandler.path,\n })\n }\n\n if (enabled) {\n uploadInstructions = instructions\n } else if (adminHandler) {\n /** Avoid importMap.js differences when client uploads vary between dev and production. */\n config.admin ??= {}\n config.admin.dependencies ??= {}\n config.admin.dependencies[adminHandler.path] = {\n type: 'component',\n path: adminHandler.path,\n }\n }\n }\n\n const fields = getFields({\n adapter,\n collection: existingCollection,\n disablePayloadAccessControl: options.disablePayloadAccessControl,\n generateFileURL: options.generateFileURL,\n prefix: options.prefix,\n useCompositePrefixes,\n })\n\n const handlers = [\n ...(typeof existingCollection.upload === 'object' &&\n Array.isArray(existingCollection.upload.handlers)\n ? existingCollection.upload.handlers\n : []),\n ]\n\n if (!options.disablePayloadAccessControl) {\n handlers.push(adapter.staticHandler)\n // Else if disablePayloadAccessControl: true and upload instructions are used\n // Build the \"proxied\" handler that responds only when addDataAndFileToRequest fetches the uploaded file\n } else if (uploadInstructions) {\n handlers.push((req, args) => {\n if ('uploadReference' in args.params) {\n return adapter.staticHandler(req, args)\n }\n })\n }\n\n const getSkipSafeFetchSetting = (): AllowList | boolean => {\n const isBooleanTrueSkipSafeFetch =\n typeof existingCollection.upload === 'object' &&\n existingCollection.upload.skipSafeFetch === true\n\n const isAllowListSkipSafeFetch =\n typeof existingCollection.upload === 'object' &&\n Array.isArray(existingCollection.upload.skipSafeFetch)\n\n if (isBooleanTrueSkipSafeFetch) {\n return true\n } else if (isAllowListSkipSafeFetch) {\n const existingSkipSafeFetch =\n typeof existingCollection.upload === 'object' &&\n Array.isArray(existingCollection.upload.skipSafeFetch)\n ? existingCollection.upload.skipSafeFetch\n : []\n\n const hasExactLocalhostMatch = existingSkipSafeFetch.some((entry) => {\n const entryKeys = Object.keys(entry)\n return entryKeys.length === 1 && entry.hostname === 'localhost'\n })\n\n const localhostEntry =\n process.env.NODE_ENV !== 'production' && !hasExactLocalhostMatch\n ? [{ hostname: 'localhost' }]\n : []\n\n return [...existingSkipSafeFetch, ...localhostEntry]\n }\n\n if (process.env.NODE_ENV !== 'production') {\n return [{ hostname: 'localhost' }]\n }\n\n return false\n }\n\n return {\n ...existingCollection,\n fields,\n hooks: {\n ...(existingCollection.hooks || {}),\n afterChange: [\n ...(existingCollection.hooks?.afterChange || []),\n getAfterChangeHook({\n adapter,\n collection: existingCollection,\n collectionPrefix: options.prefix,\n useCompositePrefixes,\n }),\n ],\n afterDelete: [\n ...(existingCollection.hooks?.afterDelete || []),\n getAfterDeleteHook({\n adapter,\n collection: existingCollection,\n collectionPrefix: options.prefix,\n useCompositePrefixes,\n }),\n ],\n beforeChange: [\n ...(existingCollection.hooks?.beforeChange || []),\n getNormalizeUploadPrefixHook({\n collectionPrefix: options.prefix,\n useCompositePrefixes,\n }),\n getPreserveFileDataHook(),\n ],\n },\n upload: {\n ...(typeof existingCollection.upload === 'object' ? existingCollection.upload : {}),\n adapter: adapter.name,\n ...(uploadInstructions && {\n uploadInstructions,\n }),\n disableLocalStorage:\n typeof options.disableLocalStorage === 'boolean' ? options.disableLocalStorage : true,\n handlers,\n skipSafeFetch: getSkipSafeFetchSetting(),\n },\n }\n }\n\n return existingCollection\n })\n\n return {\n ...config,\n collections,\n onInit: async (payload) => {\n // Await each init so a provisioning failure fails Payload startup\n // instead of becoming an unhandled rejection.\n await Promise.all(initFunctions.map((fn) => fn()))\n if (config.onInit) {\n await config.onInit(payload)\n }\n },\n }\n }\n"],"names":["getFields","getAfterChangeHook","getAfterDeleteHook","getNormalizeUploadPrefixHook","getPreserveFileDataHook","cloudStoragePlugin","pluginOptions","incomingConfig","alwaysInsertFields","collections","allCollectionOptions","enabled","useCompositePrefixes","config","map","existingCollection","options","slug","adapter","collection","prefix","undefined","fields","disablePayloadAccessControl","generateFileURL","initFunctions","endpointPaths","Map","onInit","push","uploadInstructions","adminHandler","endpoint","instructions","endpointPath","get","endpointCount","endpoints","filter","path","startsWith","length","handler","method","set","admin","components","providers","clientProps","collectionSlug","props","dependencies","type","handlers","upload","Array","isArray","staticHandler","req","args","params","getSkipSafeFetchSetting","isBooleanTrueSkipSafeFetch","skipSafeFetch","isAllowListSkipSafeFetch","existingSkipSafeFetch","hasExactLocalhostMatch","some","entry","entryKeys","Object","keys","hostname","localhostEntry","process","env","NODE_ENV","hooks","afterChange","collectionPrefix","afterDelete","beforeChange","name","disableLocalStorage","payload","Promise","all","fn"],"mappings":"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"}
@@ -0,0 +1,166 @@
1
+ import { createServer } from 'node:http';
2
+ import { downloadFileToBuffer } from 'payload/internal';
3
+ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
4
+ import { cloudStoragePlugin } from './plugin.js';
5
+ const collectionSlug = 'media';
6
+ describe('cloudStoragePlugin external file fetching', ()=>{
7
+ const server = createServer((_req, res)=>{
8
+ res.writeHead(200, {
9
+ 'Content-Length': '12',
10
+ 'Content-Type': 'text/plain'
11
+ });
12
+ res.end('stored file\n');
13
+ });
14
+ let port;
15
+ beforeAll(async ()=>{
16
+ await new Promise((resolve)=>server.listen(0, resolve));
17
+ port = server.address().port;
18
+ });
19
+ afterEach(()=>{
20
+ vi.unstubAllEnvs();
21
+ });
22
+ afterAll(async ()=>{
23
+ await new Promise((resolve, reject)=>{
24
+ server.close((error)=>error ? reject(error) : resolve());
25
+ });
26
+ });
27
+ it('should filter private URLs when Payload file access control is disabled', async ()=>{
28
+ vi.stubEnv('NODE_ENV', 'production');
29
+ const uploadConfig = buildUploadConfig();
30
+ await expect(fetchExternalFile({
31
+ uploadConfig,
32
+ url: `http://127.0.0.1:${port}/file.txt`
33
+ })).rejects.toThrow('Blocked unsafe attempt to 127.0.0.1');
34
+ });
35
+ it('should allow private URLs when skipSafeFetch is explicitly true', async ()=>{
36
+ vi.stubEnv('NODE_ENV', 'production');
37
+ const uploadConfig = buildUploadConfig(true);
38
+ const file = await fetchExternalFile({
39
+ uploadConfig,
40
+ url: `http://127.0.0.1:${port}/file.txt`
41
+ });
42
+ expect(file.data.toString()).toBe('stored file\n');
43
+ });
44
+ it('should allow private URLs that match an explicit skipSafeFetch allowlist', async ()=>{
45
+ vi.stubEnv('NODE_ENV', 'production');
46
+ const uploadConfig = buildUploadConfig([
47
+ {
48
+ hostname: '127.0.0.1',
49
+ port: String(port),
50
+ protocol: 'http'
51
+ }
52
+ ]);
53
+ const file = await fetchExternalFile({
54
+ uploadConfig,
55
+ url: `http://127.0.0.1:${port}/file.txt`
56
+ });
57
+ expect(file.data.toString()).toBe('stored file\n');
58
+ });
59
+ it('should allow localhost URLs outside production', async ()=>{
60
+ vi.stubEnv('NODE_ENV', 'development');
61
+ const uploadConfig = buildUploadConfig();
62
+ const file = await fetchExternalFile({
63
+ uploadConfig,
64
+ url: `http://localhost:${port}/file.txt`
65
+ });
66
+ expect(file.data.toString()).toBe('stored file\n');
67
+ });
68
+ });
69
+ function buildUploadConfig(skipSafeFetch) {
70
+ const config = cloudStoragePlugin({
71
+ collections: {
72
+ [collectionSlug]: {
73
+ adapter: ()=>({
74
+ handleDelete: async ()=>undefined,
75
+ handleUpload: async ()=>undefined,
76
+ name: 'test-adapter',
77
+ staticHandler: async ()=>new Response(null, {
78
+ status: 404
79
+ })
80
+ }),
81
+ disablePayloadAccessControl: true
82
+ }
83
+ }
84
+ })({
85
+ collections: [
86
+ {
87
+ fields: [],
88
+ slug: collectionSlug,
89
+ upload: typeof skipSafeFetch === 'undefined' ? true : {
90
+ skipSafeFetch
91
+ }
92
+ }
93
+ ]
94
+ });
95
+ const uploadConfig = config.collections?.[0]?.upload;
96
+ if (!uploadConfig || typeof uploadConfig !== 'object') {
97
+ throw new Error('Expected an upload configuration');
98
+ }
99
+ return uploadConfig;
100
+ }
101
+ async function fetchExternalFile({ uploadConfig, url }) {
102
+ return downloadFileToBuffer({
103
+ data: {
104
+ filename: 'file.txt',
105
+ url
106
+ },
107
+ req: {
108
+ headers: new Headers(),
109
+ payload: {
110
+ config: {
111
+ cookiePrefix: 'payload'
112
+ }
113
+ }
114
+ },
115
+ uploadConfig
116
+ });
117
+ }
118
+ const adapter = ()=>({
119
+ handleDelete: ()=>undefined,
120
+ handleUpload: ()=>undefined,
121
+ name: 'test-adapter',
122
+ staticHandler: ()=>new Response()
123
+ });
124
+ describe('cloudStoragePlugin', ()=>{
125
+ it('should normalize a stored prefix during a server-mediated upload', async ()=>{
126
+ const config = cloudStoragePlugin({
127
+ collections: {
128
+ media: {
129
+ adapter
130
+ }
131
+ }
132
+ })({
133
+ collections: [
134
+ {
135
+ fields: [],
136
+ slug: 'media',
137
+ upload: true
138
+ }
139
+ ]
140
+ });
141
+ const hooks = config.collections?.[0]?.hooks?.beforeChange || [];
142
+ const req = {
143
+ context: {},
144
+ file: {
145
+ mimetype: 'image/png',
146
+ name: 'photo.png',
147
+ size: 42
148
+ }
149
+ };
150
+ let data = {
151
+ filename: 'photo.png',
152
+ prefix: '/tenant/../acme'
153
+ };
154
+ for (const hook of hooks){
155
+ const result = await hook({
156
+ data,
157
+ operation: 'create',
158
+ req
159
+ });
160
+ data = result || data;
161
+ }
162
+ expect(data.prefix).toBe('tenant/acme');
163
+ });
164
+ });
165
+
166
+ //# sourceMappingURL=plugin.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin.spec.ts"],"sourcesContent":["import type { AddressInfo } from 'node:net'\nimport type { Config, PayloadRequest, UploadConfig } from 'payload'\n\nimport { createServer } from 'node:http'\nimport { downloadFileToBuffer } from 'payload/internal'\nimport { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'\n\nimport { cloudStoragePlugin } from './plugin.js'\n\nconst collectionSlug = 'media'\n\ndescribe('cloudStoragePlugin external file fetching', () => {\n const server = createServer((_req, res) => {\n res.writeHead(200, {\n 'Content-Length': '12',\n 'Content-Type': 'text/plain',\n })\n res.end('stored file\\n')\n })\n\n let port: number\n\n beforeAll(async () => {\n await new Promise<void>((resolve) => server.listen(0, resolve))\n port = (server.address() as AddressInfo).port\n })\n\n afterEach(() => {\n vi.unstubAllEnvs()\n })\n\n afterAll(async () => {\n await new Promise<void>((resolve, reject) => {\n server.close((error) => (error ? reject(error) : resolve()))\n })\n })\n\n it('should filter private URLs when Payload file access control is disabled', async () => {\n vi.stubEnv('NODE_ENV', 'production')\n const uploadConfig = buildUploadConfig()\n\n await expect(\n fetchExternalFile({ uploadConfig, url: `http://127.0.0.1:${port}/file.txt` }),\n ).rejects.toThrow('Blocked unsafe attempt to 127.0.0.1')\n })\n\n it('should allow private URLs when skipSafeFetch is explicitly true', async () => {\n vi.stubEnv('NODE_ENV', 'production')\n const uploadConfig = buildUploadConfig(true)\n\n const file = await fetchExternalFile({\n uploadConfig,\n url: `http://127.0.0.1:${port}/file.txt`,\n })\n\n expect(file.data.toString()).toBe('stored file\\n')\n })\n\n it('should allow private URLs that match an explicit skipSafeFetch allowlist', async () => {\n vi.stubEnv('NODE_ENV', 'production')\n const uploadConfig = buildUploadConfig([\n {\n hostname: '127.0.0.1',\n port: String(port),\n protocol: 'http',\n },\n ])\n\n const file = await fetchExternalFile({\n uploadConfig,\n url: `http://127.0.0.1:${port}/file.txt`,\n })\n\n expect(file.data.toString()).toBe('stored file\\n')\n })\n\n it('should allow localhost URLs outside production', async () => {\n vi.stubEnv('NODE_ENV', 'development')\n const uploadConfig = buildUploadConfig()\n\n const file = await fetchExternalFile({\n uploadConfig,\n url: `http://localhost:${port}/file.txt`,\n })\n\n expect(file.data.toString()).toBe('stored file\\n')\n })\n})\n\nfunction buildUploadConfig(skipSafeFetch?: UploadConfig['skipSafeFetch']): UploadConfig {\n const config = cloudStoragePlugin({\n collections: {\n [collectionSlug]: {\n adapter: () => ({\n handleDelete: async () => undefined,\n handleUpload: async () => undefined,\n name: 'test-adapter',\n staticHandler: async () => new Response(null, { status: 404 }),\n }),\n disablePayloadAccessControl: true,\n },\n },\n })({\n collections: [\n {\n fields: [],\n slug: collectionSlug,\n upload: typeof skipSafeFetch === 'undefined' ? true : { skipSafeFetch },\n },\n ],\n } as Config)\n\n const uploadConfig = config.collections?.[0]?.upload\n if (!uploadConfig || typeof uploadConfig !== 'object') {\n throw new Error('Expected an upload configuration')\n }\n\n return uploadConfig\n}\n\nasync function fetchExternalFile({\n uploadConfig,\n url,\n}: {\n uploadConfig: UploadConfig\n url: string\n}) {\n return downloadFileToBuffer({\n data: {\n filename: 'file.txt',\n url,\n },\n req: {\n headers: new Headers(),\n payload: {\n config: {\n cookiePrefix: 'payload',\n },\n },\n } as PayloadRequest,\n uploadConfig,\n })\n}\n\nconst adapter = () => ({\n handleDelete: () => undefined,\n handleUpload: () => undefined,\n name: 'test-adapter',\n staticHandler: () => new Response(),\n})\n\ndescribe('cloudStoragePlugin', () => {\n it('should normalize a stored prefix during a server-mediated upload', async () => {\n const config = cloudStoragePlugin({\n collections: {\n media: { adapter },\n },\n } as any)({\n collections: [{ fields: [], slug: 'media', upload: true }],\n } as any)\n const hooks = config.collections?.[0]?.hooks?.beforeChange || []\n const req = {\n context: {},\n file: { mimetype: 'image/png', name: 'photo.png', size: 42 },\n }\n let data = { filename: 'photo.png', prefix: '/tenant/../acme' }\n\n for (const hook of hooks) {\n const result = await hook({ data, operation: 'create', req } as any)\n data = result || data\n }\n\n expect(data.prefix).toBe('tenant/acme')\n })\n})\n"],"names":["createServer","downloadFileToBuffer","afterAll","afterEach","beforeAll","describe","expect","it","vi","cloudStoragePlugin","collectionSlug","server","_req","res","writeHead","end","port","Promise","resolve","listen","address","unstubAllEnvs","reject","close","error","stubEnv","uploadConfig","buildUploadConfig","fetchExternalFile","url","rejects","toThrow","file","data","toString","toBe","hostname","String","protocol","skipSafeFetch","config","collections","adapter","handleDelete","undefined","handleUpload","name","staticHandler","Response","status","disablePayloadAccessControl","fields","slug","upload","Error","filename","req","headers","Headers","payload","cookiePrefix","media","hooks","beforeChange","context","mimetype","size","prefix","hook","result","operation"],"mappings":"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"}