Compare commits

..

1 Commits

Author SHA1 Message Date
Jacob Fletcher
09d29a6ec9 feat(next): performs serverside redirect for limit query param 2025-02-24 16:46:49 -05:00
56 changed files with 843 additions and 866 deletions

37
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,37 @@
# Order matters. The last matching pattern takes precedence.
### Package Exports
**/exports/ @denolfe @jmikrut @DanRibbens
### Packages
/packages/plugin-cloud*/src/ @denolfe @jmikrut @DanRibbens
/packages/email-*/src/ @denolfe @jmikrut @DanRibbens
/packages/live-preview*/src/ @jacobsfletch
/packages/plugin-stripe/src/ @jacobsfletch
/packages/plugin-multi-tenant/src/ @JarrodMFlesch
/packages/richtext-*/src/ @AlessioGr
/packages/next/src/ @jmikrut @jacobsfletch @AlessioGr @JarrodMFlesch
/packages/ui/src/ @jmikrut @jacobsfletch @AlessioGr @JarrodMFlesch
/packages/storage-*/src/ @denolfe @jmikrut @DanRibbens
/packages/create-payload-app/src/ @denolfe @jmikrut @DanRibbens
/packages/eslint-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
### Templates
/templates/_data/ @denolfe @jmikrut @DanRibbens
/templates/_template/ @denolfe @jmikrut @DanRibbens
### Build Files
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr
**/jest.config.js @denolfe @jmikrut @DanRibbens @AlessioGr
### Root
/package.json @denolfe @jmikrut @DanRibbens
/tools/ @denolfe @jmikrut @DanRibbens
/.husky/ @denolfe @jmikrut @DanRibbens
/.vscode/ @denolfe @jmikrut @DanRibbens @AlessioGr
/.github/ @denolfe @jmikrut @DanRibbens

View File

@@ -11,7 +11,7 @@ import { DefaultListView, HydrateAuthProvider, ListQueryProvider } from '@payloa
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { renderFilters, renderTable, upsertPreferences } from '@payloadcms/ui/rsc'
import { formatAdminURL, mergeListSearchAndWhere } from '@payloadcms/ui/shared'
import { notFound } from 'next/navigation.js'
import { notFound, redirect } from 'next/navigation.js'
import { isNumber } from 'payload/shared'
import React, { Fragment } from 'react'
@@ -71,12 +71,13 @@ export const renderListView = async (
}
const query = queryFromArgs || queryFromReq
const limitFromQuery = isNumber(query?.limit) ? Number(query.limit) : undefined
const listPreferences = await upsertPreferences<ListPreferences>({
key: `${collectionSlug}-list`,
req,
value: {
limit: isNumber(query?.limit) ? Number(query.limit) : undefined,
limit: limitFromQuery,
sort: query?.sort as string,
},
})
@@ -238,6 +239,19 @@ export const renderListView = async (
}
export const ListView: React.FC<RenderListViewArgs> = async (args) => {
const {
initPageResult: { collectionConfig, req },
} = args
if (!req.query?.limit) {
return redirect(
`${req.url}?${new URLSearchParams({
...req.query,
limit: String(collectionConfig.admin.pagination.defaultLimit),
}).toString()}`,
)
}
try {
const { List: RenderedList } = await renderListView({ ...args, enableRowSelections: true })
return RenderedList

View File

@@ -1,13 +1,19 @@
import type * as AWS from '@aws-sdk/client-s3'
import type { CognitoUserSession } from 'amazon-cognito-identity-js'
import type { GetStorageClient } from './refreshSession.js'
import { CognitoIdentityClient } from '@aws-sdk/client-cognito-identity'
import * as AWS from '@aws-sdk/client-s3'
import { fromCognitoIdentityPool } from '@aws-sdk/credential-providers'
import { refreshSession } from './refreshSession.js'
import { authAsCognitoUser } from './authAsCognitoUser.js'
export let storageClient: AWS.S3 | null = null
export let session: CognitoUserSession | null = null
export let identityID: string
export type GetStorageClient = () => Promise<{
identityID: string
storageClient: AWS.S3
}>
let storageClient: AWS.S3 | null = null
let session: CognitoUserSession | null = null
let identityID: string
export const getStorageClient: GetStorageClient = async () => {
if (storageClient && session?.isValid()) {
@@ -17,8 +23,6 @@ export const getStorageClient: GetStorageClient = async () => {
}
}
;({ identityID, session, storageClient } = await refreshSession())
if (!process.env.PAYLOAD_CLOUD_PROJECT_ID) {
throw new Error('PAYLOAD_CLOUD_PROJECT_ID is required')
}
@@ -29,6 +33,34 @@ export const getStorageClient: GetStorageClient = async () => {
throw new Error('PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID is required')
}
session = await authAsCognitoUser(
process.env.PAYLOAD_CLOUD_PROJECT_ID,
process.env.PAYLOAD_CLOUD_COGNITO_PASSWORD,
)
const cognitoIdentity = new CognitoIdentityClient({
credentials: fromCognitoIdentityPool({
clientConfig: {
region: 'us-east-1',
},
identityPoolId: process.env.PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID,
logins: {
[`cognito-idp.us-east-1.amazonaws.com/${process.env.PAYLOAD_CLOUD_COGNITO_USER_POOL_ID}`]:
session.getIdToken().getJwtToken(),
},
}),
})
const credentials = await cognitoIdentity.config.credentials()
// @ts-expect-error - Incorrect AWS types
identityID = credentials.identityId
storageClient = new AWS.S3({
credentials,
region: process.env.PAYLOAD_CLOUD_BUCKET_REGION,
})
return {
identityID,
storageClient,

View File

@@ -1,46 +0,0 @@
import { CognitoIdentityClient } from '@aws-sdk/client-cognito-identity'
import * as AWS from '@aws-sdk/client-s3'
import { fromCognitoIdentityPool } from '@aws-sdk/credential-providers'
import { authAsCognitoUser } from './authAsCognitoUser.js'
export type GetStorageClient = () => Promise<{
identityID: string
storageClient: AWS.S3
}>
export const refreshSession = async () => {
const session = await authAsCognitoUser(
process.env.PAYLOAD_CLOUD_PROJECT_ID || '',
process.env.PAYLOAD_CLOUD_COGNITO_PASSWORD || '',
)
const cognitoIdentity = new CognitoIdentityClient({
credentials: fromCognitoIdentityPool({
clientConfig: {
region: 'us-east-1',
},
identityPoolId: process.env.PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID || '',
logins: {
[`cognito-idp.us-east-1.amazonaws.com/${process.env.PAYLOAD_CLOUD_COGNITO_USER_POOL_ID}`]:
session.getIdToken().getJwtToken(),
},
}),
})
const credentials = await cognitoIdentity.config.credentials()
// @ts-expect-error - Incorrect AWS types
const identityID = credentials.identityId
const storageClient = new AWS.S3({
credentials,
region: process.env.PAYLOAD_CLOUD_BUCKET_REGION,
})
return {
identityID,
session,
storageClient,
}
}

View File

@@ -1,7 +1,7 @@
import type { ImportMap } from '../../bin/generateImportMap/index.js'
import type { SanitizedConfig } from '../../config/types.js'
import type { PaginatedDocs } from '../../database/types.js'
import type { CollectionSlug, ColumnPreference } from '../../index.js'
import type { CollectionSlug } from '../../index.js'
import type { PayloadRequest, Sort, Where } from '../../types/index.js'
export type DefaultServerFunctionArgs = {
@@ -50,7 +50,7 @@ export type ListQuery = {
export type BuildTableStateArgs = {
collectionSlug: string | string[]
columns?: ColumnPreference[]
columns?: { accessor: string; active: boolean }[]
docs?: PaginatedDocs['docs']
enableRowSelections?: boolean
parent?: {

View File

@@ -1,19 +1,20 @@
// @ts-strict-ignore
import type { AuthStrategyFunctionArgs, AuthStrategyResult } from './index.js'
export const executeAuthStrategies = async (
args: AuthStrategyFunctionArgs,
): Promise<AuthStrategyResult> => {
if (!args.payload.authStrategies?.length) {
return { user: null }
}
return args.payload.authStrategies.reduce(
async (accumulatorPromise, strategy) => {
const result: AuthStrategyResult = await accumulatorPromise
if (!result.user) {
// add the configured AuthStrategy `name` to the strategy function args
args.strategyName = strategy.name
for (const strategy of args.payload.authStrategies) {
// add the configured AuthStrategy `name` to the strategy function args
args.strategyName = strategy.name
const result = await strategy.authenticate(args)
if (result.user) {
return strategy.authenticate(args)
}
return result
}
}
return { user: null }
},
Promise.resolve({ user: null }),
)
}

View File

@@ -64,18 +64,18 @@ export const forgotPasswordOperation = async <TSlug extends CollectionSlug>(
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'forgotPassword',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'forgotPassword',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },
@@ -190,11 +190,10 @@ export const forgotPasswordOperation = async <TSlug extends CollectionSlug>(
// afterForgotPassword - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterForgotPassword?.length) {
for (const hook of collectionConfig.hooks.afterForgotPassword) {
await hook({ args, collection: args.collection?.config, context: req.context })
}
}
await collectionConfig.hooks.afterForgotPassword.reduce(async (priorHook, hook) => {
await priorHook
await hook({ args, collection: args.collection?.config, context: req.context })
}, Promise.resolve())
// /////////////////////////////////////
// afterOperation - Collection

View File

@@ -51,18 +51,18 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'login',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'login',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },
@@ -227,17 +227,17 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
// beforeLogin - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeLogin?.length) {
for (const hook of collectionConfig.hooks.beforeLogin) {
user =
(await hook({
collection: args.collection?.config,
context: args.req.context,
req: args.req,
user,
})) || user
}
}
await collectionConfig.hooks.beforeLogin.reduce(async (priorHook, hook) => {
await priorHook
user =
(await hook({
collection: args.collection?.config,
context: args.req.context,
req: args.req,
user,
})) || user
}, Promise.resolve())
const { exp, token } = await jwtSign({
fieldsToSign,
@@ -251,18 +251,18 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
// afterLogin - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterLogin?.length) {
for (const hook of collectionConfig.hooks.afterLogin) {
user =
(await hook({
collection: args.collection?.config,
context: args.req.context,
req: args.req,
token,
user,
})) || user
}
}
await collectionConfig.hooks.afterLogin.reduce(async (priorHook, hook) => {
await priorHook
user =
(await hook({
collection: args.collection?.config,
context: args.req.context,
req: args.req,
token,
user,
})) || user
}, Promise.resolve())
// /////////////////////////////////////
// afterRead - Fields
@@ -286,17 +286,17 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
user =
(await hook({
collection: args.collection?.config,
context: req.context,
doc: user,
req,
})) || user
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
user =
(await hook({
collection: args.collection?.config,
context: req.context,
doc: user,
req,
})) || user
}, Promise.resolve())
let result: { user: DataFromCollectionSlug<TSlug> } & Result = {
exp,

View File

@@ -25,16 +25,16 @@ export const logoutOperation = async (incomingArgs: Arguments): Promise<boolean>
throw new APIError('Incorrect collection', httpStatus.FORBIDDEN)
}
if (collectionConfig.hooks?.afterLogout?.length) {
for (const hook of collectionConfig.hooks.afterLogout) {
args =
(await hook({
collection: args.collection?.config,
context: req.context,
req,
})) || args
}
}
await collectionConfig.hooks.afterLogout.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
collection: args.collection?.config,
context: req.context,
req,
})) || args
}, Promise.resolve())
return true
}

View File

@@ -86,17 +86,17 @@ export const meOperation = async (args: Arguments): Promise<MeOperationResult> =
// After Me - Collection
// /////////////////////////////////////
if (collection.config.hooks?.afterMe?.length) {
for (const hook of collection.config.hooks.afterMe) {
result =
(await hook({
collection: collection?.config,
context: req.context,
req,
response: result,
})) || result
}
}
await collection.config.hooks.afterMe.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collection?.config,
context: req.context,
req,
response: result,
})) || result
}, Promise.resolve())
return result
}

View File

@@ -35,8 +35,10 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
await args.collection.config.hooks.beforeOperation.reduce(
async (priorHook: BeforeOperationHook | Promise<void>, hook: BeforeOperationHook) => {
await priorHook
args =
(await hook({
args,
@@ -45,8 +47,9 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
operation: 'refresh',
req: args.req,
})) || args
}
}
},
Promise.resolve(),
)
// /////////////////////////////////////
// Refresh
@@ -119,18 +122,18 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
// After Refresh - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRefresh?.length) {
for (const hook of collectionConfig.hooks.afterRefresh) {
result =
(await hook({
collection: args.collection?.config,
context: args.req.context,
exp: result.exp,
req: args.req,
token: result.refreshedToken,
})) || result
}
}
await collectionConfig.hooks.afterRefresh.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: args.collection?.config,
context: args.req.context,
exp: result.exp,
req: args.req,
token: result.refreshedToken,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterOperation - Collection

View File

@@ -91,17 +91,17 @@ export const resetPasswordOperation = async (args: Arguments): Promise<Result> =
// beforeValidate - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeValidate?.length) {
for (const hook of collectionConfig.hooks.beforeValidate) {
await hook({
collection: args.collection?.config,
context: req.context,
data: user,
operation: 'update',
req,
})
}
}
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await priorHook
await hook({
collection: args.collection?.config,
context: req.context,
data: user,
operation: 'update',
req,
})
}, Promise.resolve())
// /////////////////////////////////////
// Update new password

View File

@@ -44,9 +44,7 @@ const batchAndLoadDocs =
*
**/
const batchByFindArgs = {}
for (const key of keys) {
const batchByFindArgs = keys.reduce((batches, key) => {
const [
transactionID,
collection,
@@ -79,16 +77,27 @@ const batchAndLoadDocs =
const batchKey = JSON.stringify(batchKeyArray)
const idType = payload.collections?.[collection].customIDType || payload.db.defaultIDType
const sanitizedID = idType === 'number' ? parseFloat(id) : id
let sanitizedID: number | string = id
if (idType === 'number') {
sanitizedID = parseFloat(id)
}
if (isValidID(sanitizedID, idType)) {
batchByFindArgs[batchKey] = [...(batchByFindArgs[batchKey] || []), sanitizedID]
return {
...batches,
[batchKey]: [...(batches[batchKey] || []), sanitizedID],
}
}
}
return batches
}, {})
// Run find requests one after another, so as to not hang transactions
for (const [batchKey, ids] of Object.entries(batchByFindArgs)) {
await Object.entries(batchByFindArgs).reduce(async (priorFind, [batchKey, ids]) => {
await priorFind
const [
transactionID,
collection,
@@ -128,7 +137,8 @@ const batchAndLoadDocs =
// For each returned doc, find index in original keys
// Inject doc within docs array if index exists
for (const doc of result.docs) {
result.docs.forEach((doc) => {
const docKey = createDataloaderCacheKey({
collectionSlug: collection,
currentDepth,
@@ -148,8 +158,8 @@ const batchAndLoadDocs =
if (docsIndex > -1) {
docs[docsIndex] = doc
}
}
}
})
}, Promise.resolve())
// Return docs array,
// which has now been injected with all fetched docs

View File

@@ -28,18 +28,18 @@ export const countOperation = async <TSlug extends CollectionSlug>(
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'count',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'count',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },

View File

@@ -28,18 +28,18 @@ export const countVersionsOperation = async <TSlug extends CollectionSlug>(
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'countVersions',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'countVersions',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },

View File

@@ -78,8 +78,10 @@ export const createOperation = async <
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
await args.collection.config.hooks.beforeOperation.reduce(
async (priorHook: BeforeOperationHook | Promise<void>, hook: BeforeOperationHook) => {
await priorHook
args =
(await hook({
args,
@@ -88,8 +90,9 @@ export const createOperation = async <
operation: 'create',
req: args.req,
})) || args
}
}
},
Promise.resolve(),
)
const {
autosave = false,
@@ -180,8 +183,10 @@ export const createOperation = async <
// beforeValidate - Collections
// /////////////////////////////////////
if (collectionConfig.hooks.beforeValidate?.length) {
for (const hook of collectionConfig.hooks.beforeValidate) {
await collectionConfig.hooks.beforeValidate.reduce(
async (priorHook: BeforeValidateHook | Promise<void>, hook: BeforeValidateHook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
@@ -191,26 +196,27 @@ export const createOperation = async <
originalDoc: duplicatedFromDoc,
req,
})) || data
}
}
},
Promise.resolve(),
)
// /////////////////////////////////////
// beforeChange - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeChange?.length) {
for (const hook of collectionConfig.hooks.beforeChange) {
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'create',
originalDoc: duplicatedFromDoc,
req,
})) || data
}
}
await collectionConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'create',
originalDoc: duplicatedFromDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Fields
@@ -326,17 +332,17 @@ export const createOperation = async <
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterChange - Fields
@@ -357,8 +363,10 @@ export const createOperation = async <
// afterChange - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterChange?.length) {
for (const hook of collectionConfig.hooks.afterChange) {
await collectionConfig.hooks.afterChange.reduce(
async (priorHook: AfterChangeHook | Promise<void>, hook: AfterChangeHook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
@@ -368,8 +376,9 @@ export const createOperation = async <
previousDoc: {},
req: args.req,
})) || result
}
}
},
Promise.resolve(),
)
// /////////////////////////////////////
// afterOperation - Collection

View File

@@ -54,8 +54,10 @@ export const deleteOperation = async <
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
await args.collection.config.hooks.beforeOperation.reduce(
async (priorHook: BeforeOperationHook | Promise<void>, hook: BeforeOperationHook) => {
await priorHook
args =
(await hook({
args,
@@ -64,8 +66,9 @@ export const deleteOperation = async <
operation: 'delete',
req: args.req,
})) || args
}
}
},
Promise.resolve(),
)
const {
collection: { config: collectionConfig },
@@ -144,16 +147,16 @@ export const deleteOperation = async <
// beforeDelete - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeDelete?.length) {
for (const hook of collectionConfig.hooks.beforeDelete) {
await hook({
id,
collection: collectionConfig,
context: req.context,
req,
})
}
}
await collectionConfig.hooks.beforeDelete.reduce(async (priorHook, hook) => {
await priorHook
return hook({
id,
collection: collectionConfig,
context: req.context,
req,
})
}, Promise.resolve())
await deleteAssociatedFiles({
collectionConfig,
@@ -226,34 +229,34 @@ export const deleteOperation = async <
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result || doc,
req,
})) || result
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result || doc,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterDelete - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterDelete?.length) {
for (const hook of collectionConfig.hooks.afterDelete) {
result =
(await hook({
id,
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}
}
await collectionConfig.hooks.afterDelete.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
id,
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// 8. Return results

View File

@@ -48,8 +48,10 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
await args.collection.config.hooks.beforeOperation.reduce(
async (priorHook: BeforeOperationHook | Promise<void>, hook: BeforeOperationHook) => {
await priorHook
args =
(await hook({
args,
@@ -58,8 +60,9 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
operation: 'delete',
req: args.req,
})) || args
}
}
},
Promise.resolve(),
)
const {
id,
@@ -92,16 +95,16 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
// beforeDelete - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeDelete?.length) {
for (const hook of collectionConfig.hooks.beforeDelete) {
await hook({
id,
collection: collectionConfig,
context: req.context,
req,
})
}
}
await collectionConfig.hooks.beforeDelete.reduce(async (priorHook, hook) => {
await priorHook
return hook({
id,
collection: collectionConfig,
context: req.context,
req,
})
}, Promise.resolve())
// /////////////////////////////////////
// Retrieve document
@@ -212,34 +215,34 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterDelete - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterDelete?.length) {
for (const hook of collectionConfig.hooks.afterDelete) {
result =
(await hook({
id,
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}
}
await collectionConfig.hooks.afterDelete.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
id,
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterOperation - Collection

View File

@@ -63,18 +63,18 @@ export const findOperation = async <
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'read',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'read',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },
@@ -257,7 +257,9 @@ export const findOperation = async <
result.docs.map(async (doc) => {
let docRef = doc
for (const hook of collectionConfig.hooks.beforeRead) {
await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
await priorHook
docRef =
(await hook({
collection: collectionConfig,
@@ -266,7 +268,7 @@ export const findOperation = async <
query: fullWhere,
req,
})) || docRef
}
}, Promise.resolve())
return docRef
}),
@@ -308,7 +310,9 @@ export const findOperation = async <
result.docs.map(async (doc) => {
let docRef = doc
for (const hook of collectionConfig.hooks.afterRead) {
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
docRef =
(await hook({
collection: collectionConfig,
@@ -318,7 +322,7 @@ export const findOperation = async <
query: fullWhere,
req,
})) || doc
}
}, Promise.resolve())
return docRef
}),

View File

@@ -54,18 +54,18 @@ export const findByIDOperation = async <
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'read',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'read',
req: args.req,
})) || args
}, Promise.resolve())
const {
id,
@@ -221,18 +221,18 @@ export const findByIDOperation = async <
// beforeRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeRead?.length) {
for (const hook of collectionConfig.hooks.beforeRead) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
query: findOneArgs.where,
req,
})) || result
}
}
await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
query: findOneArgs.where,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterRead - Fields
@@ -259,18 +259,18 @@ export const findByIDOperation = async <
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
query: findOneArgs.where,
req,
})) || result
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
query: findOneArgs.where,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterOperation - Collection

View File

@@ -101,18 +101,18 @@ export const findVersionByIDOperation = async <TData extends TypeWithID = any>(
// beforeRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeRead?.length) {
for (const hook of collectionConfig.hooks.beforeRead) {
result.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result.version,
query: fullWhere,
req,
})) || result.version
}
}
await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
await priorHook
result.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result.version,
query: fullWhere,
req,
})) || result.version
}, Promise.resolve())
// /////////////////////////////////////
// afterRead - Fields
@@ -139,18 +139,18 @@ export const findVersionByIDOperation = async <TData extends TypeWithID = any>(
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
result.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result.version,
query: fullWhere,
req,
})) || result.version
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result.version,
query: fullWhere,
req,
})) || result.version
}, Promise.resolve())
// /////////////////////////////////////
// Return results

View File

@@ -96,19 +96,18 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
if (!docRef.version) {
;(docRef as any).version = {}
}
await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
await priorHook
if (collectionConfig.hooks?.beforeRead?.length) {
for (const hook of collectionConfig.hooks.beforeRead) {
docRef.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: docRef.version,
query: fullWhere,
req,
})) || docRef.version
}
}
docRef.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: docRef.version,
query: fullWhere,
req,
})) || docRef.version
}, Promise.resolve())
return docRef
}),
@@ -148,7 +147,9 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
result.docs.map(async (doc) => {
const docRef = doc
for (const hook of collectionConfig.hooks.afterRead) {
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
docRef.version =
(await hook({
collection: collectionConfig,
@@ -158,7 +159,7 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
query: fullWhere,
req,
})) || doc.version
}
}, Promise.resolve())
return docRef
}),

View File

@@ -165,17 +165,17 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterChange - Fields
@@ -196,19 +196,19 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
// afterChange - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterChange?.length) {
for (const hook of collectionConfig.hooks.afterChange) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'update',
previousDoc: prevDocWithLocales,
req,
})) || result
}
}
await collectionConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'update',
previousDoc: prevDocWithLocales,
req,
})) || result
}, Promise.resolve())
return result
} catch (error: unknown) {

View File

@@ -62,18 +62,18 @@ export const updateOperation = async <
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'update',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'update',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },

View File

@@ -64,18 +64,18 @@ export const updateByIDOperation = async <
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'update',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'update',
req: args.req,
})) || args
}, Promise.resolve())
if (args.publishSpecificLocale) {
args.req.locale = args.publishSpecificLocale

View File

@@ -171,19 +171,19 @@ export const updateDocument = async <
// beforeValidate - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeValidate?.length) {
for (const hook of collectionConfig.hooks.beforeValidate) {
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}
}
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// Write files to local storage
@@ -197,19 +197,19 @@ export const updateDocument = async <
// beforeChange - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeChange?.length) {
for (const hook of collectionConfig.hooks.beforeChange) {
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}
}
await collectionConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Fields
@@ -338,17 +338,17 @@ export const updateDocument = async <
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterChange - Fields
@@ -369,19 +369,19 @@ export const updateDocument = async <
// afterChange - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterChange?.length) {
for (const hook of collectionConfig.hooks.afterChange) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'update',
previousDoc: originalDoc,
req,
})) || result
}
}
await collectionConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'update',
previousDoc: originalDoc,
req,
})) || result
}, Promise.resolve())
return result as TransformCollectionWithSelect<TSlug, TSelect>
}

View File

@@ -125,8 +125,10 @@ export const buildAfterOperation = async <
let newResult = result as OperationResult<TOperationGeneric, O>
if (args.collection.config.hooks?.afterOperation?.length) {
for (const hook of args.collection.config.hooks.afterOperation) {
await args.collection.config.hooks.afterOperation.reduce(
async (priorHook, hook: AfterOperationHook<TOperationGeneric>) => {
await priorHook
const hookResult = await hook({
args,
collection,
@@ -138,8 +140,9 @@ export const buildAfterOperation = async <
if (hookResult !== undefined) {
newResult = hookResult as OperationResult<TOperationGeneric, O>
}
}
}
},
Promise.resolve(),
)
return newResult
}

View File

@@ -9,10 +9,11 @@ import { sanitizeConfig } from './sanitize.js'
*/
export async function buildConfig(config: Config): Promise<SanitizedConfig> {
if (Array.isArray(config.plugins)) {
let configAfterPlugins = config
for (const plugin of config.plugins) {
configAfterPlugins = await plugin(configAfterPlugins)
}
const configAfterPlugins = await config.plugins.reduce(async (acc, plugin) => {
const configAfterPlugin = await acc
return plugin(configAfterPlugin)
}, Promise.resolve(config))
return await sanitizeConfig(configAfterPlugins)
}

View File

@@ -179,7 +179,10 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
}))
} else {
// is Locale[], so convert to string[] for localeCodes
config.localization.localeCodes = config.localization.locales.map((locale) => locale.code)
config.localization.localeCodes = config.localization.locales.reduce((locales, locale) => {
locales.push(locale.code)
return locales
}, [] as string[])
config.localization.locales = (
config.localization as LocalizationConfigWithLabels

View File

@@ -133,17 +133,17 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
// Execute before global hook
// /////////////////////////////////////
if (globalConfig.hooks?.beforeRead?.length) {
for (const hook of globalConfig.hooks.beforeRead) {
doc =
(await hook({
context: req.context,
doc,
global: globalConfig,
req,
})) || doc
}
}
await globalConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
await priorHook
doc =
(await hook({
context: req.context,
doc,
global: globalConfig,
req,
})) || doc
}, Promise.resolve())
// /////////////////////////////////////
// Execute globalType field if not selected
@@ -182,17 +182,17 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
// Execute after global hook
// /////////////////////////////////////
if (globalConfig.hooks?.afterRead?.length) {
for (const hook of globalConfig.hooks.afterRead) {
doc =
(await hook({
context: req.context,
doc,
global: globalConfig,
req,
})) || doc
}
}
await globalConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
doc =
(await hook({
context: req.context,
doc,
global: globalConfig,
req,
})) || doc
}, Promise.resolve())
// /////////////////////////////////////
// Return results

View File

@@ -102,17 +102,17 @@ export const findVersionByIDOperation = async <T extends TypeWithVersion<T> = an
// beforeRead - Collection
// /////////////////////////////////////
if (globalConfig.hooks?.beforeRead?.length) {
for (const hook of globalConfig.hooks.beforeRead) {
result =
(await hook({
context: req.context,
doc: result.version,
global: globalConfig,
req,
})) || result.version
}
}
await globalConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
context: req.context,
doc: result.version,
global: globalConfig,
req,
})) || result.version
}, Promise.resolve())
// /////////////////////////////////////
// afterRead - Fields
@@ -139,18 +139,18 @@ export const findVersionByIDOperation = async <T extends TypeWithVersion<T> = an
// afterRead - Global
// /////////////////////////////////////
if (globalConfig.hooks?.afterRead?.length) {
for (const hook of globalConfig.hooks.afterRead) {
result.version =
(await hook({
context: req.context,
doc: result.version,
global: globalConfig,
query: findGlobalVersionsArgs.where,
req,
})) || result.version
}
}
await globalConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result.version =
(await hook({
context: req.context,
doc: result.version,
global: globalConfig,
query: findGlobalVersionsArgs.where,
req,
})) || result.version
}, Promise.resolve())
return result
} catch (error: unknown) {

View File

@@ -126,12 +126,15 @@ export const findVersionsOperation = async <T extends TypeWithVersion<T>>(
// afterRead - Global
// /////////////////////////////////////
if (globalConfig.hooks?.afterRead?.length) {
result.docs = await Promise.all(
result = {
...result,
docs: await Promise.all(
result.docs.map(async (doc) => {
const docRef = doc
for (const hook of globalConfig.hooks.afterRead) {
await globalConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
docRef.version =
(await hook({
context: req.context,
@@ -141,11 +144,11 @@ export const findVersionsOperation = async <T extends TypeWithVersion<T>>(
query: fullWhere,
req,
})) || doc.version
}
}, Promise.resolve())
return docRef
}),
)
),
}
// /////////////////////////////////////

View File

@@ -143,17 +143,17 @@ export const restoreVersionOperation = async <T extends TypeWithVersion<T> = any
// afterRead - Global
// /////////////////////////////////////
if (globalConfig.hooks?.afterRead?.length) {
for (const hook of globalConfig.hooks.afterRead) {
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
req,
})) || result
}
}
await globalConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterChange - Fields
@@ -174,18 +174,18 @@ export const restoreVersionOperation = async <T extends TypeWithVersion<T> = any
// afterChange - Global
// /////////////////////////////////////
if (globalConfig.hooks?.afterChange?.length) {
for (const hook of globalConfig.hooks.afterChange) {
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
previousDoc,
req,
})) || result
}
}
await globalConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
previousDoc,
req,
})) || result
}, Promise.resolve())
if (shouldCommit) {
await commitTransaction(req)

View File

@@ -168,35 +168,35 @@ export const updateOperation = async <
// beforeValidate - Global
// /////////////////////////////////////
if (globalConfig.hooks?.beforeValidate?.length) {
for (const hook of globalConfig.hooks.beforeValidate) {
data =
(await hook({
context: req.context,
data,
global: globalConfig,
originalDoc,
req,
})) || data
}
}
await globalConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
context: req.context,
data,
global: globalConfig,
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Global
// /////////////////////////////////////
if (globalConfig.hooks?.beforeChange?.length) {
for (const hook of globalConfig.hooks.beforeChange) {
data =
(await hook({
context: req.context,
data,
global: globalConfig,
originalDoc,
req,
})) || data
}
}
await globalConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
context: req.context,
data,
global: globalConfig,
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Fields
@@ -326,17 +326,17 @@ export const updateOperation = async <
// afterRead - Global
// /////////////////////////////////////
if (globalConfig.hooks?.afterRead?.length) {
for (const hook of globalConfig.hooks.afterRead) {
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
req,
})) || result
}
}
await globalConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterChange - Fields
@@ -357,18 +357,18 @@ export const updateOperation = async <
// afterChange - Global
// /////////////////////////////////////
if (globalConfig.hooks?.afterChange?.length) {
for (const hook of globalConfig.hooks.afterChange) {
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
previousDoc: originalDoc,
req,
})) || result
}
}
await globalConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
previousDoc: originalDoc,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// Return results

View File

@@ -1374,7 +1374,6 @@ export { restoreVersionOperation as restoreVersionOperationGlobal } from './glob
export { updateOperation as updateOperationGlobal } from './globals/operations/update.js'
export type {
CollapsedPreferences,
ColumnPreference,
DocumentPreferences,
FieldsPreferences,
InsideFieldsPreferences,

View File

@@ -1,19 +0,0 @@
/**
* @todo remove this function and subsequent hooks in v4
* They are used to transform the old shape of `columnPreferences` to new shape
* i.e. ({ accessor: string, active: boolean })[] to ({ [accessor: string]: boolean })[]
* In v4 can we use the new shape directly
*/
export const migrateColumns = (value: Record<string, any>) => {
if (value && typeof value === 'object' && 'columns' in value && Array.isArray(value.columns)) {
value.columns = value.columns.map((col) => {
if ('accessor' in col) {
return { [col.accessor]: col.active }
}
return col
})
}
return value
}

View File

@@ -2,7 +2,6 @@
import type { CollectionConfig } from '../collections/config/types.js'
import type { Access, Config } from '../config/types.js'
import { migrateColumns } from './migrateColumns.js'
import { deleteHandler } from './requestHandlers/delete.js'
import { findByIDHandler } from './requestHandlers/findOne.js'
import { updateHandler } from './requestHandlers/update.js'
@@ -77,14 +76,6 @@ const getPreferencesCollection = (config: Config): CollectionConfig => ({
{
name: 'value',
type: 'json',
/**
* @todo remove these hooks in v4
* See `migrateColumns` for more information
*/
hooks: {
afterRead: [({ value }) => migrateColumns(value)],
beforeValidate: [({ value }) => migrateColumns(value)],
},
validate: (value) => {
if (value) {
try {

View File

@@ -28,12 +28,8 @@ export type DocumentPreferences = {
fields: FieldsPreferences
}
export type ColumnPreference = {
[key: string]: boolean
}
export type ListPreferences = {
columns?: ColumnPreference[]
columns?: { accessor: string; active: boolean }[]
limit?: number
sort?: string
}

View File

@@ -1156,13 +1156,14 @@ export function configToJSONSchema(
)
: {}
const blocksDefinition: JSONSchema4 | undefined = {
type: 'object',
additionalProperties: false,
properties: {},
required: [],
}
let blocksDefinition: JSONSchema4 | undefined = undefined
if (config?.blocks?.length) {
blocksDefinition = {
type: 'object',
additionalProperties: false,
properties: {},
required: [],
}
for (const block of config.blocks) {
const blockFieldSchemas = fieldsToJSONSchema(
collectionIDFieldTypes,

View File

@@ -4,9 +4,12 @@ import { useModal } from '@faceless-ui/modal'
import React from 'react'
import { useTranslation } from '../../../providers/Translation/index.js'
import { ConfirmationModal } from '../../ConfirmationModal/index.js'
import { Button } from '../../Button/index.js'
import { FullscreenModal } from '../../FullscreenModal/index.js'
import { useBulkUpload } from '../index.js'
export const discardBulkUploadModalSlug = 'bulk-upload--discard-without-saving'
const baseClass = 'leave-without-saving'
export function DiscardWithoutSaving() {
const { t } = useTranslation()
@@ -23,14 +26,21 @@ export function DiscardWithoutSaving() {
}, [closeModal, drawerSlug])
return (
<ConfirmationModal
body={t('general:changesNotSaved')}
cancelLabel={t('general:stayOnThisPage')}
confirmLabel={t('general:leaveAnyway')}
heading={t('general:leaveWithoutSaving')}
modalSlug={discardBulkUploadModalSlug}
onCancel={onCancel}
onConfirm={onConfirm}
/>
<FullscreenModal className={baseClass} slug={discardBulkUploadModalSlug}>
<div className={`${baseClass}__wrapper`}>
<div className={`${baseClass}__content`}>
<h1>{t('general:leaveWithoutSaving')}</h1>
<p>{t('general:changesNotSaved')}</p>
</div>
<div className={`${baseClass}__controls`}>
<Button buttonStyle="secondary" onClick={onCancel} size="large">
{t('general:stayOnThisPage')}
</Button>
<Button onClick={onConfirm} size="large">
{t('general:leaveAnyway')}
</Button>
</div>
</div>
</FullscreenModal>
)
}

View File

@@ -2,7 +2,6 @@
import type {
CollectionSlug,
Column,
ColumnPreference,
JoinFieldClient,
ListQuery,
PaginatedDocs,
@@ -26,6 +25,7 @@ import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { hoistQueryParamsToAnd } from '../../utilities/mergeListSearchAndWhere.js'
import { AnimateHeight } from '../AnimateHeight/index.js'
import './index.scss'
import { ColumnSelector } from '../ColumnSelector/index.js'
import { useDocumentDrawer } from '../DocumentDrawer/index.js'
import { Popup, PopupList } from '../Popup/index.js'
@@ -33,7 +33,6 @@ import { RelationshipProvider } from '../Table/RelationshipProvider/index.js'
import { TableColumnsProvider } from '../TableColumns/index.js'
import { DrawerLink } from './cells/DrawerLink/index.js'
import { RelationshipTablePagination } from './Pagination.js'
import './index.scss'
const baseClass = 'relationship-table'
@@ -124,10 +123,11 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
newQuery.where = hoistQueryParamsToAnd(newQuery.where, filterOptions)
}
// map columns from string[] to ColumnPreference[]
const defaultColumns: ColumnPreference[] = field.admin.defaultColumns
// map columns from string[] to ListPreferences['columns']
const defaultColumns = field.admin.defaultColumns
? field.admin.defaultColumns.map((accessor) => ({
[accessor]: true,
accessor,
active: true,
}))
: undefined

View File

@@ -4,10 +4,10 @@ import type {
ClientComponentProps,
ClientField,
Column,
ColumnPreference,
DefaultCellComponentProps,
DefaultServerCellComponentProps,
Field,
ListPreferences,
PaginatedDocs,
Payload,
SanitizedCollectionConfig,
@@ -39,8 +39,8 @@ type Args = {
beforeRows?: Column[]
clientCollectionConfig: ClientCollectionConfig
collectionConfig: SanitizedCollectionConfig
columnPreferences: ColumnPreference[]
columns?: ColumnPreference[]
columnPreferences: ListPreferences['columns']
columns?: ListPreferences['columns']
customCellProps: DefaultCellComponentProps['customCellProps']
docs: PaginatedDocs['docs']
enableRowSelections: boolean
@@ -99,10 +99,10 @@ export const buildColumnState = (args: Args): Column[] => {
const sortTo = columnPreferences || columns
const sortFieldMap = (fieldMap, sortTo: ColumnPreference[]) =>
const sortFieldMap = (fieldMap, sortTo) =>
fieldMap?.sort((a, b) => {
const aIndex = sortTo.findIndex((column) => 'name' in a && a.name in column)
const bIndex = sortTo.findIndex((column) => 'name' in b && b.name in column)
const aIndex = sortTo.findIndex((column) => 'name' in a && column.accessor === a.name)
const bIndex = sortTo.findIndex((column) => 'name' in b && column.accessor === b.name)
if (aIndex === -1 && bIndex === -1) {
return 0
@@ -136,12 +136,18 @@ export const buildColumnState = (args: Args): Column[] => {
(f) => 'name' in field && 'name' in f && f.name === field.name,
)
const columnPreference = columnPreferences?.find(
(preference) => field && 'name' in field && preference.accessor === field.name,
)
let active = false
if (columnPreferences) {
active = 'name' in field && columnPreferences?.some((col) => col?.[field.name])
if (columnPreference) {
active = columnPreference.active
} else if (columns && Array.isArray(columns) && columns.length > 0) {
active = 'name' in field && columns.some((col) => col?.[field.name])
active = columns.find(
(column) => field && 'name' in field && column.accessor === field.name,
)?.active
} else if (activeColumnsIndices.length < 4) {
active = true
}

View File

@@ -4,10 +4,10 @@ import type { I18nClient } from '@payloadcms/translations'
import type {
ClientField,
Column,
ColumnPreference,
DefaultCellComponentProps,
DefaultServerCellComponentProps,
Field,
ListPreferences,
PaginatedDocs,
Payload,
SanitizedCollectionConfig,
@@ -36,8 +36,8 @@ import { filterFields } from './filterFields.js'
type Args = {
beforeRows?: Column[]
columnPreferences: ColumnPreference[]
columns?: ColumnPreference[]
columnPreferences: ListPreferences['columns']
columns?: ListPreferences['columns']
customCellProps: DefaultCellComponentProps['customCellProps']
docs: PaginatedDocs['docs']
enableRowSelections: boolean
@@ -92,8 +92,8 @@ export const buildPolymorphicColumnState = (args: Args): Column[] => {
const sortFieldMap = (fieldMap, sortTo) =>
fieldMap?.sort((a, b) => {
const aIndex = sortTo.findIndex((column) => 'name' in a && a.name in column)
const bIndex = sortTo.findIndex((column) => 'name' in b && b.name in column)
const aIndex = sortTo.findIndex((column) => 'name' in a && column.accessor === a.name)
const bIndex = sortTo.findIndex((column) => 'name' in b && column.accessor === b.name)
if (aIndex === -1 && bIndex === -1) {
return 0
@@ -127,12 +127,18 @@ export const buildPolymorphicColumnState = (args: Args): Column[] => {
(f) => 'name' in field && 'name' in f && f.name === field.name,
)
const columnPreference = columnPreferences?.find(
(preference) => field && 'name' in field && preference.accessor === field.name,
)
let active = false
if (columnPreferences) {
active = 'name' in field && columnPreferences?.some((col) => col?.[field.name])
if (columnPreference) {
active = columnPreference.active
} else if (columns && Array.isArray(columns) && columns.length > 0) {
active = 'name' in field && columns.some((col) => col?.[field.name])
active = columns.find(
(column) => field && 'name' in field && column.accessor === field.name,
)?.active
} else if (activeColumnsIndices.length < 4) {
active = true
}

View File

@@ -1,11 +1,11 @@
import type { ClientField, CollectionConfig, ColumnPreference, Field } from 'payload'
import type { ClientField, CollectionConfig, Field, ListPreferences } from 'payload'
import { fieldAffectsData } from 'payload/shared'
const getRemainingColumns = <T extends ClientField[] | Field[]>(
fields: T,
useAsTitle: string,
): ColumnPreference[] =>
): ListPreferences['columns'] =>
fields?.reduce((remaining, field) => {
if (fieldAffectsData(field) && field.name === useAsTitle) {
return remaining
@@ -40,7 +40,7 @@ export const getInitialColumns = <T extends ClientField[] | Field[]>(
fields: T,
useAsTitle: CollectionConfig['admin']['useAsTitle'],
defaultColumns: CollectionConfig['admin']['defaultColumns'],
): ColumnPreference[] => {
): ListPreferences['columns'] => {
let initialColumns = []
if (Array.isArray(defaultColumns) && defaultColumns.length >= 1) {
@@ -57,6 +57,7 @@ export const getInitialColumns = <T extends ClientField[] | Field[]>(
}
return initialColumns.map((column) => ({
[column]: true,
accessor: column,
active: true,
}))
}

View File

@@ -1,5 +1,5 @@
'use client'
import type { Column, ColumnPreference, ListPreferences, SanitizedCollectionConfig } from 'payload'
import type { Column, ListPreferences, SanitizedCollectionConfig } from 'payload'
import React, { createContext, useCallback, useContext, useEffect } from 'react'
@@ -39,10 +39,12 @@ type Props = {
}
// strip out Heading, Label, and renderedCells properties, they cannot be sent to the server
const formatColumnPreferences = (columns: Column[]): ColumnPreference[] =>
columns.map(({ accessor, active }) => ({
[accessor]: active,
const sanitizeColumns = (columns: Column[]) => {
return columns.map(({ accessor, active }) => ({
accessor,
active,
}))
}
export const TableColumnsProvider: React.FC<Props> = ({
children,
@@ -88,7 +90,7 @@ export const TableColumnsProvider: React.FC<Props> = ({
const result = await getTableState({
collectionSlug,
columns: formatColumnPreferences(withMovedColumn),
columns: sanitizeColumns(withMovedColumn),
docs,
enableRowSelections,
renderRowTypes,
@@ -121,7 +123,7 @@ export const TableColumnsProvider: React.FC<Props> = ({
const { newColumnState, toggledColumns } = tableColumns.reduce<{
newColumnState: Column[]
toggledColumns: ColumnPreference[]
toggledColumns: Pick<Column, 'accessor' | 'active'>[]
}>(
(acc, col) => {
if (col.accessor === column) {
@@ -131,12 +133,14 @@ export const TableColumnsProvider: React.FC<Props> = ({
active: !col.active,
})
acc.toggledColumns.push({
[col.accessor]: !col.active,
accessor: col.accessor,
active: !col.active,
})
} else {
acc.newColumnState.push(col)
acc.toggledColumns.push({
[col.accessor]: col.active,
accessor: col.accessor,
active: col.active,
})
}
@@ -178,8 +182,14 @@ export const TableColumnsProvider: React.FC<Props> = ({
const setActiveColumns = React.useCallback(
async (activeColumnAccessors: string[]) => {
const activeColumns: ColumnPreference[] = formatColumnPreferences(
tableColumns.sort((first, second) => {
const activeColumns: Pick<Column, 'accessor' | 'active'>[] = tableColumns
.map((col) => {
return {
accessor: col.accessor,
active: activeColumnAccessors.includes(col.accessor),
}
})
.sort((first, second) => {
const indexOfFirst = activeColumnAccessors.indexOf(first.accessor)
const indexOfSecond = activeColumnAccessors.indexOf(second.accessor)
@@ -188,8 +198,7 @@ export const TableColumnsProvider: React.FC<Props> = ({
}
return indexOfFirst > indexOfSecond ? 1 : -1
}),
)
})
const { state: columnState, Table } = await getTableState({
collectionSlug,
@@ -230,7 +239,7 @@ export const TableColumnsProvider: React.FC<Props> = ({
if (collectionHasChanged || !listPreferences) {
const currentPreferences = await getPreference<{
columns: ColumnPreference[]
columns: ListPreferences['columns']
}>(preferenceKey)
prevCollection.current = defaultCollection

View File

@@ -10,10 +10,10 @@ import { useField } from '../../forms/useField/index.js'
import { withCondition } from '../../forms/withCondition/index.js'
import { FieldDescription } from '../FieldDescription/index.js'
import { FieldError } from '../FieldError/index.js'
import './index.scss'
import { FieldLabel } from '../FieldLabel/index.js'
import { mergeFieldStyles } from '../mergeFieldStyles.js'
import { fieldBaseClass } from '../shared/index.js'
import './index.scss'
const baseClass = 'json-field'
@@ -31,9 +31,10 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
readOnly,
validate,
} = props
const [stringValue, setStringValue] = useState<string>()
const [jsonError, setJsonError] = useState<string>()
const inputChangeFromRef = React.useRef<'system' | 'user'>('system')
const [editorKey, setEditorKey] = useState<string>('')
const [hasLoadedValue, setHasLoadedValue] = useState(false)
const memoizedValidate = useCallback(
(value, options) => {
@@ -55,12 +56,6 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
validate: memoizedValidate,
})
const [initialStringValue, setInitialStringValue] = useState<string | undefined>(() =>
(value || initialValue) !== undefined
? JSON.stringify(value ?? initialValue, null, 2)
: undefined,
)
const handleMount = useCallback<OnMount>(
(editor, monaco) => {
if (!jsonSchema) {
@@ -93,7 +88,7 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
if (readOnly) {
return
}
inputChangeFromRef.current = 'user'
setStringValue(val)
try {
setValue(val ? JSON.parse(val) : null)
@@ -103,21 +98,20 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
setJsonError(e)
}
},
[readOnly, setValue],
[readOnly, setValue, setStringValue],
)
useEffect(() => {
if (inputChangeFromRef.current === 'system') {
setInitialStringValue(
(value || initialValue) !== undefined
? JSON.stringify(value ?? initialValue, null, 2)
: undefined,
)
setEditorKey(new Date().toString())
if (hasLoadedValue || value === undefined) {
return
}
inputChangeFromRef.current = 'system'
}, [initialValue, value])
setStringValue(
value || initialValue ? JSON.stringify(value ? value : initialValue, null, 2) : '',
)
setHasLoadedValue(true)
}, [initialValue, value, hasLoadedValue])
const styles = useMemo(() => mergeFieldStyles(field), [field])
@@ -148,16 +142,12 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
{BeforeInput}
<CodeEditor
defaultLanguage="json"
key={editorKey}
maxHeight={maxHeight}
onChange={handleChange}
onMount={handleMount}
options={editorOptions}
readOnly={readOnly}
value={initialStringValue}
wrapperProps={{
id: `field-${path?.replace(/\./g, '__')}`,
}}
value={stringValue}
/>
{AfterInput}
</div>

View File

@@ -102,11 +102,7 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
setCurrentQuery(newQuery)
},
[
currentQuery?.page,
currentQuery?.limit,
currentQuery?.search,
currentQuery?.sort,
currentQuery?.where,
currentQuery,
defaultLimit,
defaultSort,
modifySearchParams,
@@ -173,8 +169,7 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
if (shouldUpdateQueryString) {
setCurrentQuery(newQuery)
// Do not use router.replace here to avoid re-rendering on initial load
window.history.replaceState(null, '', `?${qs.stringify(newQuery)}`)
router.replace(`${qs.stringify(newQuery, { addQueryPrefix: true })}`)
}
}
}, [defaultSort, defaultLimit, router, modifySearchParams])

View File

@@ -3,10 +3,9 @@ import type {
ClientConfig,
ClientField,
CollectionConfig,
Column,
ColumnPreference,
Field,
ImportMap,
ListPreferences,
PaginatedDocs,
Payload,
SanitizedCollectionConfig,
@@ -15,6 +14,9 @@ import type {
import { getTranslation, type I18nClient } from '@payloadcms/translations'
import { fieldAffectsData, fieldIsHiddenOrDisabled, flattenTopLevelFields } from 'payload/shared'
// eslint-disable-next-line payload/no-imports-from-exports-dir
import type { Column } from '../exports/client/index.js'
import { RenderServerComponent } from '../elements/RenderServerComponent/index.js'
import { buildColumnState } from '../elements/TableColumns/buildColumnState.js'
import { buildPolymorphicColumnState } from '../elements/TableColumns/buildPolymorphicColumnState.js'
@@ -69,8 +71,8 @@ export const renderTable = ({
clientConfig?: ClientConfig
collectionConfig?: SanitizedCollectionConfig
collections?: string[]
columnPreferences: ColumnPreference[]
columns?: ColumnPreference[]
columnPreferences: ListPreferences['columns']
columns?: ListPreferences['columns']
customCellProps?: Record<string, any>
docs: PaginatedDocs['docs']
drawerSlug?: string
@@ -107,7 +109,7 @@ export const renderTable = ({
const columns = columnsFromArgs
? columnsFromArgs?.filter((column) =>
flattenTopLevelFields(fields, true)?.some(
(field) => 'name' in field && column[field.name],
(field) => 'name' in field && field.name === column.accessor,
),
)
: getInitialColumns(fields, useAsTitle, [])
@@ -128,7 +130,7 @@ export const renderTable = ({
const columns = columnsFromArgs
? columnsFromArgs?.filter((column) =>
flattenTopLevelFields(clientCollectionConfig.fields, true)?.some(
(field) => 'name' in field && field.name in column,
(field) => 'name' in field && field.name === column.accessor,
),
)
: getInitialColumns(

View File

@@ -151,7 +151,6 @@ export function DefaultListView(props: ListViewClientProps) {
])
}
}, [setStepNav, labels, drawerDepth])
return (
<Fragment>
<TableColumnsProvider

View File

@@ -1,38 +0,0 @@
'use client'
import { useField } from '@payloadcms/ui'
export function AfterField() {
const { setValue } = useField({ path: 'customJSON' })
return (
<button
id="set-custom-json"
onClick={(e) => {
e.preventDefault()
setValue({
users: [
{
id: 1,
name: 'John Doe',
email: 'john.doe@example.com',
isActive: true,
roles: ['admin', 'editor'],
},
{
id: 2,
name: 'Jane Smith',
email: 'jane.smith@example.com',
isActive: false,
roles: ['viewer'],
},
],
})
}}
style={{ marginTop: '5px', padding: '5px 10px' }}
type="button"
>
Set Custom JSON
</button>
)
}

View File

@@ -103,24 +103,4 @@ describe('JSON', () => {
'"foo.with.periods": "bar"',
)
})
test('should update', async () => {
const createdDoc = await payload.create({
collection: 'json-fields',
data: {
customJSON: {
default: 'value',
},
},
})
await page.goto(url.edit(createdDoc.id))
const jsonField = page.locator('.json-field #field-customJSON')
await expect(jsonField).toContainText('"default": "value"')
const originalHeight = (await page.locator('#field-customJSON').boundingBox())?.height || 0
await page.locator('#set-custom-json').click()
const newHeight = (await page.locator('#field-customJSON').boundingBox())?.height || 0
expect(newHeight).toBeGreaterThan(originalHeight)
})
})

View File

@@ -67,16 +67,6 @@ const JSON: CollectionConfig = {
},
],
},
{
name: 'customJSON',
type: 'json',
admin: {
components: {
afterInput: ['./collections/JSON/AfterField#AfterField'],
},
},
label: 'Custom Json',
},
],
versions: {
maxPerDoc: 1,

View File

@@ -170,7 +170,12 @@ describe('Text', () => {
user: client.user,
key: 'text-fields-list',
value: {
columns: [{ disableListColumnText: true }],
columns: [
{
accessor: 'disableListColumnText',
active: true,
},
],
},
})

View File

@@ -1474,15 +1474,6 @@ export interface JsonField {
| boolean
| null;
};
customJSON?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
@@ -3174,7 +3165,6 @@ export interface JsonFieldsSelect<T extends boolean = true> {
| {
jsonWithinGroup?: T;
};
customJSON?: T;
updatedAt?: T;
createdAt?: T;
}

View File

@@ -1205,22 +1205,6 @@ describe('Joins Field', () => {
expect(parent.children.docs[1]?.value.id).toBe(child_1.id)
expect(parent.children.docs[1]?.relationTo).toBe('multiple-collections-1')
// Pagination across collections
parent = await payload.findByID({
collection: 'multiple-collections-parents',
id: parent.id,
depth: 1,
joins: {
children: {
limit: 1,
sort: 'title',
},
},
})
expect(parent.children.docs).toHaveLength(1)
expect(parent.children?.hasNextPage).toBe(true)
// Sorting across collections
parent = await payload.findByID({
collection: 'multiple-collections-parents',

View File

@@ -31,7 +31,7 @@
}
],
"paths": {
"@payload-config": ["./test/_community/config.ts"],
"@payload-config": ["./test/fields-relationship/config.ts"],
"@payloadcms/live-preview": ["./packages/live-preview/src"],
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],