Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/payload/src/config/__tests__/sanitize.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'

import { sanitizeConfig } from '../sanitize.js'

describe('sanitizeConfig', () => {
it('should populate collectionsBySlug map from collections array', async () => {
const config = await sanitizeConfig({
collections: [
{ slug: 'posts', fields: [] },
{ slug: 'users', auth: true, fields: [] },
],
globals: [{ slug: 'settings', fields: [] }],
} as any)

expect(config.collectionsBySlug).toBeInstanceOf(Map)
expect(config.collectionsBySlug.size).toBe(config.collections.length)
expect(config.collectionsBySlug.get('posts')).toBe(
config.collections.find((c) => c.slug === 'posts'),
)
})

it('should populate globalsBySlug map from globals array', async () => {
const config = await sanitizeConfig({
collections: [],
globals: [
{ slug: 'settings', fields: [] },
{ slug: 'nav', fields: [] },
],
} as any)

expect(config.globalsBySlug).toBeInstanceOf(Map)
expect(config.globalsBySlug.size).toBe(config.globals.length)
expect(config.globalsBySlug.get('settings')).toBe(
config.globals.find((g) => g.slug === 'settings'),
)
})
})
29 changes: 19 additions & 10 deletions packages/payload/src/config/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { en } from '@payloadcms/translations/languages/en'
import { deepMergeSimple } from '@payloadcms/translations/utilities'

import type { OrderableJoinInfo } from '../fields/config/sanitizeJoinField.js'
import type { CollectionSlug, GlobalSlug, SanitizedCollectionConfig } from '../index.js'
import type {
CollectionSlug,
GlobalSlug,
SanitizedCollectionConfig,
SanitizedGlobalConfig,
} from '../index.js'
import type { SanitizedJobsConfig } from '../queues/config/types/index.js'
import type {
Config,
Expand Down Expand Up @@ -460,15 +465,6 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
config.csrf!.push(config.serverURL!)
}

const uploadAdapters = new Set<string>()
// interact with all collections
for (const collection of config.collections!) {
// deduped upload adapters
if (collection.upload?.adapter) {
uploadAdapters.add(collection.upload.adapter)
}
}

if (!config.upload) {
config.upload = { adapters: [] }
}
Expand Down Expand Up @@ -504,5 +500,18 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC

await Promise.all(promises)

const collectionsBySlug = new Map<string, SanitizedCollectionConfig>()
for (const collection of config.collections as SanitizedCollectionConfig[]) {
collectionsBySlug.set(collection.slug, collection)
}

const globalsBySlug = new Map<string, SanitizedGlobalConfig>()
for (const global of config.globals ?? []) {
globalsBySlug.set(global.slug, global)
}

;(config as any).collectionsBySlug = collectionsBySlug
;(config as any).globalsBySlug = globalsBySlug

return config as SanitizedConfig
}
2 changes: 2 additions & 0 deletions packages/payload/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1586,10 +1586,12 @@ export type SanitizedConfig = {
} & DeepRequired<Config['admin']>
blocks?: FlattenedBlock[]
collections: SanitizedCollectionConfig[]
collectionsBySlug: Map<string, SanitizedCollectionConfig>
/** Default richtext editor to use for richText fields */
editor?: RichTextAdapter<any, any, any>
endpoints: Endpoint[]
globals: SanitizedGlobalConfig[]
globalsBySlug: Map<string, SanitizedGlobalConfig>
i18n: Required<I18nOptions>
jobs: SanitizedJobsConfig
localization: false | SanitizedLocalizationConfig
Expand Down
54 changes: 23 additions & 31 deletions packages/plugin-ecommerce/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import type { Config, Endpoint } from 'payload'

import { deepMergeSimple } from 'payload/shared'

import type { PluginDefaultTranslationsObject } from './translations/types.js'
import type { EcommercePluginConfig, SanitizedEcommercePluginConfig } from './types/index.js'

Expand Down Expand Up @@ -311,41 +309,35 @@ export const ecommercePlugin =
incomingConfig.i18n = {}
}

if (!incomingConfig.i18n?.translations) {
if (!incomingConfig.i18n.translations) {
incomingConfig.i18n.translations = {}
}

incomingConfig.i18n.translations = deepMergeSimple(
translations,
incomingConfig.i18n?.translations,
)

/**
* Merge plugin translations
* Merge plugin translations — only for languages the user has enabled.
* Plugins run before sanitize, so `supportedLanguages` may be undefined; sanitize will
* default it to `{ en }`, so we mirror that here. Plugin-ecommerce translations always
* win over user-provided ones for the `plugin-ecommerce` namespace.
*/
if (!incomingConfig.i18n) {
incomingConfig.i18n = {}
}
Object.entries(translations).forEach(([locale, pluginI18nObject]) => {
const typedLocale = locale as AcceptedLanguages
if (!incomingConfig.i18n!.translations) {
incomingConfig.i18n!.translations = {}
const supportedLanguageKeys = incomingConfig.i18n?.supportedLanguages
? Object.keys(incomingConfig.i18n.supportedLanguages)
: ['en']

for (const lang of supportedLanguageKeys) {
const pluginEntry = translations[lang as keyof typeof translations]
if (!pluginEntry) {
continue
}
if (!(typedLocale in incomingConfig.i18n!.translations)) {
incomingConfig.i18n!.translations[typedLocale] = {}
}
if (!('plugin-ecommerce' in incomingConfig.i18n!.translations[typedLocale]!)) {
;(incomingConfig.i18n!.translations[typedLocale] as PluginDefaultTranslationsObject)[
'plugin-ecommerce'
] = {} as PluginDefaultTranslationsObject['plugin-ecommerce']
}

;(incomingConfig.i18n!.translations[typedLocale] as PluginDefaultTranslationsObject)[
'plugin-ecommerce'
] = {
...pluginI18nObject.translations['plugin-ecommerce'],
}
})
const typedLocale = lang as AcceptedLanguages
const existing = (incomingConfig.i18n.translations[typedLocale] ?? {}) as Record<
string,
unknown
>
incomingConfig.i18n.translations[typedLocale] = {
...existing,
'plugin-ecommerce': pluginEntry.translations['plugin-ecommerce'],
} as PluginDefaultTranslationsObject
}

if (!incomingConfig.typescript) {
incomingConfig.typescript = {}
Expand Down
22 changes: 14 additions & 8 deletions packages/plugin-import-export/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,15 +191,21 @@ export const importExportPlugin =
}

/**
* Merge plugin translations
* Merge plugin translations — only for languages the user has enabled.
* Plugins run before sanitize, so `supportedLanguages` may be undefined; sanitize will
* default it to `{ en }`, so we mirror that here to avoid merging 30+ unused tables.
*/
const simplifiedTranslations = Object.entries(translations).reduce(
(acc, [key, value]) => {
acc[key] = value.translations
return acc
},
{} as Record<string, PluginDefaultTranslationsObject>,
)
const supportedLanguageKeys = config.i18n?.supportedLanguages
? Object.keys(config.i18n.supportedLanguages)
: ['en']

const simplifiedTranslations: Record<string, PluginDefaultTranslationsObject> = {}
for (const lang of supportedLanguageKeys) {
const entry = translations[lang as keyof typeof translations]
if (entry) {
simplifiedTranslations[lang] = entry.translations
}
}

config.i18n = {
...config.i18n,
Expand Down
18 changes: 11 additions & 7 deletions packages/plugin-search/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@ export const searchPlugin =
incomingPluginConfig.localize = shouldLocalize

if (collections) {
const labels = Object.fromEntries(
collections
.filter(({ slug }) => incomingPluginConfig.collections?.includes(slug))
.map((collection) => [collection.slug, collection.labels]),
)
// O(1) slug lookup for enabled-collection checks; replaces an Array.indexOf in the
// hook-attachment pass that was O(M) per collection.
const enabledSlugSet = new Set(incomingPluginConfig.collections ?? [])

const labels: Record<string, (typeof collections)[number]['labels']> = {}
for (const collection of collections) {
if (enabledSlugSet.has(collection.slug)) {
labels[collection.slug] = collection.labels
}
}

const pluginConfig: SanitizedSearchPluginConfig<ConfigTypes> = {
// write any config defaults here
Expand All @@ -42,8 +47,7 @@ export const searchPlugin =
?.map((collection) => {
const { hooks: existingHooks } = collection

const enabledCollections = pluginConfig.collections || []
const isEnabled = enabledCollections.indexOf(collection.slug) > -1
const isEnabled = enabledSlugSet.has(collection.slug)
if (isEnabled) {
return {
...collection,
Expand Down
49 changes: 33 additions & 16 deletions packages/plugin-seo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ export const seoPlugin =
},
]

const collectionConfigBySlug = new Map<string, NonNullable<Config['collections']>[number]>()
for (const c of config.collections ?? []) {
collectionConfigBySlug.set(c.slug, c)
}
const globalConfigBySlug = new Map<string, NonNullable<Config['globals']>[number]>()
for (const g of config.globals ?? []) {
globalConfigBySlug.set(g.slug, g)
}

return {
...config,
collections:
Expand Down Expand Up @@ -144,10 +153,12 @@ export const seoPlugin =
const result = pluginConfig.generateTitle
? await pluginConfig.generateTitle({
...data,
collectionConfig: config.collections?.find(
(c) => c.slug === reqData.collectionSlug,
),
globalConfig: config.globals?.find((g) => g.slug === reqData.globalSlug),
collectionConfig: reqData.collectionSlug
? collectionConfigBySlug.get(reqData.collectionSlug)
: undefined,
globalConfig: reqData.globalSlug
? globalConfigBySlug.get(reqData.globalSlug)
: undefined,
req,
} satisfies Parameters<GenerateTitle>[0])
: ''
Expand All @@ -168,10 +179,12 @@ export const seoPlugin =
const result = pluginConfig.generateDescription
? await pluginConfig.generateDescription({
...data,
collectionConfig: config.collections?.find(
(c) => c.slug === reqData.collectionSlug,
),
globalConfig: config.globals?.find((g) => g.slug === reqData.globalSlug),
collectionConfig: reqData.collectionSlug
? collectionConfigBySlug.get(reqData.collectionSlug)
: undefined,
globalConfig: reqData.globalSlug
? globalConfigBySlug.get(reqData.globalSlug)
: undefined,
req,
} satisfies Parameters<GenerateDescription>[0])
: ''
Expand All @@ -192,10 +205,12 @@ export const seoPlugin =
const result = pluginConfig.generateURL
? await pluginConfig.generateURL({
...data,
collectionConfig: config.collections?.find(
(c) => c.slug === reqData.collectionSlug,
),
globalConfig: config.globals?.find((g) => g.slug === reqData.globalSlug),
collectionConfig: reqData.collectionSlug
? collectionConfigBySlug.get(reqData.collectionSlug)
: undefined,
globalConfig: reqData.globalSlug
? globalConfigBySlug.get(reqData.globalSlug)
: undefined,
req,
} satisfies Parameters<GenerateURL>[0])
: ''
Expand All @@ -216,10 +231,12 @@ export const seoPlugin =
const result = pluginConfig.generateImage
? await pluginConfig.generateImage({
...data,
collectionConfig: config.collections?.find(
(c) => c.slug === reqData.collectionSlug,
),
globalConfig: config.globals?.find((g) => g.slug === reqData.globalSlug),
collectionConfig: reqData.collectionSlug
? collectionConfigBySlug.get(reqData.collectionSlug)
: undefined,
globalConfig: reqData.globalSlug
? globalConfigBySlug.get(reqData.globalSlug)
: undefined,
req,
} satisfies Parameters<GenerateImage>[0])
: ''
Expand Down
30 changes: 17 additions & 13 deletions packages/storage-s3/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type * as AWS from '@aws-sdk/client-s3'
import type { S3, S3ClientConfig } from '@aws-sdk/client-s3'
import type {
Adapter,
ClientUploadsConfig,
Expand All @@ -7,17 +7,14 @@ import type {

import type { SignedDownloadsConfig } from './getFile.js'

import { deleteFile } from './deleteFile.js'
import { generateURL } from './generateURL.js'
import { getFile } from './getFile.js'
import { uploadFile } from './uploadFile.js'

interface CreateS3AdapterArgs {
acl?: 'private' | 'public-read'
bucket: string
clientUploads?: ClientUploadsConfig
config: AWS.S3ClientConfig
getStorageClient: () => AWS.S3
config: S3ClientConfig
getStorageClient: () => S3
signedDownloads: SignedDownloadsConfig
useCompositePrefixes?: boolean
}
Expand Down Expand Up @@ -45,17 +42,22 @@ export function createS3Adapter({
useCompositePrefixes,
}),

handleDelete: ({ doc: { prefix: docPrefix = '' }, filename }) =>
deleteFile({
// Helpers below dynamic-import their @aws-sdk dependencies so the SDK only
// loads on the first request that actually needs it.
handleDelete: async ({ doc: { prefix: docPrefix = '' }, filename }) => {
const { deleteFile } = await import('./deleteFile.js')
return deleteFile({
bucket,
client: getStorageClient(),
collectionPrefix: prefix,
docPrefix,
filename,
useCompositePrefixes,
}),
})
},

handleUpload: async ({ data, file }) => {
const { uploadFile } = await import('./uploadFile.js')
await uploadFile({
acl,
bucket,
Expand All @@ -72,11 +74,12 @@ export function createS3Adapter({
return data
},

staticHandler: (
staticHandler: async (
req,
{ headers, params: { clientUploadContext, filename, prefix: prefixQueryParam } },
) =>
getFile({
) => {
const { getFile } = await import('./getFile.js')
return getFile({
bucket,
client: getStorageClient(),
clientUploadContext,
Expand All @@ -88,6 +91,7 @@ export function createS3Adapter({
req,
signedDownloads,
useCompositePrefixes,
}),
})
},
})
}
Loading
Loading