@payloadcms/plugin-cloud-storage 4.0.0-internal.688c4d0 → 4.0.0-internal.77191e5

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 (85) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +2 -0
  3. package/dist/client/createClientUploadHandler.d.ts +8 -7
  4. package/dist/client/createClientUploadHandler.d.ts.map +1 -1
  5. package/dist/client/createClientUploadHandler.js +26 -22
  6. package/dist/client/createClientUploadHandler.js.map +1 -1
  7. package/dist/exports/client.d.ts +2 -0
  8. package/dist/exports/client.d.ts.map +1 -1
  9. package/dist/exports/client.js +2 -0
  10. package/dist/exports/client.js.map +1 -1
  11. package/dist/exports/utilities.d.ts +1 -2
  12. package/dist/exports/utilities.d.ts.map +1 -1
  13. package/dist/exports/utilities.js +1 -2
  14. package/dist/exports/utilities.js.map +1 -1
  15. package/dist/fields/getFields.d.ts +1 -5
  16. package/dist/fields/getFields.d.ts.map +1 -1
  17. package/dist/fields/getFields.js +41 -18
  18. package/dist/fields/getFields.js.map +1 -1
  19. package/dist/hooks/afterChange.d.ts +3 -1
  20. package/dist/hooks/afterChange.d.ts.map +1 -1
  21. package/dist/hooks/afterChange.js +79 -22
  22. package/dist/hooks/afterChange.js.map +1 -1
  23. package/dist/hooks/afterDelete.d.ts +3 -1
  24. package/dist/hooks/afterDelete.d.ts.map +1 -1
  25. package/dist/hooks/afterDelete.js +16 -2
  26. package/dist/hooks/afterDelete.js.map +1 -1
  27. package/dist/hooks/afterRead.d.ts.map +1 -1
  28. package/dist/hooks/afterRead.js +15 -2
  29. package/dist/hooks/afterRead.js.map +1 -1
  30. package/dist/hooks/beforeChange.d.ts.map +1 -1
  31. package/dist/hooks/beforeChange.js +12 -1
  32. package/dist/hooks/beforeChange.js.map +1 -1
  33. package/dist/hooks/normalizeUploadPrefix.d.ts +11 -0
  34. package/dist/hooks/normalizeUploadPrefix.d.ts.map +1 -0
  35. package/dist/hooks/normalizeUploadPrefix.js +42 -0
  36. package/dist/hooks/normalizeUploadPrefix.js.map +1 -0
  37. package/dist/plugin.d.ts.map +1 -1
  38. package/dist/plugin.js +178 -128
  39. package/dist/plugin.js.map +1 -1
  40. package/dist/types.d.ts +35 -21
  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/getFilePrefix.d.ts +19 -11
  52. package/dist/utilities/getFilePrefix.d.ts.map +1 -1
  53. package/dist/utilities/getFilePrefix.js +70 -30
  54. package/dist/utilities/getFilePrefix.js.map +1 -1
  55. package/dist/utilities/getIncomingFiles.js +2 -2
  56. package/dist/utilities/getIncomingFiles.js.map +1 -1
  57. package/dist/utilities/resolveSignedURLKey.d.ts +13 -2
  58. package/dist/utilities/resolveSignedURLKey.d.ts.map +1 -1
  59. package/dist/utilities/resolveSignedURLKey.js +43 -9
  60. package/dist/utilities/resolveSignedURLKey.js.map +1 -1
  61. package/dist/utilities/sanitizePrefix.d.ts +1 -9
  62. package/dist/utilities/sanitizePrefix.d.ts.map +1 -1
  63. package/dist/utilities/sanitizePrefix.js +1 -21
  64. package/dist/utilities/sanitizePrefix.js.map +1 -1
  65. package/package.json +4 -4
  66. package/dist/admin/fields/getFields.d.ts +0 -17
  67. package/dist/admin/fields/getFields.d.ts.map +0 -1
  68. package/dist/admin/fields/getFields.js +0 -98
  69. package/dist/admin/fields/getFields.js.map +0 -1
  70. package/dist/admin/index.d.ts +0 -4
  71. package/dist/admin/index.d.ts.map +0 -1
  72. package/dist/admin/index.js +0 -34
  73. package/dist/admin/index.js.map +0 -1
  74. package/dist/utilities/getFileKey.d.ts +0 -22
  75. package/dist/utilities/getFileKey.d.ts.map +0 -1
  76. package/dist/utilities/getFileKey.js +0 -23
  77. package/dist/utilities/getFileKey.js.map +0 -1
  78. package/dist/utilities/getFileKey.spec.js +0 -174
  79. package/dist/utilities/getFileKey.spec.js.map +0 -1
  80. package/dist/utilities/getFilePrefix.spec.js +0 -105
  81. package/dist/utilities/getFilePrefix.spec.js.map +0 -1
  82. package/dist/utilities/initClientUploads.d.ts +0 -13
  83. package/dist/utilities/initClientUploads.d.ts.map +0 -1
  84. package/dist/utilities/initClientUploads.js +0 -63
  85. package/dist/utilities/initClientUploads.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"afterDelete.d.ts","sourceRoot":"","sources":["../../src/hooks/afterDelete.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAEhG,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEnE,UAAU,IAAI;IACZ,OAAO,EAAE,gBAAgB,CAAA;IACzB,UAAU,EAAE,gBAAgB,CAAA;CAC7B;AAED,eAAO,MAAM,kBAAkB,GAAI,0BAGhC,IAAI,KAAG,yBAAyB,CAAC,QAAQ,GAAG,UAAU,GAAG,cAAc,CAyBzE,CAAA"}
1
+ {"version":3,"file":"afterDelete.d.ts","sourceRoot":"","sources":["../../src/hooks/afterDelete.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAEhG,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAKnE,UAAU,IAAI;IACZ,OAAO,EAAE,gBAAgB,CAAA;IACzB,UAAU,EAAE,gBAAgB,CAAA;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B;AAED,eAAO,MAAM,kBAAkB,qEAK5B,IAAI,KAAG,yBAAyB,CAAC,QAAQ,GAAG,UAAU,GAAG,cAAc,CAqCzE,CAAA"}
@@ -1,17 +1,31 @@
1
- export const getAfterDeleteHook = ({ adapter, collection })=>{
1
+ import { buildPrefixWithObjectKey } from '../utilities/buildPrefixWithObjectKey.js';
2
+ import { buildStoragePathData } from '../utilities/buildStoragePathData.js';
3
+ export const getAfterDeleteHook = ({ adapter, collection, collectionPrefix, useCompositePrefixes })=>{
2
4
  return async ({ doc, req })=>{
3
5
  try {
6
+ // Fold `_objectKey` so deletes target the real object folder.
7
+ const docPrefix = buildPrefixWithObjectKey({
8
+ objectKey: doc._objectKey,
9
+ prefix: doc.prefix
10
+ });
4
11
  const filesToDelete = [
5
12
  doc.filename,
6
13
  ...Object.values(doc?.sizes || []).map((resizedFileData)=>resizedFileData?.filename)
7
14
  ];
8
15
  const promises = filesToDelete.map(async (filename)=>{
9
16
  if (filename) {
17
+ const { storageFilePath } = buildStoragePathData({
18
+ collectionPrefix,
19
+ docPrefix,
20
+ filename,
21
+ useCompositePrefixes
22
+ });
10
23
  await adapter.handleDelete({
11
24
  collection,
12
25
  doc,
13
26
  filename,
14
- req
27
+ req,
28
+ storageFilePath
15
29
  });
16
30
  }
17
31
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/afterDelete.ts"],"sourcesContent":["import type { CollectionAfterDeleteHook, CollectionConfig, FileData, TypeWithID } from 'payload'\n\nimport type { GeneratedAdapter, TypeWithPrefix } from '../types.js'\n\ninterface Args {\n adapter: GeneratedAdapter\n collection: CollectionConfig\n}\n\nexport const getAfterDeleteHook = ({\n adapter,\n collection,\n}: Args): CollectionAfterDeleteHook<FileData & TypeWithID & TypeWithPrefix> => {\n return async ({ doc, req }) => {\n try {\n const filesToDelete: string[] = [\n doc.filename,\n ...Object.values(doc?.sizes || []).map(\n (resizedFileData) => resizedFileData?.filename as string,\n ),\n ]\n\n const promises = filesToDelete.map(async (filename) => {\n if (filename) {\n await adapter.handleDelete({ collection, doc, filename, req })\n }\n })\n\n await Promise.all(promises)\n } catch (err: unknown) {\n req.payload.logger.error({\n err,\n msg: `There was an error while deleting files corresponding to the ${collection.labels?.singular} with ID ${doc.id}.`,\n })\n }\n return doc\n }\n}\n"],"names":["getAfterDeleteHook","adapter","collection","doc","req","filesToDelete","filename","Object","values","sizes","map","resizedFileData","promises","handleDelete","Promise","all","err","payload","logger","error","msg","labels","singular","id"],"mappings":"AASA,OAAO,MAAMA,qBAAqB,CAAC,EACjCC,OAAO,EACPC,UAAU,EACL;IACL,OAAO,OAAO,EAAEC,GAAG,EAAEC,GAAG,EAAE;QACxB,IAAI;YACF,MAAMC,gBAA0B;gBAC9BF,IAAIG,QAAQ;mBACTC,OAAOC,MAAM,CAACL,KAAKM,SAAS,EAAE,EAAEC,GAAG,CACpC,CAACC,kBAAoBA,iBAAiBL;aAEzC;YAED,MAAMM,WAAWP,cAAcK,GAAG,CAAC,OAAOJ;gBACxC,IAAIA,UAAU;oBACZ,MAAML,QAAQY,YAAY,CAAC;wBAAEX;wBAAYC;wBAAKG;wBAAUF;oBAAI;gBAC9D;YACF;YAEA,MAAMU,QAAQC,GAAG,CAACH;QACpB,EAAE,OAAOI,KAAc;YACrBZ,IAAIa,OAAO,CAACC,MAAM,CAACC,KAAK,CAAC;gBACvBH;gBACAI,KAAK,CAAC,6DAA6D,EAAElB,WAAWmB,MAAM,EAAEC,SAAS,SAAS,EAAEnB,IAAIoB,EAAE,CAAC,CAAC,CAAC;YACvH;QACF;QACA,OAAOpB;IACT;AACF,EAAC"}
1
+ {"version":3,"sources":["../../src/hooks/afterDelete.ts"],"sourcesContent":["import type { CollectionAfterDeleteHook, CollectionConfig, FileData, TypeWithID } from 'payload'\n\nimport type { GeneratedAdapter, TypeWithPrefix } from '../types.js'\n\nimport { buildPrefixWithObjectKey } from '../utilities/buildPrefixWithObjectKey.js'\nimport { buildStoragePathData } from '../utilities/buildStoragePathData.js'\n\ninterface Args {\n adapter: GeneratedAdapter\n collection: CollectionConfig\n collectionPrefix?: string\n useCompositePrefixes?: boolean\n}\n\nexport const getAfterDeleteHook = ({\n adapter,\n collection,\n collectionPrefix,\n useCompositePrefixes,\n}: Args): CollectionAfterDeleteHook<FileData & TypeWithID & TypeWithPrefix> => {\n return async ({ doc, req }) => {\n try {\n // Fold `_objectKey` so deletes target the real object folder.\n const docPrefix = buildPrefixWithObjectKey({\n objectKey: (doc as { _objectKey?: string })._objectKey,\n prefix: doc.prefix,\n })\n\n const filesToDelete: string[] = [\n doc.filename,\n ...Object.values(doc?.sizes || []).map(\n (resizedFileData) => resizedFileData?.filename as string,\n ),\n ]\n\n const promises = filesToDelete.map(async (filename) => {\n if (filename) {\n const { storageFilePath } = buildStoragePathData({\n collectionPrefix,\n docPrefix,\n filename,\n useCompositePrefixes,\n })\n await adapter.handleDelete({ collection, doc, filename, req, storageFilePath })\n }\n })\n\n await Promise.all(promises)\n } catch (err: unknown) {\n req.payload.logger.error({\n err,\n msg: `There was an error while deleting files corresponding to the ${collection.labels?.singular} with ID ${doc.id}.`,\n })\n }\n return doc\n }\n}\n"],"names":["buildPrefixWithObjectKey","buildStoragePathData","getAfterDeleteHook","adapter","collection","collectionPrefix","useCompositePrefixes","doc","req","docPrefix","objectKey","_objectKey","prefix","filesToDelete","filename","Object","values","sizes","map","resizedFileData","promises","storageFilePath","handleDelete","Promise","all","err","payload","logger","error","msg","labels","singular","id"],"mappings":"AAIA,SAASA,wBAAwB,QAAQ,2CAA0C;AACnF,SAASC,oBAAoB,QAAQ,uCAAsC;AAS3E,OAAO,MAAMC,qBAAqB,CAAC,EACjCC,OAAO,EACPC,UAAU,EACVC,gBAAgB,EAChBC,oBAAoB,EACf;IACL,OAAO,OAAO,EAAEC,GAAG,EAAEC,GAAG,EAAE;QACxB,IAAI;YACF,8DAA8D;YAC9D,MAAMC,YAAYT,yBAAyB;gBACzCU,WAAW,AAACH,IAAgCI,UAAU;gBACtDC,QAAQL,IAAIK,MAAM;YACpB;YAEA,MAAMC,gBAA0B;gBAC9BN,IAAIO,QAAQ;mBACTC,OAAOC,MAAM,CAACT,KAAKU,SAAS,EAAE,EAAEC,GAAG,CACpC,CAACC,kBAAoBA,iBAAiBL;aAEzC;YAED,MAAMM,WAAWP,cAAcK,GAAG,CAAC,OAAOJ;gBACxC,IAAIA,UAAU;oBACZ,MAAM,EAAEO,eAAe,EAAE,GAAGpB,qBAAqB;wBAC/CI;wBACAI;wBACAK;wBACAR;oBACF;oBACA,MAAMH,QAAQmB,YAAY,CAAC;wBAAElB;wBAAYG;wBAAKO;wBAAUN;wBAAKa;oBAAgB;gBAC/E;YACF;YAEA,MAAME,QAAQC,GAAG,CAACJ;QACpB,EAAE,OAAOK,KAAc;YACrBjB,IAAIkB,OAAO,CAACC,MAAM,CAACC,KAAK,CAAC;gBACvBH;gBACAI,KAAK,CAAC,6DAA6D,EAAEzB,WAAW0B,MAAM,EAAEC,SAAS,SAAS,EAAExB,IAAIyB,EAAE,CAAC,CAAC,CAAC;YACvH;QACF;QACA,OAAOzB;IACT;AACF,EAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"afterRead.d.ts","sourceRoot":"","sources":["../../src/hooks/afterRead.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAErE,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEpE,UAAU,IAAI;IACZ,OAAO,EAAE,gBAAgB,CAAA;IACzB,UAAU,EAAE,gBAAgB,CAAA;IAC5B,2BAA2B,CAAC,EAAE,OAAO,CAAA;IACrC,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAED,eAAO,MAAM,gBAAgB,GAC1B,6EAA6E,IAAI,KAAG,SA4BpF,CAAA"}
1
+ {"version":3,"file":"afterRead.d.ts","sourceRoot":"","sources":["../../src/hooks/afterRead.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAErE,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAIpE,UAAU,IAAI;IACZ,OAAO,EAAE,gBAAgB,CAAA;IACzB,UAAU,EAAE,gBAAgB,CAAA;IAC5B,2BAA2B,CAAC,EAAE,OAAO,CAAA;IACrC,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAiBD,eAAO,MAAM,gBAAgB,gFACmD,IAAI,KAAG,SA8BpF,CAAA"}
@@ -1,13 +1,26 @@
1
+ import { sanitizePrefix } from '../utilities/sanitizePrefix.js';
2
+ // The object's folder: semantic prefix + `_objectKey` segment.
3
+ const getObjectFolder = (data)=>{
4
+ const record = data ?? {};
5
+ const safePrefix = sanitizePrefix(typeof record.prefix === 'string' ? record.prefix : '');
6
+ const safeObjectKey = sanitizePrefix(typeof record._objectKey === 'string' ? record._objectKey : '');
7
+ if (safePrefix && safeObjectKey) {
8
+ return `${safePrefix}/${safeObjectKey}`;
9
+ }
10
+ return safePrefix || safeObjectKey;
11
+ };
1
12
  export const getAfterReadHook = ({ adapter, collection, disablePayloadAccessControl, generateFileURL, size })=>async ({ data, value })=>{
2
13
  const filename = size ? data?.sizes?.[size.name]?.filename : data?.filename;
3
14
  const prefix = data?.prefix;
15
+ // Direct-serve URLs encode the full location; the proxy resolves `_objectKey` server-side.
16
+ const objectFolder = getObjectFolder(data);
4
17
  let url = value;
5
18
  if (filename) {
6
19
  if (generateFileURL) {
7
20
  url = await generateFileURL({
8
21
  collection,
9
22
  filename,
10
- prefix,
23
+ prefix: objectFolder,
11
24
  size
12
25
  });
13
26
  } else if (disablePayloadAccessControl && adapter.generateURL) {
@@ -15,7 +28,7 @@ export const getAfterReadHook = ({ adapter, collection, disablePayloadAccessCont
15
28
  collection,
16
29
  data,
17
30
  filename,
18
- prefix
31
+ prefix: objectFolder
19
32
  });
20
33
  } else if (url && prefix) {
21
34
  const separator = url.includes('?') ? '&' : '?';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/afterRead.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 getAfterReadHook =\n ({ adapter, collection, disablePayloadAccessControl, generateFileURL, size }: Args): FieldHook =>\n async ({ data, value }) => {\n const filename = size ? data?.sizes?.[size.name]?.filename : data?.filename\n const prefix = data?.prefix\n let url = value\n\n if (filename) {\n if (generateFileURL) {\n url = await generateFileURL({\n collection,\n filename,\n prefix,\n size,\n })\n } else if (disablePayloadAccessControl && adapter.generateURL) {\n url = await adapter.generateURL({\n collection,\n data,\n filename,\n prefix,\n })\n } else if (url && prefix) {\n const separator = url.includes('?') ? '&' : '?'\n url = `${url}${separator}prefix=${encodeURIComponent(prefix)}`\n }\n }\n\n return url\n }\n"],"names":["getAfterReadHook","adapter","collection","disablePayloadAccessControl","generateFileURL","size","data","value","filename","sizes","name","prefix","url","generateURL","separator","includes","encodeURIComponent"],"mappings":"AAYA,OAAO,MAAMA,mBACX,CAAC,EAAEC,OAAO,EAAEC,UAAU,EAAEC,2BAA2B,EAAEC,eAAe,EAAEC,IAAI,EAAQ,GAClF,OAAO,EAAEC,IAAI,EAAEC,KAAK,EAAE;QACpB,MAAMC,WAAWH,OAAOC,MAAMG,OAAO,CAACJ,KAAKK,IAAI,CAAC,EAAEF,WAAWF,MAAME;QACnE,MAAMG,SAASL,MAAMK;QACrB,IAAIC,MAAML;QAEV,IAAIC,UAAU;YACZ,IAAIJ,iBAAiB;gBACnBQ,MAAM,MAAMR,gBAAgB;oBAC1BF;oBACAM;oBACAG;oBACAN;gBACF;YACF,OAAO,IAAIF,+BAA+BF,QAAQY,WAAW,EAAE;gBAC7DD,MAAM,MAAMX,QAAQY,WAAW,CAAC;oBAC9BX;oBACAI;oBACAE;oBACAG;gBACF;YACF,OAAO,IAAIC,OAAOD,QAAQ;gBACxB,MAAMG,YAAYF,IAAIG,QAAQ,CAAC,OAAO,MAAM;gBAC5CH,MAAM,GAAGA,MAAME,UAAU,OAAO,EAAEE,mBAAmBL,SAAS;YAChE;QACF;QAEA,OAAOC;IACT,EAAC"}
1
+ {"version":3,"sources":["../../src/hooks/afterRead.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): string => {\n const record = (data ?? {}) as Record<string, unknown>\n const safePrefix = sanitizePrefix(typeof record.prefix === 'string' ? record.prefix : '')\n const safeObjectKey = sanitizePrefix(\n typeof record._objectKey === 'string' ? record._objectKey : '',\n )\n\n if (safePrefix && safeObjectKey) {\n return `${safePrefix}/${safeObjectKey}`\n }\n\n return safePrefix || safeObjectKey\n}\n\nexport const getAfterReadHook =\n ({ adapter, collection, disablePayloadAccessControl, generateFileURL, size }: Args): FieldHook =>\n async ({ data, value }) => {\n const filename = size ? data?.sizes?.[size.name]?.filename : data?.filename\n const prefix = data?.prefix\n // Direct-serve URLs encode the full location; the proxy resolves `_objectKey` server-side.\n const objectFolder = getObjectFolder(data)\n let url = value\n\n if (filename) {\n if (generateFileURL) {\n url = await generateFileURL({\n collection,\n filename,\n prefix: objectFolder,\n size,\n })\n } else if (disablePayloadAccessControl && adapter.generateURL) {\n url = await adapter.generateURL({\n collection,\n data,\n filename,\n prefix: objectFolder,\n })\n } else if (url && prefix) {\n const separator = url.includes('?') ? '&' : '?'\n url = `${url}${separator}prefix=${encodeURIComponent(prefix)}`\n }\n }\n\n return url\n }\n"],"names":["sanitizePrefix","getObjectFolder","data","record","safePrefix","prefix","safeObjectKey","_objectKey","getAfterReadHook","adapter","collection","disablePayloadAccessControl","generateFileURL","size","value","filename","sizes","name","objectFolder","url","generateURL","separator","includes","encodeURIComponent"],"mappings":"AAIA,SAASA,cAAc,QAAQ,iCAAgC;AAU/D,+DAA+D;AAC/D,MAAMC,kBAAkB,CAACC;IACvB,MAAMC,SAAUD,QAAQ,CAAC;IACzB,MAAME,aAAaJ,eAAe,OAAOG,OAAOE,MAAM,KAAK,WAAWF,OAAOE,MAAM,GAAG;IACtF,MAAMC,gBAAgBN,eACpB,OAAOG,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,mBACX,CAAC,EAAEC,OAAO,EAAEC,UAAU,EAAEC,2BAA2B,EAAEC,eAAe,EAAEC,IAAI,EAAQ,GAClF,OAAO,EAAEX,IAAI,EAAEY,KAAK,EAAE;QACpB,MAAMC,WAAWF,OAAOX,MAAMc,OAAO,CAACH,KAAKI,IAAI,CAAC,EAAEF,WAAWb,MAAMa;QACnE,MAAMV,SAASH,MAAMG;QACrB,2FAA2F;QAC3F,MAAMa,eAAejB,gBAAgBC;QACrC,IAAIiB,MAAML;QAEV,IAAIC,UAAU;YACZ,IAAIH,iBAAiB;gBACnBO,MAAM,MAAMP,gBAAgB;oBAC1BF;oBACAK;oBACAV,QAAQa;oBACRL;gBACF;YACF,OAAO,IAAIF,+BAA+BF,QAAQW,WAAW,EAAE;gBAC7DD,MAAM,MAAMV,QAAQW,WAAW,CAAC;oBAC9BV;oBACAR;oBACAa;oBACAV,QAAQa;gBACV;YACF,OAAO,IAAIC,OAAOd,QAAQ;gBACxB,MAAMgB,YAAYF,IAAIG,QAAQ,CAAC,OAAO,MAAM;gBAC5CH,MAAM,GAAGA,MAAME,UAAU,OAAO,EAAEE,mBAAmBlB,SAAS;YAChE;QACF;QAEA,OAAOc;IACT,EAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"beforeChange.d.ts","sourceRoot":"","sources":["../../src/hooks/beforeChange.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAErE,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEpE,UAAU,IAAI;IACZ,OAAO,EAAE,gBAAgB,CAAA;IACzB,UAAU,EAAE,gBAAgB,CAAA;IAC5B,2BAA2B,CAAC,EAAE,OAAO,CAAA;IACrC,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAED,eAAO,MAAM,mBAAmB,GAC7B,6EAA6E,IAAI,KAAG,SA2BpF,CAAA"}
1
+ {"version":3,"file":"beforeChange.d.ts","sourceRoot":"","sources":["../../src/hooks/beforeChange.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAErE,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAIpE,UAAU,IAAI;IACZ,OAAO,EAAE,gBAAgB,CAAA;IACzB,UAAU,EAAE,gBAAgB,CAAA;IAC5B,2BAA2B,CAAC,EAAE,OAAO,CAAA;IACrC,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAiBD,eAAO,MAAM,mBAAmB,gFACgD,IAAI,KAAG,SA2BpF,CAAA"}
@@ -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"}
@@ -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,EAAa,aAAa,EAAE,MAAM,YAAY,CAAA;AAgB1D,eAAO,MAAM,kBAAkB,GAC5B,eAAe,aAAa,MAC5B,gBAAgB,MAAM,KAAG,MAgLzB,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,MAwOzB,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.
@@ -9,148 +10,197 @@ import { getPreserveFileDataHook } from './hooks/preserveFileData.js';
9
10
  // 1. handleUpload, 2. handleDelete, 3. generateURL, 4. staticHandler
10
11
  // Optionally, the adapter can specify any Webpack config overrides if they are necessary.
11
12
  export const cloudStoragePlugin = (pluginOptions)=>(incomingConfig)=>{
12
- const { alwaysInsertFields, collections: allCollectionOptions, enabled, useCompositePrefixes } = pluginOptions;
13
+ const { collections: allCollectionOptions, enabled, useCompositePrefixes } = pluginOptions;
13
14
  const config = {
14
15
  ...incomingConfig
15
16
  };
16
- // If disabled but alwaysInsertFields is true, only insert fields without full plugin functionality
17
+ // If disabled, only insert fields (e.g. prefix) without full plugin functionality,
18
+ // so the collection schema stays consistent across environments.
17
19
  if (enabled === false) {
18
- if (alwaysInsertFields) {
20
+ return {
21
+ ...config,
22
+ collections: (config.collections || []).map((existingCollection)=>{
23
+ const options = allCollectionOptions[existingCollection.slug];
24
+ if (options) {
25
+ // If adapter is provided, use it to get fields
26
+ const adapter = options.adapter ? options.adapter({
27
+ collection: existingCollection,
28
+ prefix: options.prefix
29
+ }) : undefined;
30
+ const fields = getFields({
31
+ adapter,
32
+ collection: existingCollection,
33
+ disablePayloadAccessControl: options.disablePayloadAccessControl,
34
+ generateFileURL: options.generateFileURL,
35
+ prefix: options.prefix,
36
+ useCompositePrefixes
37
+ });
38
+ return {
39
+ ...existingCollection,
40
+ fields
41
+ };
42
+ }
43
+ return existingCollection;
44
+ })
45
+ };
46
+ }
47
+ const initFunctions = [];
48
+ const endpointPaths = new Map();
49
+ const collections = (config.collections || []).map((existingCollection)=>{
50
+ const options = allCollectionOptions[existingCollection.slug];
51
+ if (options?.adapter) {
52
+ const adapter = options.adapter({
53
+ collection: existingCollection,
54
+ prefix: options.prefix
55
+ });
56
+ if (adapter.onInit) {
57
+ initFunctions.push(adapter.onInit);
58
+ }
59
+ let uploadInstructions;
60
+ if (adapter.uploadInstructions) {
61
+ const { adminHandler, enabled, endpoint, ...instructions } = adapter.uploadInstructions;
62
+ let endpointPath = endpointPaths.get(options.adapter);
63
+ if (enabled && endpoint && !endpointPath) {
64
+ const endpointCount = config.endpoints?.filter(({ path })=>path?.startsWith(endpoint.path)).length || 0;
65
+ endpointPath = endpointCount ? `${endpoint.path}-${endpointCount}` : endpoint.path;
66
+ config.endpoints ??= [];
67
+ config.endpoints.push({
68
+ handler: endpoint.handler,
69
+ method: 'post',
70
+ path: endpointPath
71
+ });
72
+ endpointPaths.set(options.adapter, endpointPath);
73
+ }
74
+ if (enabled && adminHandler) {
75
+ config.admin ??= {};
76
+ config.admin.components ??= {};
77
+ config.admin.components.providers ??= [];
78
+ config.admin.components.providers.push({
79
+ clientProps: {
80
+ collectionSlug: existingCollection.slug,
81
+ endpointPath,
82
+ prefix: options.prefix,
83
+ props: adminHandler.props || {}
84
+ },
85
+ path: adminHandler.path
86
+ });
87
+ }
88
+ if (enabled) {
89
+ uploadInstructions = instructions;
90
+ } else if (adminHandler) {
91
+ /** Avoid importMap.js differences when client uploads vary between dev and production. */ config.admin ??= {};
92
+ config.admin.dependencies ??= {};
93
+ config.admin.dependencies[adminHandler.path] = {
94
+ type: 'component',
95
+ path: adminHandler.path
96
+ };
97
+ }
98
+ }
99
+ const fields = getFields({
100
+ adapter,
101
+ collection: existingCollection,
102
+ disablePayloadAccessControl: options.disablePayloadAccessControl,
103
+ generateFileURL: options.generateFileURL,
104
+ prefix: options.prefix,
105
+ useCompositePrefixes
106
+ });
107
+ const handlers = [
108
+ ...typeof existingCollection.upload === 'object' && Array.isArray(existingCollection.upload.handlers) ? existingCollection.upload.handlers : []
109
+ ];
110
+ if (!options.disablePayloadAccessControl) {
111
+ handlers.push(adapter.staticHandler);
112
+ // Else if disablePayloadAccessControl: true and upload instructions are used
113
+ // Build the "proxied" handler that responds only when addDataAndFileToRequest fetches the uploaded file
114
+ } else if (uploadInstructions) {
115
+ handlers.push((req, args)=>{
116
+ if ('uploadReference' in args.params) {
117
+ return adapter.staticHandler(req, args);
118
+ }
119
+ });
120
+ }
121
+ const getSkipSafeFetchSetting = ()=>{
122
+ const isBooleanTrueSkipSafeFetch = typeof existingCollection.upload === 'object' && existingCollection.upload.skipSafeFetch === true;
123
+ const isAllowListSkipSafeFetch = typeof existingCollection.upload === 'object' && Array.isArray(existingCollection.upload.skipSafeFetch);
124
+ if (isBooleanTrueSkipSafeFetch) {
125
+ return true;
126
+ } else if (isAllowListSkipSafeFetch) {
127
+ const existingSkipSafeFetch = typeof existingCollection.upload === 'object' && Array.isArray(existingCollection.upload.skipSafeFetch) ? existingCollection.upload.skipSafeFetch : [];
128
+ const hasExactLocalhostMatch = existingSkipSafeFetch.some((entry)=>{
129
+ const entryKeys = Object.keys(entry);
130
+ return entryKeys.length === 1 && entry.hostname === 'localhost';
131
+ });
132
+ const localhostEntry = process.env.NODE_ENV !== 'production' && !hasExactLocalhostMatch ? [
133
+ {
134
+ hostname: 'localhost'
135
+ }
136
+ ] : [];
137
+ return [
138
+ ...existingSkipSafeFetch,
139
+ ...localhostEntry
140
+ ];
141
+ }
142
+ if (process.env.NODE_ENV !== 'production') {
143
+ return [
144
+ {
145
+ hostname: 'localhost'
146
+ }
147
+ ];
148
+ }
149
+ return false;
150
+ };
19
151
  return {
20
- ...config,
21
- collections: (config.collections || []).map((existingCollection)=>{
22
- const options = allCollectionOptions[existingCollection.slug];
23
- if (options) {
24
- // If adapter is provided, use it to get fields
25
- const adapter = options.adapter ? options.adapter({
152
+ ...existingCollection,
153
+ fields,
154
+ hooks: {
155
+ ...existingCollection.hooks || {},
156
+ afterChange: [
157
+ ...existingCollection.hooks?.afterChange || [],
158
+ getAfterChangeHook({
159
+ adapter,
26
160
  collection: existingCollection,
27
- prefix: options.prefix
28
- }) : undefined;
29
- const fields = getFields({
161
+ collectionPrefix: options.prefix,
162
+ useCompositePrefixes
163
+ })
164
+ ],
165
+ afterDelete: [
166
+ ...existingCollection.hooks?.afterDelete || [],
167
+ getAfterDeleteHook({
30
168
  adapter,
31
- alwaysInsertFields: true,
32
169
  collection: existingCollection,
33
- disablePayloadAccessControl: options.disablePayloadAccessControl,
34
- generateFileURL: options.generateFileURL,
35
- prefix: options.prefix,
170
+ collectionPrefix: options.prefix,
36
171
  useCompositePrefixes
37
- });
38
- return {
39
- ...existingCollection,
40
- fields
41
- };
42
- }
43
- return existingCollection;
44
- })
172
+ })
173
+ ],
174
+ beforeChange: [
175
+ ...existingCollection.hooks?.beforeChange || [],
176
+ getNormalizeUploadPrefixHook({
177
+ collectionPrefix: options.prefix,
178
+ useCompositePrefixes
179
+ }),
180
+ getPreserveFileDataHook()
181
+ ]
182
+ },
183
+ upload: {
184
+ ...typeof existingCollection.upload === 'object' ? existingCollection.upload : {},
185
+ adapter: adapter.name,
186
+ ...uploadInstructions && {
187
+ uploadInstructions
188
+ },
189
+ disableLocalStorage: typeof options.disableLocalStorage === 'boolean' ? options.disableLocalStorage : true,
190
+ handlers,
191
+ skipSafeFetch: getSkipSafeFetchSetting()
192
+ }
45
193
  };
46
194
  }
47
- return config;
48
- }
49
- const initFunctions = [];
195
+ return existingCollection;
196
+ });
50
197
  return {
51
198
  ...config,
52
- collections: (config.collections || []).map((existingCollection)=>{
53
- const options = allCollectionOptions[existingCollection.slug];
54
- if (options?.adapter) {
55
- const adapter = options.adapter({
56
- collection: existingCollection,
57
- prefix: options.prefix
58
- });
59
- if (adapter.onInit) {
60
- initFunctions.push(adapter.onInit);
61
- }
62
- const fields = getFields({
63
- adapter,
64
- collection: existingCollection,
65
- disablePayloadAccessControl: options.disablePayloadAccessControl,
66
- generateFileURL: options.generateFileURL,
67
- prefix: options.prefix,
68
- useCompositePrefixes
69
- });
70
- const handlers = [
71
- ...typeof existingCollection.upload === 'object' && Array.isArray(existingCollection.upload.handlers) ? existingCollection.upload.handlers : []
72
- ];
73
- if (!options.disablePayloadAccessControl) {
74
- handlers.push(adapter.staticHandler);
75
- // Else if disablePayloadAccessControl: true and clientUploads is used
76
- // Build the "proxied" handler that responses only when the file was requested by client upload in addDataAndFileToRequest
77
- } else if (adapter.clientUploads) {
78
- handlers.push((req, args)=>{
79
- if ('clientUploadContext' in args.params) {
80
- return adapter.staticHandler(req, args);
81
- }
82
- });
83
- }
84
- const getSkipSafeFetchSetting = ()=>{
85
- if (options.disablePayloadAccessControl) {
86
- return true;
87
- }
88
- const isBooleanTrueSkipSafeFetch = typeof existingCollection.upload === 'object' && existingCollection.upload.skipSafeFetch === true;
89
- const isAllowListSkipSafeFetch = typeof existingCollection.upload === 'object' && Array.isArray(existingCollection.upload.skipSafeFetch);
90
- if (isBooleanTrueSkipSafeFetch) {
91
- return true;
92
- } else if (isAllowListSkipSafeFetch) {
93
- const existingSkipSafeFetch = typeof existingCollection.upload === 'object' && Array.isArray(existingCollection.upload.skipSafeFetch) ? existingCollection.upload.skipSafeFetch : [];
94
- const hasExactLocalhostMatch = existingSkipSafeFetch.some((entry)=>{
95
- const entryKeys = Object.keys(entry);
96
- return entryKeys.length === 1 && entry.hostname === 'localhost';
97
- });
98
- const localhostEntry = process.env.NODE_ENV !== 'production' && !hasExactLocalhostMatch ? [
99
- {
100
- hostname: 'localhost'
101
- }
102
- ] : [];
103
- return [
104
- ...existingSkipSafeFetch,
105
- ...localhostEntry
106
- ];
107
- }
108
- if (process.env.NODE_ENV !== 'production') {
109
- return [
110
- {
111
- hostname: 'localhost'
112
- }
113
- ];
114
- }
115
- return false;
116
- };
117
- return {
118
- ...existingCollection,
119
- fields,
120
- hooks: {
121
- ...existingCollection.hooks || {},
122
- afterChange: [
123
- ...existingCollection.hooks?.afterChange || [],
124
- getAfterChangeHook({
125
- adapter,
126
- collection: existingCollection
127
- })
128
- ],
129
- afterDelete: [
130
- ...existingCollection.hooks?.afterDelete || [],
131
- getAfterDeleteHook({
132
- adapter,
133
- collection: existingCollection
134
- })
135
- ],
136
- beforeChange: [
137
- ...existingCollection.hooks?.beforeChange || [],
138
- getPreserveFileDataHook()
139
- ]
140
- },
141
- upload: {
142
- ...typeof existingCollection.upload === 'object' ? existingCollection.upload : {},
143
- adapter: adapter.name,
144
- disableLocalStorage: typeof options.disableLocalStorage === 'boolean' ? options.disableLocalStorage : true,
145
- handlers,
146
- skipSafeFetch: getSkipSafeFetchSetting()
147
- }
148
- };
149
- }
150
- return existingCollection;
151
- }),
199
+ collections,
152
200
  onInit: async (payload)=>{
153
- initFunctions.forEach((fn)=>fn());
201
+ // Await each init so a provisioning failure fails Payload startup
202
+ // instead of becoming an unhandled rejection.
203
+ await Promise.all(initFunctions.map((fn)=>fn()));
154
204
  if (config.onInit) {
155
205
  await config.onInit(payload);
156
206
  }