Merge branch 'main' into fix/parent-labels-in-toast

This commit is contained in:
Jessica Chowdhury
2025-02-26 13:40:03 +00:00
216 changed files with 2854 additions and 1749 deletions

37
.github/CODEOWNERS vendored
View File

@@ -1,37 +0,0 @@
# 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

@@ -63,41 +63,41 @@ export default buildConfig({
The following options are available:
| Option | Description |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
| **`bin`** | Register custom bin scripts for Payload to execute. |
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
| **`db`** * | The Database Adapter which will be used by Payload. [More details](../database/overview). |
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
| **`compatibility`** | Compatibility flags for earlier versions of Payload. [More details](#compatibility-flags). |
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). |
| **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). |
| **`logger`** | Logger options, logger options with a destination stream, or an instantiated logger instance. [More details](https://getpino.io/#/docs/api?id=options). |
| **`loggingLevels`** | An object to override the level to use in the logger for Payload's errors. |
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). |
| **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). |
| **`email`** | Configure the Email Adapter for Payload to use. [More details](../email/overview). |
| **`onInit`** | A function that is called immediately following startup that receives the Payload instance as its only argument. |
| **`debug`** | Enable to expose more detailed error information. |
| **`telemetry`** | Disable Payload telemetry by passing `false`. [More details](#telemetry). |
| **`hooks`** | An array of Root Hooks. [More details](../hooks/overview). |
| **`plugins`** | An array of Plugins. [More details](../plugins/overview). |
| **`endpoints`** | An array of Custom Endpoints added to the Payload router. [More details](../rest-api/overview#custom-endpoints). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
| **`i18n`** | Internationalization configuration. Pass all i18n languages you'd like the admin UI to support. Defaults to English-only. [More details](./i18n). |
| **`secret`** * | A secure, unguessable string that Payload will use for any encryption workflows - for example, password salt / hashing. |
| **`sharp`** | If you would like Payload to offer cropping, focal point selection, and automatic media resizing, install and pass the Sharp module to the config here. |
| **`typescript`** | Configure TypeScript settings here. [More details](#typescript). |
| Option | Description |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
| **`bin`** | Register custom bin scripts for Payload to execute. [More Details](#custom-bin-scripts). |
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
| **`db`** * | The Database Adapter which will be used by Payload. [More details](../database/overview). |
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
| **`compatibility`** | Compatibility flags for earlier versions of Payload. [More details](#compatibility-flags). |
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). |
| **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). |
| **`logger`** | Logger options, logger options with a destination stream, or an instantiated logger instance. [More details](https://getpino.io/#/docs/api?id=options). |
| **`loggingLevels`** | An object to override the level to use in the logger for Payload's errors. |
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). |
| **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). |
| **`email`** | Configure the Email Adapter for Payload to use. [More details](../email/overview). |
| **`onInit`** | A function that is called immediately following startup that receives the Payload instance as its only argument. |
| **`debug`** | Enable to expose more detailed error information. |
| **`telemetry`** | Disable Payload telemetry by passing `false`. [More details](#telemetry). |
| **`hooks`** | An array of Root Hooks. [More details](../hooks/overview). |
| **`plugins`** | An array of Plugins. [More details](../plugins/overview). |
| **`endpoints`** | An array of Custom Endpoints added to the Payload router. [More details](../rest-api/overview#custom-endpoints). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
| **`i18n`** | Internationalization configuration. Pass all i18n languages you'd like the admin UI to support. Defaults to English-only. [More details](./i18n). |
| **`secret`** * | A secure, unguessable string that Payload will use for any encryption workflows - for example, password salt / hashing. |
| **`sharp`** | If you would like Payload to offer cropping, focal point selection, and automatic media resizing, install and pass the Sharp module to the config here. |
| **`typescript`** | Configure TypeScript settings here. [More details](#typescript). |
_* An asterisk denotes that a property is required._
@@ -265,3 +265,43 @@ The Payload Config can accept compatibility flags for running the newest version
Payload localization works on a field-by-field basis. As you can nest fields within other fields, you could potentially nest a localized field within a localized field—but this would be redundant and unnecessary. There would be no reason to define a localized field within a localized parent field, given that the entire data structure from the parent field onward would be localized.
By default, Payload will remove the `localized: true` property from sub-fields if a parent field is localized. Set this compatibility flag to `true` only if you have an existing Payload MongoDB database from pre-3.0, and you have nested localized fields that you would like to maintain without migrating.
## Custom bin scripts
Using the `bin` configuration property, you can inject your own scripts to `npx payload`.
Example for `pnpm payload seed`:
Step 1: create `seed.ts` file in the same folder with `payload.config.ts` with:
```ts
import type { SanitizedConfig } from 'payload'
import payload from 'payload'
// Script must define a "script" function export that accepts the sanitized config
export const script = async (config: SanitizedConfig) => {
await payload.init({ config })
await payload.create({ collection: 'pages', data: { title: 'my title' } })
payload.logger.info('Succesffully seeded!')
process.exit(0)
}
```
Step 2: add the `seed` script to `bin`:
```ts
export default buildConfig({
bin: [
{
scriptPath: path.resolve(dirname, 'seed.ts'),
key: 'seed',
},
],
})
```
Now you can run the command using:
```sh
pnpm payload seed
```

View File

@@ -276,7 +276,7 @@ export default async function MyServerComponent({
But, the Payload Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) by design. It is full of custom validation functions and more. This means that the Payload Config, in its entirety, cannot be passed directly to Client Components.
For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](../admin/hooks#useconfig) hook:
For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](../admin/react-hooks#useconfig) hook:
```tsx
'use client'
@@ -375,7 +375,7 @@ export function MyClientComponent() {
```
<Banner type="success">
See the [Hooks](../admin/hooks) documentation for a full list of available hooks.
See the [Hooks](../admin/react-hooks) documentation for a full list of available hooks.
</Banner>
### Getting the Current Locale
@@ -422,12 +422,12 @@ function Greeting() {
```
<Banner type="success">
See the [Hooks](../admin/hooks) documentation for a full list of available hooks.
See the [Hooks](../admin/react-hooks) documentation for a full list of available hooks.
</Banner>
### Using Hooks
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](../admin/hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts. To do this, you can use one of the many hooks available depending on your needs.
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](../admin/react-hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts. To do this, you can use one of the many hooks available depending on your needs.
```tsx
'use client'
@@ -444,7 +444,7 @@ export function MyClientComponent() {
```
<Banner type="success">
See the [Hooks](../admin/hooks) documentation for a full list of available hooks.
See the [Hooks](../admin/react-hooks) documentation for a full list of available hooks.
</Banner>
### Adding Styles

View File

@@ -658,7 +658,7 @@ In addition to the above props, all Server Components will also receive the foll
When swapping out the `Field` component, you are responsible for sending and receiving the field's `value` from the form itself.
To do so, import the [`useField`](../admin/hooks#usefield) hook from `@payloadcms/ui` and use it to manage the field's value:
To do so, import the [`useField`](../admin/react-hooks#usefield) hook from `@payloadcms/ui` and use it to manage the field's value:
```tsx
'use client'
@@ -677,7 +677,7 @@ export const CustomTextField: React.FC = () => {
```
<Banner type="success">
For a complete list of all available React hooks, see the [Payload React Hooks](../admin/hooks) documentation. For additional help, see [Building Custom Components](../custom-components/overview#building-custom-components).
For a complete list of all available React hooks, see the [Payload React Hooks](../admin/react-hooks) documentation. For additional help, see [Building Custom Components](../custom-components/overview#building-custom-components).
</Banner>
##### TypeScript#field-component-types

View File

@@ -27,7 +27,7 @@ There are four main types of Hooks in Payload:
<Banner type="warning">
**Reminder:**
Payload also ships a set of _React_ hooks that you can use in your frontend application. Although they share a common name, these are very different things and should not be confused. [More details](../admin/hooks).
Payload also ships a set of _React_ hooks that you can use in your frontend application. Although they share a common name, these are very different things and should not be confused. [More details](../admin/react-hooks).
</Banner>
## Root Hooks

View File

@@ -414,6 +414,15 @@ For more details, see the [Documentation](https://payloadcms.com/docs/getting-st
```
1. The `./src/public` directory is now located directly at root level `./public` [see Next.js docs for details](https://nextjs.org/docs/pages/building-your-application/optimizing/static-assets)
1. Payload now automatically removes `localized: true` property from sub-fields if a parent is localized, as it's redunant and unnecessary. If you have some existing data in this structure and you want to disable that behavior, you need to enable `allowLocalizedWithinLocalized` flag in your payload.config [read more in documentation](https://payloadcms.com/docs/configuration/overview#compatibility-flags), or create a migration script that aligns your data.
Mongodb example for a link in a page layout.
```diff
- layout.columns.en.link.en.type.en
+ layout.columns.en.link.type
```
## Custom Components
1. All Payload React components have been moved from the `payload` package to `@payloadcms/ui`. If you were previously importing components into your app from the `payload` package, for example to create Custom Components, you will need to change your import paths:

View File

@@ -278,6 +278,50 @@ async rewrites() {
}
```
### React Hooks
Below are the hooks exported from the plugin that you can import into your own custom components to consume.
#### useTenantSelection
You can import this like so:
```tsx
import { useTenantSelection } from '@payloadcms/plugin-multi-tenant/client'
...
const tenantContext = useTenantSelection()
```
The hook returns the following context:
```ts
type ContextType = {
/**
* Array of options to select from
*/
options: OptionObject[]
/**
* The currently selected tenant ID
*/
selectedTenantID: number | string | undefined
/**
* Prevents a refresh when the tenant is changed
*
* If not switching tenants while viewing a "global", set to true
*/
setPreventRefreshOnChange: React.Dispatch<React.SetStateAction<boolean>>
/**
* Sets the selected tenant ID
*
* @param args.id - The ID of the tenant to select
* @param args.refresh - Whether to refresh the page after changing the tenant
*/
setTenant: (args: { id: number | string | undefined; refresh?: boolean }) => void
}
```
## Examples

View File

@@ -145,13 +145,13 @@ Here's an overview of all the included features:
| Feature Name | Included by default | Description |
| --- | --- | --- |
| **`BoldTextFeature`** | Yes | Handles the bold text format |
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
| **`StrikethroughTextFeature`** | Yes | Handles the strikethrough text format |
| **`SubscriptTextFeature`** | Yes | Handles the subscript text format |
| **`SuperscriptTextFeature`** | Yes | Handles the superscript text format |
| **`InlineCodeTextFeature`** | Yes | Handles the inline-code text format |
| **`BoldFeature`** | Yes | Handles the bold text format |
| **`ItalicFeature`** | Yes | Handles the italic text format |
| **`UnderlineFeature`** | Yes | Handles the underline text format |
| **`StrikethroughFeature`** | Yes | Handles the strikethrough text format |
| **`SubscriptFeature`** | Yes | Handles the subscript text format |
| **`SuperscriptFeature`** | Yes | Handles the superscript text format |
| **`InlineCodeFeature`** | Yes | Handles the inline-code text format |
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |

View File

@@ -5,7 +5,7 @@ import type { MongooseAdapter } from './index.js'
import { getSession } from './utilities/getSession.js'
import { handleError } from './utilities/handleError.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { transform } from './utilities/transform.js'
export const create: Create = async function create(
this: MongooseAdapter,
@@ -18,31 +18,31 @@ export const create: Create = async function create(
let doc
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
transform({
adapter: this,
data,
fields: this.payload.collections[collection].config.fields,
operation: 'write',
})
if (this.payload.collections[collection].customIDType) {
sanitizedData._id = sanitizedData.id
data._id = data.id
}
try {
;[doc] = await Model.create([sanitizedData], options)
;[doc] = await Model.create([data], options)
} catch (error) {
handleError({ collection, error, req })
}
// doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here
const result: Document = JSON.parse(JSON.stringify(doc))
const verificationToken = doc._verificationToken
doc = doc.toObject()
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
transform({
adapter: this,
data: doc,
fields: this.payload.collections[collection].config.fields,
operation: 'read',
})
return result
return doc
}

View File

@@ -4,8 +4,7 @@ import type { CreateGlobal } from 'payload'
import type { MongooseAdapter } from './index.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { transform } from './utilities/transform.js'
export const createGlobal: CreateGlobal = async function createGlobal(
this: MongooseAdapter,
@@ -13,26 +12,28 @@ export const createGlobal: CreateGlobal = async function createGlobal(
) {
const Model = this.globals
const global = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
globalType: slug,
...data,
},
transform({
adapter: this,
data,
fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
globalSlug: slug,
operation: 'write',
})
const options: CreateOptions = {
session: await getSession(this, req),
}
let [result] = (await Model.create([global], options)) as any
let [result] = (await Model.create([data], options)) as any
result = JSON.parse(JSON.stringify(result))
result = result.toObject()
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
transform({
adapter: this,
data: result,
fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
operation: 'read',
})
return result
}

View File

@@ -1,11 +1,11 @@
import type { CreateOptions } from 'mongoose'
import { buildVersionGlobalFields, type CreateGlobalVersion, type Document } from 'payload'
import { buildVersionGlobalFields, type CreateGlobalVersion } from 'payload'
import type { MongooseAdapter } from './index.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { transform } from './utilities/transform.js'
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
this: MongooseAdapter,
@@ -26,25 +26,30 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
session: await getSession(this, req),
}
const data = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
fields: buildVersionGlobalFields(
this.payload.config,
this.payload.config.globals.find((global) => global.slug === globalSlug),
),
const data = {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
}
const fields = buildVersionGlobalFields(
this.payload.config,
this.payload.config.globals.find((global) => global.slug === globalSlug),
)
transform({
adapter: this,
data,
fields,
operation: 'write',
})
const [doc] = await VersionModel.create([data], options, req)
let [doc] = await VersionModel.create([data], options, req)
await VersionModel.updateMany(
{
@@ -70,13 +75,14 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
options,
)
const result: Document = JSON.parse(JSON.stringify(doc))
const verificationToken = doc._verificationToken
doc = doc.toObject()
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
transform({
adapter: this,
data: doc,
fields,
operation: 'read',
})
return doc
}

View File

@@ -1,12 +1,11 @@
import type { CreateOptions } from 'mongoose'
import { Types } from 'mongoose'
import { buildVersionCollectionFields, type CreateVersion, type Document } from 'payload'
import { buildVersionCollectionFields, type CreateVersion } from 'payload'
import type { MongooseAdapter } from './index.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { transform } from './utilities/transform.js'
export const createVersion: CreateVersion = async function createVersion(
this: MongooseAdapter,
@@ -27,25 +26,30 @@ export const createVersion: CreateVersion = async function createVersion(
session: await getSession(this, req),
}
const data = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collectionSlug].config,
),
const data = {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
}
const fields = buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collectionSlug].config,
)
transform({
adapter: this,
data,
fields,
operation: 'write',
})
const [doc] = await VersionModel.create([data], options, req)
let [doc] = await VersionModel.create([data], options, req)
const parentQuery = {
$or: [
@@ -56,13 +60,6 @@ export const createVersion: CreateVersion = async function createVersion(
},
],
}
if (data.parent instanceof Types.ObjectId) {
parentQuery.$or.push({
parent: {
$eq: data.parent.toString(),
},
})
}
await VersionModel.updateMany(
{
@@ -89,13 +86,14 @@ export const createVersion: CreateVersion = async function createVersion(
options,
)
const result: Document = JSON.parse(JSON.stringify(doc))
const verificationToken = doc._verificationToken
doc = doc.toObject()
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
transform({
adapter: this,
data: doc,
fields,
operation: 'read',
})
return doc
}

View File

@@ -1,12 +1,12 @@
import type { QueryOptions } from 'mongoose'
import type { DeleteOne, Document } from 'payload'
import type { DeleteOne } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { transform } from './utilities/transform.js'
export const deleteOne: DeleteOne = async function deleteOne(
this: MongooseAdapter,
@@ -35,11 +35,12 @@ export const deleteOne: DeleteOne = async function deleteOne(
return null
}
let result: Document = JSON.parse(JSON.stringify(doc))
transform({
adapter: this,
data: doc,
fields: this.payload.collections[collection].config.fields,
operation: 'read',
})
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
return result
return doc
}

View File

@@ -10,7 +10,7 @@ import { buildSortParam } from './queries/buildSortParam.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { transform } from './utilities/transform.js'
export const find: Find = async function find(
this: MongooseAdapter,
@@ -133,13 +133,12 @@ export const find: Find = async function find(
result = await Model.paginate(query, paginationOptions)
}
const docs = JSON.parse(JSON.stringify(result.docs))
transform({
adapter: this,
data: result.docs,
fields: this.payload.collections[collection].config.fields,
operation: 'read',
})
return {
...result,
docs: docs.map((doc) => {
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}
return result
}

View File

@@ -8,14 +8,15 @@ import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { transform } from './utilities/transform.js'
export const findGlobal: FindGlobal = async function findGlobal(
this: MongooseAdapter,
{ slug, locale, req, select, where },
) {
const Model = this.globals
const fields = this.payload.globals.config.find((each) => each.slug === slug).flattenedFields
const globalConfig = this.payload.globals.config.find((each) => each.slug === slug)
const fields = globalConfig.flattenedFields
const options: QueryOptions = {
lean: true,
select: buildProjectionFromSelect({
@@ -34,18 +35,18 @@ export const findGlobal: FindGlobal = async function findGlobal(
where: combineQueries({ globalType: { equals: slug } }, where),
})
let doc = (await Model.findOne(query, {}, options)) as any
const doc = (await Model.findOne(query, {}, options)) as any
if (!doc) {
return null
}
if (doc._id) {
doc.id = doc._id
delete doc._id
}
doc = JSON.parse(JSON.stringify(doc))
doc = sanitizeInternalFields(doc)
transform({
adapter: this,
data: doc,
fields: globalConfig.fields,
operation: 'read',
})
return doc
}

View File

@@ -9,18 +9,15 @@ import { buildQuery } from './queries/buildQuery.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { transform } from './utilities/transform.js'
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
this: MongooseAdapter,
{ global, limit, locale, page, pagination, req, select, skip, sort: sortArg, where },
) {
const globalConfig = this.payload.globals.config.find(({ slug }) => slug === global)
const Model = this.versions[global]
const versionFields = buildVersionGlobalFields(
this.payload.config,
this.payload.globals.config.find(({ slug }) => slug === global),
true,
)
const versionFields = buildVersionGlobalFields(this.payload.config, globalConfig, true)
const session = await getSession(this, req)
const options: QueryOptions = {
@@ -103,13 +100,13 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
}
const result = await Model.paginate(query, paginationOptions)
const docs = JSON.parse(JSON.stringify(result.docs))
return {
...result,
docs: docs.map((doc) => {
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}
transform({
adapter: this,
data: result.docs,
fields: buildVersionGlobalFields(this.payload.config, globalConfig),
operation: 'read',
})
return result
}

View File

@@ -1,5 +1,5 @@
import type { AggregateOptions, QueryOptions } from 'mongoose'
import type { Document, FindOne } from 'payload'
import type { FindOne } from 'payload'
import type { MongooseAdapter } from './index.js'
@@ -7,7 +7,7 @@ import { buildQuery } from './queries/buildQuery.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { transform } from './utilities/transform.js'
export const findOne: FindOne = async function findOne(
this: MongooseAdapter,
@@ -58,11 +58,7 @@ export const findOne: FindOne = async function findOne(
return null
}
let result: Document = JSON.parse(JSON.stringify(doc))
transform({ adapter: this, data: doc, fields: collectionConfig.fields, operation: 'read' })
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
return result
return doc
}

View File

@@ -9,7 +9,7 @@ import { buildQuery } from './queries/buildQuery.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { transform } from './utilities/transform.js'
export const findVersions: FindVersions = async function findVersions(
this: MongooseAdapter,
@@ -104,13 +104,13 @@ export const findVersions: FindVersions = async function findVersions(
}
const result = await Model.paginate(query, paginationOptions)
const docs = JSON.parse(JSON.stringify(result.docs))
return {
...result,
docs: docs.map((doc) => {
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}
transform({
adapter: this,
data: result.docs,
fields: buildVersionCollectionFields(this.payload.config, collectionConfig),
operation: 'read',
})
return result
}

View File

@@ -476,6 +476,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
schemaToReturn = {
_id: false,
type: payload.config.localization.localeCodes.reduce((locales, locale) => {
let localeSchema: { [key: string]: any } = {}
@@ -698,6 +699,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
schemaToReturn = {
_id: false,
type: payload.config.localization.localeCodes.reduce((locales, locale) => {
let localeSchema: { [key: string]: any } = {}

View File

@@ -6,11 +6,12 @@ import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
import type { MongooseAdapter } from '../index.js'
import { getSession } from '../utilities/getSession.js'
import { sanitizeRelationshipIDs } from '../utilities/sanitizeRelationshipIDs.js'
import { transform } from '../utilities/transform.js'
const migrateModelWithBatching = async ({
batchSize,
config,
db,
fields,
Model,
parentIsLocalized,
@@ -18,6 +19,7 @@ const migrateModelWithBatching = async ({
}: {
batchSize: number
config: SanitizedConfig
db: MongooseAdapter
fields: Field[]
Model: Model<any>
parentIsLocalized: boolean
@@ -49,7 +51,7 @@ const migrateModelWithBatching = async ({
}
for (const doc of docs) {
sanitizeRelationshipIDs({ config, data: doc, fields, parentIsLocalized })
transform({ adapter: db, data: doc, fields, operation: 'write', parentIsLocalized })
}
await Model.collection.bulkWrite(
@@ -124,6 +126,7 @@ export async function migrateRelationshipsV2_V3({
await migrateModelWithBatching({
batchSize,
config,
db,
fields: collection.fields,
Model: db.collections[collection.slug],
parentIsLocalized: false,
@@ -139,6 +142,7 @@ export async function migrateRelationshipsV2_V3({
await migrateModelWithBatching({
batchSize,
config,
db,
fields: buildVersionCollectionFields(config, collection),
Model: db.versions[collection.slug],
parentIsLocalized: false,
@@ -167,10 +171,11 @@ export async function migrateRelationshipsV2_V3({
// in case if the global doesn't exist in the database yet (not saved)
if (doc) {
sanitizeRelationshipIDs({
config,
transform({
adapter: db,
data: doc,
fields: global.fields,
operation: 'write',
})
await GlobalsModel.collection.updateOne(
@@ -191,6 +196,7 @@ export async function migrateRelationshipsV2_V3({
await migrateModelWithBatching({
batchSize,
config,
db,
fields: buildVersionGlobalFields(config, global),
Model: db.versions[global.slug],
parentIsLocalized: false,

View File

@@ -255,6 +255,25 @@ export async function buildSearchParam({
return result
}
if (formattedOperator === 'not_like' && typeof formattedValue === 'string') {
const words = formattedValue.split(' ')
const result = {
value: {
$and: words.map((word) => ({
[path]: {
$not: {
$options: 'i',
$regex: word.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'),
},
},
})),
},
}
return result
}
// Some operators like 'near' need to define a full query
// so if there is no operator key, just return the value
if (!operatorKey) {

View File

@@ -10,7 +10,7 @@ import { buildSortParam } from './queries/buildSortParam.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { transform } from './utilities/transform.js'
export const queryDrafts: QueryDrafts = async function queryDrafts(
this: MongooseAdapter,
@@ -124,18 +124,18 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
result = await VersionModel.paginate(versionQuery, paginationOptions)
}
const docs = JSON.parse(JSON.stringify(result.docs))
transform({
adapter: this,
data: result.docs,
fields: buildVersionCollectionFields(this.payload.config, collectionConfig),
operation: 'read',
})
return {
...result,
docs: docs.map((doc) => {
doc = {
_id: doc.parent,
id: doc.parent,
...doc.version,
}
return sanitizeInternalFields(doc)
}),
for (let i = 0; i < result.docs.length; i++) {
const id = result.docs[i].parent
result.docs[i] = result.docs[i].version
result.docs[i].id = id
}
return result
}

View File

@@ -5,8 +5,7 @@ import type { MongooseAdapter } from './index.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { transform } from './utilities/transform.js'
export const updateGlobal: UpdateGlobal = async function updateGlobal(
this: MongooseAdapter,
@@ -27,25 +26,11 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
session: await getSession(this, req),
}
let result
transform({ adapter: this, data, fields, globalSlug: slug, operation: 'write' })
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data,
fields,
})
const result: any = await Model.findOneAndUpdate({ globalType: slug }, data, options)
result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options)
if (!result) {
return null
}
result = JSON.parse(JSON.stringify(result))
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
transform({ adapter: this, data: result, fields, globalSlug: slug, operation: 'read' })
return result
}

View File

@@ -7,7 +7,7 @@ import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { transform } from './utilities/transform.js'
export async function updateGlobalVersion<T extends TypeWithID>(
this: MongooseAdapter,
@@ -47,26 +47,15 @@ export async function updateGlobalVersion<T extends TypeWithID>(
where: whereToUse,
})
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data: versionData,
fields,
})
transform({ adapter: this, data: versionData, fields, operation: 'write' })
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
if (!doc) {
return null
}
const result = JSON.parse(JSON.stringify(doc))
transform({ adapter: this, data: doc, fields, operation: 'read' })
const verificationToken = doc._verificationToken
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
return doc
}

View File

@@ -7,8 +7,7 @@ import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { handleError } from './utilities/handleError.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { transform } from './utilities/transform.js'
export const updateOne: UpdateOne = async function updateOne(
this: MongooseAdapter,
@@ -39,14 +38,10 @@ export const updateOne: UpdateOne = async function updateOne(
let result
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data,
fields,
})
transform({ adapter: this, data, fields, operation: 'write' })
try {
result = await Model.findOneAndUpdate(query, sanitizedData, options)
result = await Model.findOneAndUpdate(query, data, options)
} catch (error) {
handleError({ collection, error, req })
}
@@ -55,9 +50,7 @@ export const updateOne: UpdateOne = async function updateOne(
return null
}
result = JSON.parse(JSON.stringify(result))
result.id = result._id
result = sanitizeInternalFields(result)
transform({ adapter: this, data: result, fields, operation: 'read' })
return result
}

View File

@@ -7,7 +7,7 @@ import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { transform } from './utilities/transform.js'
export const updateVersion: UpdateVersion = async function updateVersion(
this: MongooseAdapter,
@@ -45,26 +45,15 @@ export const updateVersion: UpdateVersion = async function updateVersion(
where: whereToUse,
})
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data: versionData,
fields,
})
transform({ adapter: this, data: versionData, fields, operation: 'write' })
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
if (!doc) {
return null
}
const result = JSON.parse(JSON.stringify(doc))
transform({ adapter: this, data: doc, fields, operation: 'write' })
const verificationToken = doc._verificationToken
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
return doc
}

View File

@@ -195,17 +195,17 @@ export const buildJoinAggregation = async ({
const sliceValue = page ? [(page - 1) * limitJoin, limitJoin] : [limitJoin]
aggregate.push({
$set: {
[`${as}.docs`]: {
$slice: [`$${as}.docs`, ...sliceValue],
$addFields: {
[`${as}.hasNextPage`]: {
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
},
},
})
aggregate.push({
$addFields: {
[`${as}.hasNextPage`]: {
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
$set: {
[`${as}.docs`]: {
$slice: [`$${as}.docs`, ...sliceValue],
},
},
})

View File

@@ -1,20 +0,0 @@
const internalFields = ['__v']
export const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc: T): T =>
Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => {
if (key === '_id') {
return {
...newDoc,
id: val,
}
}
if (internalFields.indexOf(key) > -1) {
return newDoc
}
return {
...newDoc,
[key]: val,
}
}, {} as T)

View File

@@ -1,165 +0,0 @@
import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } from 'payload'
import { Types } from 'mongoose'
import { traverseFields } from 'payload'
import { fieldAffectsData, fieldShouldBeLocalized } from 'payload/shared'
type Args = {
config: SanitizedConfig
data: Record<string, unknown>
fields: Field[]
parentIsLocalized?: boolean
}
interface RelationObject {
relationTo: string
value: number | string
}
function isValidRelationObject(value: unknown): value is RelationObject {
return typeof value === 'object' && value !== null && 'relationTo' in value && 'value' in value
}
const convertValue = ({
relatedCollection,
value,
}: {
relatedCollection: CollectionConfig
value: number | string
}): number | string | Types.ObjectId => {
const customIDField = relatedCollection.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
if (customIDField) {
return value
}
try {
return new Types.ObjectId(value)
} catch {
return value
}
}
const sanitizeRelationship = ({ config, field, locale, ref, value }) => {
let relatedCollection: CollectionConfig | undefined
let result = value
const hasManyRelations = typeof field.relationTo !== 'string'
if (!hasManyRelations) {
relatedCollection = config.collections?.find(({ slug }) => slug === field.relationTo)
}
if (Array.isArray(value)) {
result = value.map((val) => {
// Handle has many
if (relatedCollection && val && (typeof val === 'string' || typeof val === 'number')) {
return convertValue({
relatedCollection,
value: val,
})
}
// Handle has many - polymorphic
if (isValidRelationObject(val)) {
const relatedCollectionForSingleValue = config.collections?.find(
({ slug }) => slug === val.relationTo,
)
if (relatedCollectionForSingleValue) {
return {
relationTo: val.relationTo,
value: convertValue({
relatedCollection: relatedCollectionForSingleValue,
value: val.value,
}),
}
}
}
return val
})
}
// Handle has one - polymorphic
if (isValidRelationObject(value)) {
relatedCollection = config.collections?.find(({ slug }) => slug === value.relationTo)
if (relatedCollection) {
result = {
relationTo: value.relationTo,
value: convertValue({ relatedCollection, value: value.value }),
}
}
}
// Handle has one
if (relatedCollection && value && (typeof value === 'string' || typeof value === 'number')) {
result = convertValue({
relatedCollection,
value,
})
}
if (locale) {
ref[locale] = result
} else {
ref[field.name] = result
}
}
export const sanitizeRelationshipIDs = ({
config,
data,
fields,
parentIsLocalized,
}: Args): Record<string, unknown> => {
const sanitize: TraverseFieldsCallback = ({ field, ref }) => {
if (!ref || typeof ref !== 'object') {
return
}
if (field.type === 'relationship' || field.type === 'upload') {
if (!ref[field.name]) {
return
}
// handle localized relationships
if (config.localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
const locales = config.localization.locales
const fieldRef = ref[field.name]
if (typeof fieldRef !== 'object') {
return
}
for (const { code } of locales) {
const value = ref[field.name][code]
if (value) {
sanitizeRelationship({ config, field, locale: code, ref: fieldRef, value })
}
}
} else {
// handle non-localized relationships
sanitizeRelationship({
config,
field,
locale: undefined,
ref,
value: ref[field.name],
})
}
}
}
traverseFields({
callback: sanitize,
config,
fields,
fillEmpty: false,
parentIsLocalized,
ref: data,
})
return data
}

View File

@@ -2,7 +2,8 @@ import { flattenAllFields, type Field, type SanitizedConfig } from 'payload'
import { Types } from 'mongoose'
import { sanitizeRelationshipIDs } from './sanitizeRelationshipIDs.js'
import { transform } from './transform.js'
import type { MongooseAdapter } from '../index.js'
const flattenRelationshipValues = (obj: Record<string, any>, prefix = ''): Record<string, any> => {
return Object.keys(obj).reduce(
@@ -297,7 +298,7 @@ const relsData = {
},
}
describe('sanitizeRelationshipIDs', () => {
describe('transform', () => {
it('should sanitize relationships', () => {
const data = {
...relsData,
@@ -382,7 +383,18 @@ describe('sanitizeRelationshipIDs', () => {
}
const flattenValuesBefore = Object.values(flattenRelationshipValues(data))
sanitizeRelationshipIDs({ config, data, fields: config.collections[0].fields })
const mockAdapter = {
payload: {
config,
},
} as MongooseAdapter
transform({
adapter: mockAdapter,
operation: 'write',
data,
fields: config.collections[0].fields,
})
const flattenValuesAfter = Object.values(flattenRelationshipValues(data))
flattenValuesAfter.forEach((value, i) => {

View File

@@ -0,0 +1,347 @@
import type {
CollectionConfig,
DateField,
Field,
JoinField,
RelationshipField,
SanitizedConfig,
TraverseFieldsCallback,
UploadField,
} from 'payload'
import { Types } from 'mongoose'
import { traverseFields } from 'payload'
import { fieldAffectsData, fieldShouldBeLocalized } from 'payload/shared'
import type { MongooseAdapter } from '../index.js'
interface RelationObject {
relationTo: string
value: number | string
}
function isValidRelationObject(value: unknown): value is RelationObject {
return typeof value === 'object' && value !== null && 'relationTo' in value && 'value' in value
}
const convertRelationshipValue = ({
operation,
relatedCollection,
validateRelationships,
value,
}: {
operation: Args['operation']
relatedCollection: CollectionConfig
validateRelationships?: boolean
value: unknown
}) => {
const customIDField = relatedCollection.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
if (operation === 'read') {
if (value instanceof Types.ObjectId) {
return value.toHexString()
}
return value
}
if (customIDField) {
return value
}
if (typeof value === 'string') {
try {
return new Types.ObjectId(value)
} catch (e) {
if (validateRelationships) {
throw e
}
return value
}
}
return value
}
const sanitizeRelationship = ({
config,
field,
locale,
operation,
ref,
validateRelationships,
value,
}: {
config: SanitizedConfig
field: JoinField | RelationshipField | UploadField
locale?: string
operation: Args['operation']
ref: Record<string, unknown>
validateRelationships?: boolean
value?: unknown
}) => {
if (field.type === 'join') {
if (
operation === 'read' &&
value &&
typeof value === 'object' &&
'docs' in value &&
Array.isArray(value.docs)
) {
for (let i = 0; i < value.docs.length; i++) {
const item = value.docs[i]
if (item instanceof Types.ObjectId) {
value.docs[i] = item.toHexString()
} else if (Array.isArray(field.collection) && item) {
// Fields here for polymorphic joins cannot be determinted, JSON.parse needed
value.docs[i] = JSON.parse(JSON.stringify(value.docs[i]))
}
}
}
return value
}
let relatedCollection: CollectionConfig | undefined
let result = value
const hasManyRelations = typeof field.relationTo !== 'string'
if (!hasManyRelations) {
relatedCollection = config.collections?.find(({ slug }) => slug === field.relationTo)
}
if (Array.isArray(value)) {
result = value.map((val) => {
// Handle has many - polymorphic
if (isValidRelationObject(val)) {
const relatedCollectionForSingleValue = config.collections?.find(
({ slug }) => slug === val.relationTo,
)
if (relatedCollectionForSingleValue) {
return {
relationTo: val.relationTo,
value: convertRelationshipValue({
operation,
relatedCollection: relatedCollectionForSingleValue,
validateRelationships,
value: val.value,
}),
}
}
}
if (relatedCollection) {
return convertRelationshipValue({
operation,
relatedCollection,
validateRelationships,
value: val,
})
}
return val
})
}
// Handle has one - polymorphic
else if (isValidRelationObject(value)) {
relatedCollection = config.collections?.find(({ slug }) => slug === value.relationTo)
if (relatedCollection) {
result = {
relationTo: value.relationTo,
value: convertRelationshipValue({
operation,
relatedCollection,
validateRelationships,
value: value.value,
}),
}
}
}
// Handle has one
else if (relatedCollection) {
result = convertRelationshipValue({
operation,
relatedCollection,
validateRelationships,
value,
})
}
if (locale) {
ref[locale] = result
} else {
ref[field.name] = result
}
}
const sanitizeDate = ({
field,
locale,
ref,
value,
}: {
field: DateField
locale?: string
ref: Record<string, unknown>
value: unknown
}) => {
if (!value) {
return
}
if (value instanceof Date) {
value = value.toISOString()
}
if (locale) {
ref[locale] = value
} else {
ref[field.name] = value
}
}
type Args = {
/** instance of the adapter */
adapter: MongooseAdapter
/** data to transform, can be an array of documents or a single document */
data: Record<string, unknown> | Record<string, unknown>[]
/** fields accossiated with the data */
fields: Field[]
/** slug of the global, pass only when the operation is `write` */
globalSlug?: string
/**
* Type of the operation
* read - sanitizes ObjectIDs, Date to strings.
* write - sanitizes string relationships to ObjectIDs.
*/
operation: 'read' | 'write'
parentIsLocalized?: boolean
/**
* Throw errors on invalid relationships
* @default true
*/
validateRelationships?: boolean
}
export const transform = ({
adapter,
data,
fields,
globalSlug,
operation,
parentIsLocalized,
validateRelationships = true,
}: Args) => {
if (Array.isArray(data)) {
for (let i = 0; i < data.length; i++) {
transform({ adapter, data: data[i], fields, globalSlug, operation, validateRelationships })
}
return
}
const {
payload: { config },
} = adapter
if (operation === 'read') {
delete data['__v']
data.id = data._id
delete data['_id']
if (data.id instanceof Types.ObjectId) {
data.id = data.id.toHexString()
}
}
if (operation === 'write' && globalSlug) {
data.globalType = globalSlug
}
const sanitize: TraverseFieldsCallback = ({ field, ref }) => {
if (!ref || typeof ref !== 'object') {
return
}
if (field.type === 'date' && operation === 'read' && ref[field.name]) {
if (config.localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
const fieldRef = ref[field.name]
if (!fieldRef || typeof fieldRef !== 'object') {
return
}
for (const locale of config.localization.localeCodes) {
sanitizeDate({
field,
ref: fieldRef,
value: fieldRef[locale],
})
}
} else {
sanitizeDate({
field,
ref: ref as Record<string, unknown>,
value: ref[field.name],
})
}
}
if (
field.type === 'relationship' ||
field.type === 'upload' ||
(operation === 'read' && field.type === 'join')
) {
if (!ref[field.name]) {
return
}
// handle localized relationships
if (config.localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
const locales = config.localization.locales
const fieldRef = ref[field.name]
if (typeof fieldRef !== 'object') {
return
}
for (const { code } of locales) {
const value = ref[field.name][code]
if (value) {
sanitizeRelationship({
config,
field,
locale: code,
operation,
ref: fieldRef,
validateRelationships,
value,
})
}
}
} else {
// handle non-localized relationships
sanitizeRelationship({
config,
field,
locale: undefined,
operation,
ref: ref as Record<string, unknown>,
validateRelationships,
value: ref[field.name],
})
}
}
}
traverseFields({
callback: sanitize,
config,
fields,
fillEmpty: false,
parentIsLocalized,
ref: data,
})
}

View File

@@ -50,10 +50,12 @@ const createConstraint = ({
const newAlias = `${pathSegments[0]}_alias_${pathSegments.length - 1}`
let formattedValue = value
let formattedOperator = operator
if (['contains', 'like'].includes(operator)) {
formattedOperator = 'like'
formattedValue = `%${value}%`
} else if (['not_like', 'notlike'].includes(operator)) {
formattedOperator = 'not like'
formattedValue = `%${value}%`
} else if (operator === 'equals') {
formattedOperator = '='
}
@@ -61,7 +63,7 @@ const createConstraint = ({
return `EXISTS (
SELECT 1
FROM json_each(${alias}.value -> '${pathSegments[0]}') AS ${newAlias}
WHERE ${newAlias}.value ->> '${pathSegments[1]}' ${formattedOperator} '${formattedValue}'
WHERE COALESCE(${newAlias}.value ->> '${pathSegments[1]}', '') ${formattedOperator} '${formattedValue}'
)`
}

View File

@@ -37,7 +37,7 @@ import {
updateOne,
updateVersion,
} from '@payloadcms/drizzle'
import { like } from 'drizzle-orm'
import { like, notLike } from 'drizzle-orm'
import { createDatabaseAdapter, defaultBeginTransaction } from 'payload'
import { fileURLToPath } from 'url'
@@ -81,6 +81,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
...operatorMap,
contains: like,
like,
not_like: notLike,
} as unknown as Operators
return createDatabaseAdapter<SQLiteAdapter>({

View File

@@ -31,9 +31,9 @@ export { buildRawSchema } from './schema/buildRawSchema.js'
export { beginTransaction } from './transactions/beginTransaction.js'
export { commitTransaction } from './transactions/commitTransaction.js'
export { rollbackTransaction } from './transactions/rollbackTransaction.js'
export { updateOne } from './update.js'
export { updateGlobal } from './updateGlobal.js'
export { updateGlobalVersion } from './updateGlobalVersion.js'
export { updateOne } from './updateOne.js'
export { updateVersion } from './updateVersion.js'
export { upsertRow } from './upsertRow/index.js'
export { buildCreateMigration } from './utilities/buildCreateMigration.js'

View File

@@ -7,12 +7,13 @@ const operatorMap: Record<string, string> = {
like: 'like_regex',
not_equals: '!=',
not_in: 'in',
not_like: '!like_regex',
}
const sanitizeValue = (value: unknown, operator?: string) => {
if (typeof value === 'string') {
// ignore casing with like
return `"${operator === 'like' ? '(?i)' : ''}${value}"`
// ignore casing with like or not_like
return `"${['like', 'not_like'].includes(operator) ? '(?i)' : ''}${value}"`
}
return value as string
@@ -35,6 +36,10 @@ export const createJSONQuery = ({ column, operator, pathSegments, value }: Creat
})
} else if (operator === 'exists') {
sql = `${value === false ? 'NOT ' : ''}jsonb_path_exists(${columnName}, '$.${jsonPaths}')`
} else if (['not_like'].includes(operator)) {
const mappedOperator = operatorMap[operator]
sql = `NOT jsonb_path_exists(${columnName}, '$.${jsonPaths} ? (@ ${mappedOperator.substring(1)} ${sanitizeValue(value, operator)})')`
} else {
sql = `jsonb_path_exists(${columnName}, '$.${jsonPaths} ? (@ ${operatorMap[operator]} ${sanitizeValue(value, operator)})')`
}

View File

@@ -11,6 +11,7 @@ import {
lt,
lte,
ne,
notIlike,
notInArray,
or,
type SQL,
@@ -31,6 +32,7 @@ type OperatorKeys =
| 'like'
| 'not_equals'
| 'not_in'
| 'not_like'
| 'or'
export type Operators = Record<OperatorKeys, (column: Column, value: SQLWrapper | unknown) => SQL>
@@ -48,6 +50,7 @@ export const operatorMap: Operators = {
less_than_equal: lte,
like: ilike,
not_equals: ne,
not_like: notIlike,
// TODO: support this
// all: all,
not_in: notInArray,

View File

@@ -161,6 +161,7 @@ export function parseParams({
like: { operator: 'like', wildcard: '%' },
not_equals: { operator: '<>', wildcard: '' },
not_in: { operator: 'not in', wildcard: '' },
not_like: { operator: 'not like', wildcard: '%' },
}
let formattedValue = val
@@ -175,11 +176,15 @@ export function parseParams({
formattedValue = ''
}
constraints.push(
sql.raw(
`${table[columnName].name}${jsonQuery} ${operatorKeys[operator].operator} ${formattedValue}`,
),
)
let jsonQuerySelector = `${table[columnName].name}${jsonQuery}`
if (adapter.name === 'sqlite' && operator === 'not_like') {
jsonQuerySelector = `COALESCE(${table[columnName].name}${jsonQuery}, '')`
}
const rawSQLQuery = `${jsonQuerySelector} ${operatorKeys[operator].operator} ${formattedValue}`
constraints.push(sql.raw(rawSQLQuery))
break
}

View File

@@ -1,10 +1,10 @@
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { UpdateOne } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from './types.js'
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { upsertRow } from './upsertRow/index.js'
@@ -28,6 +28,7 @@ export const updateOne: UpdateOne = async function updateOne(
where: whereToUse,
})
// selectDistinct will only return if there are joins
const selectDistinctResult = await selectDistinct({
adapter: this,
chainedMethods: [{ args: [1], method: 'limit' }],
@@ -40,22 +41,18 @@ export const updateOne: UpdateOne = async function updateOne(
if (selectDistinctResult?.[0]?.id) {
idToUpdate = selectDistinctResult?.[0]?.id
// If id wasn't passed but `where` without any joins, retrieve it with findFirst
} else if (whereArg && !joins.length) {
const findManyArgs = buildFindManyArgs({
adapter: this,
depth: 0,
fields: collection.flattenedFields,
joinQuery: false,
select: {},
tableName,
})
const table = this.tables[tableName]
findManyArgs.where = where
const docToUpdate = await db.query[tableName].findFirst(findManyArgs)
idToUpdate = docToUpdate?.id
const docsToUpdate = await (db as LibSQLDatabase)
.select({
id: table.id,
})
.from(table)
.where(where)
.limit(1)
idToUpdate = docsToUpdate?.[0]?.id
}
const result = await upsertRow({

View File

@@ -153,7 +153,7 @@ export const renderListView = async (
const renderedFilters = renderFilters(collectionConfig.fields, req.payload.importMap)
const resolvedFilterOptions = await resolveAllFilterOptions({
collectionConfig,
fields: collectionConfig.fields,
req,
})

View File

@@ -1,19 +1,21 @@
import type { CollectionConfig, PayloadRequest, ResolvedFilterOptions } from 'payload'
import type { Field, PayloadRequest, ResolvedFilterOptions } from 'payload'
import { resolveFilterOptions } from '@payloadcms/ui/rsc'
import { fieldIsHiddenOrDisabled } from 'payload/shared'
import { fieldHasSubFields, fieldIsHiddenOrDisabled } from 'payload/shared'
export const resolveAllFilterOptions = async ({
collectionConfig,
fields,
req,
result,
}: {
collectionConfig: CollectionConfig
fields: Field[]
req: PayloadRequest
result?: Map<string, ResolvedFilterOptions>
}): Promise<Map<string, ResolvedFilterOptions>> => {
const resolvedFilterOptions = new Map<string, ResolvedFilterOptions>()
const resolvedFilterOptions = !result ? new Map<string, ResolvedFilterOptions>() : result
await Promise.all(
collectionConfig.fields.map(async (field) => {
fields.map(async (field) => {
if (fieldIsHiddenOrDisabled(field)) {
return
}
@@ -28,8 +30,29 @@ export const resolveAllFilterOptions = async ({
siblingData: {}, // use empty object to prevent breaking queries when accessing properties of data
user: req.user,
})
resolvedFilterOptions.set(field.name, options)
}
if (fieldHasSubFields(field)) {
await resolveAllFilterOptions({
fields: field.fields,
req,
result: resolvedFilterOptions,
})
}
if (field.type === 'tabs') {
await Promise.all(
field.tabs.map((tab) =>
resolveAllFilterOptions({
fields: tab.fields,
req,
result: resolvedFilterOptions,
}),
),
)
}
}),
)

View File

@@ -147,10 +147,6 @@ export const withPayload = (nextConfig = {}) => {
toReturn.env.NEXT_BASE_PATH = nextConfig.basePath
}
if (nextConfig.assetPrefix) {
toReturn.env.NEXT_ASSET_PREFIX = nextConfig.assetPrefix
}
return toReturn
}

View File

@@ -1,19 +1,13 @@
import type * as AWS from '@aws-sdk/client-s3'
import type { CognitoUserSession } from 'amazon-cognito-identity-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 type { GetStorageClient } from './refreshSession.js'
import { authAsCognitoUser } from './authAsCognitoUser.js'
import { refreshSession } from './refreshSession.js'
export type GetStorageClient = () => Promise<{
identityID: string
storageClient: AWS.S3
}>
let storageClient: AWS.S3 | null = null
let session: CognitoUserSession | null = null
let identityID: string
export let storageClient: AWS.S3 | null = null
export let session: CognitoUserSession | null = null
export let identityID: string
export const getStorageClient: GetStorageClient = async () => {
if (storageClient && session?.isValid()) {
@@ -23,6 +17,8 @@ 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')
}
@@ -33,34 +29,6 @@ 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

@@ -0,0 +1,46 @@
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

@@ -88,7 +88,7 @@ export type BeforeChangeRichTextHookArgs<
errors?: ValidationFieldError[]
/** Only available in `beforeChange` field hooks */
mergeLocaleActions?: (() => Promise<void>)[]
mergeLocaleActions?: (() => Promise<void> | void)[]
/** A string relating to which operation the field type is currently executing within. */
operation?: 'create' | 'delete' | 'read' | 'update'
/** The sibling data of the document before changes being applied. */

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 } from '../../index.js'
import type { CollectionSlug, ColumnPreference } 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?: { accessor: string; active: boolean }[]
columns?: ColumnPreference[]
docs?: PaginatedDocs['docs']
enableRowSelections?: boolean
parent?: {

View File

@@ -1,20 +1,19 @@
// @ts-strict-ignore
import type { AuthStrategyFunctionArgs, AuthStrategyResult } from './index.js'
export const executeAuthStrategies = async (
args: AuthStrategyFunctionArgs,
): Promise<AuthStrategyResult> => {
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
if (!args.payload.authStrategies?.length) {
return { user: null }
}
return strategy.authenticate(args)
}
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 result
},
Promise.resolve({ user: null }),
)
}
}
return { user: null }
}

View File

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

View File

@@ -51,18 +51,18 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
// beforeOperation - Collection
// /////////////////////////////////////
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())
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
}
}
const {
collection: { config: collectionConfig },
@@ -227,17 +227,17 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
// beforeLogin - Collection
// /////////////////////////////////////
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())
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
}
}
const { exp, token } = await jwtSign({
fieldsToSign,
@@ -251,18 +251,18 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
// afterLogin - Collection
// /////////////////////////////////////
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())
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
}
}
// /////////////////////////////////////
// afterRead - Fields
@@ -286,17 +286,17 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
// afterRead - Collection
// /////////////////////////////////////
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())
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
}
}
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)
}
await collectionConfig.hooks.afterLogout.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
collection: args.collection?.config,
context: req.context,
req,
})) || args
}, Promise.resolve())
if (collectionConfig.hooks?.afterLogout?.length) {
for (const hook of collectionConfig.hooks.afterLogout) {
args =
(await hook({
collection: args.collection?.config,
context: req.context,
req,
})) || args
}
}
return true
}

View File

@@ -86,17 +86,17 @@ export const meOperation = async (args: Arguments): Promise<MeOperationResult> =
// After Me - Collection
// /////////////////////////////////////
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())
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
}
}
return result
}

View File

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

View File

@@ -91,17 +91,17 @@ export const resetPasswordOperation = async (args: Arguments): Promise<Result> =
// beforeValidate - Collection
// /////////////////////////////////////
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())
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,
})
}
}
// /////////////////////////////////////
// Update new password

View File

@@ -54,6 +54,8 @@ async function autoLogin({
await payload.find({
collection: collection.config.slug,
depth: isGraphQL ? 0 : collection.config.auth.depth,
limit: 1,
pagination: false,
where,
})
).docs[0]

View File

@@ -76,8 +76,18 @@ export const bin = async () => {
if (userBinScript) {
try {
const script: BinScript = await import(pathToFileURL(userBinScript.scriptPath).toString())
await script(config)
const module = await import(pathToFileURL(userBinScript.scriptPath).toString())
if (!module.script || typeof module.script !== 'function') {
console.error(
`Could not find "script" function export for script ${userBinScript.key} in ${userBinScript.scriptPath}`,
)
} else {
await module.script(config).catch((err: unknown) => {
console.log(`Script ${userBinScript.key} failed, details:`)
console.error(err)
})
}
} catch (err) {
console.log(`Could not find associated bin script for the ${userBinScript.key} command`)
console.error(err)

View File

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

View File

@@ -28,18 +28,18 @@ export const countVersionsOperation = async <TSlug extends CollectionSlug>(
// beforeOperation - Collection
// /////////////////////////////////////
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())
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
}
}
const {
collection: { config: collectionConfig },

View File

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

View File

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

View File

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

View File

@@ -63,18 +63,18 @@ export const findOperation = async <
// beforeOperation - Collection
// /////////////////////////////////////
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())
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
}
}
const {
collection: { config: collectionConfig },
@@ -257,9 +257,7 @@ export const findOperation = async <
result.docs.map(async (doc) => {
let docRef = doc
await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
await priorHook
for (const hook of collectionConfig.hooks.beforeRead) {
docRef =
(await hook({
collection: collectionConfig,
@@ -268,7 +266,7 @@ export const findOperation = async <
query: fullWhere,
req,
})) || docRef
}, Promise.resolve())
}
return docRef
}),
@@ -310,9 +308,7 @@ export const findOperation = async <
result.docs.map(async (doc) => {
let docRef = doc
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
for (const hook of collectionConfig.hooks.afterRead) {
docRef =
(await hook({
collection: collectionConfig,
@@ -322,7 +318,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
// /////////////////////////////////////
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())
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
}
}
const {
id,
@@ -221,18 +221,18 @@ export const findByIDOperation = async <
// beforeRead - Collection
// /////////////////////////////////////
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())
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
}
}
// /////////////////////////////////////
// afterRead - Fields
@@ -259,18 +259,18 @@ export const findByIDOperation = async <
// afterRead - Collection
// /////////////////////////////////////
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())
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
}
}
// /////////////////////////////////////
// afterOperation - Collection

View File

@@ -101,18 +101,18 @@ export const findVersionByIDOperation = async <TData extends TypeWithID = any>(
// beforeRead - Collection
// /////////////////////////////////////
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())
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
}
}
// /////////////////////////////////////
// afterRead - Fields
@@ -139,18 +139,18 @@ export const findVersionByIDOperation = async <TData extends TypeWithID = any>(
// afterRead - Collection
// /////////////////////////////////////
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())
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
}
}
// /////////////////////////////////////
// Return results

View File

@@ -96,18 +96,19 @@ 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
docRef.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: docRef.version,
query: fullWhere,
req,
})) || docRef.version
}, Promise.resolve())
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
}
}
return docRef
}),
@@ -147,9 +148,7 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
result.docs.map(async (doc) => {
const docRef = doc
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
for (const hook of collectionConfig.hooks.afterRead) {
docRef.version =
(await hook({
collection: collectionConfig,
@@ -159,7 +158,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
// /////////////////////////////////////
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}
}
// /////////////////////////////////////
// afterChange - Fields
@@ -196,19 +196,19 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
// afterChange - Collection
// /////////////////////////////////////
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())
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
}
}
return result
} catch (error: unknown) {

View File

@@ -62,18 +62,18 @@ export const updateOperation = async <
// beforeOperation - Collection
// /////////////////////////////////////
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.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
}
}
const {
collection: { config: collectionConfig },

View File

@@ -64,18 +64,18 @@ export const updateByIDOperation = async <
// beforeOperation - Collection
// /////////////////////////////////////
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.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
}
}
if (args.publishSpecificLocale) {
args.req.locale = args.publishSpecificLocale

View File

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

View File

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

View File

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

View File

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

View File

@@ -75,10 +75,8 @@ export const promise = async ({
if (fieldAffectsData(field)) {
// Execute hooks
if (field.hooks?.afterChange) {
await field.hooks.afterChange.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
for (const hook of field.hooks.afterChange) {
const hookedValue = await hook({
blockData,
collection,
context,
@@ -102,7 +100,7 @@ export const promise = async ({
if (hookedValue !== undefined) {
siblingDoc[field.name] = hookedValue
}
}, Promise.resolve())
}
}
}
@@ -242,17 +240,15 @@ export const promise = async ({
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
if (typeof field?.editor === 'function') {
if (typeof field.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
const editor: RichTextAdapter = field?.editor
const editor: RichTextAdapter = field.editor
if (editor?.hooks?.afterChange?.length) {
await editor.hooks.afterChange.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
for (const hook of editor.hooks.afterChange) {
const hookedValue = await hook({
collection,
context,
data,
@@ -275,7 +271,7 @@ export const promise = async ({
if (hookedValue !== undefined) {
siblingDoc[field.name] = hookedValue
}
}, Promise.resolve())
}
}
break
}

View File

@@ -237,18 +237,17 @@ export const promise = async ({
if (fieldAffectsData(field)) {
// Execute hooks
if (triggerHooks && field.hooks?.afterRead) {
await field.hooks.afterRead.reduce(async (priorHook, currentHook) => {
await priorHook
for (const hook of field.hooks.afterRead) {
const shouldRunHookOnAllLocales =
fieldShouldBeLocalized({ field, parentIsLocalized }) &&
(locale === 'all' || !flattenLocales) &&
typeof siblingDoc[field.name] === 'object'
if (shouldRunHookOnAllLocales) {
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) =>
(async () => {
const hookedValue = await currentHook({
const localesAndValues = Object.entries(siblingDoc[field.name])
await Promise.all(
localesAndValues.map(async ([localeKey, value]) => {
const hookedValue = await hook({
blockData,
collection,
context,
@@ -273,14 +272,12 @@ export const promise = async ({
})
if (hookedValue !== undefined) {
siblingDoc[field.name][locale] = hookedValue
siblingDoc[field.name][localeKey] = hookedValue
}
})(),
}),
)
await Promise.all(hookPromises)
} else {
const hookedValue = await currentHook({
const hookedValue = await hook({
blockData,
collection,
context,
@@ -308,7 +305,7 @@ export const promise = async ({
siblingDoc[field.name] = hookedValue
}
}
}, Promise.resolve())
}
}
// Execute access control
@@ -677,18 +674,18 @@ export const promise = async ({
const editor: RichTextAdapter = field?.editor
if (editor?.hooks?.afterRead?.length) {
await editor.hooks.afterRead.reduce(async (priorHook, currentHook) => {
await priorHook
for (const hook of editor.hooks.afterRead) {
const shouldRunHookOnAllLocales =
fieldShouldBeLocalized({ field, parentIsLocalized }) &&
(locale === 'all' || !flattenLocales) &&
typeof siblingDoc[field.name] === 'object'
if (shouldRunHookOnAllLocales) {
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) =>
(async () => {
const hookedValue = await currentHook({
const localesAndValues = Object.entries(siblingDoc[field.name])
await Promise.all(
localesAndValues.map(async ([locale, value]) => {
const hookedValue = await hook({
collection,
context,
currentDepth,
@@ -722,12 +719,10 @@ export const promise = async ({
if (hookedValue !== undefined) {
siblingDoc[field.name][locale] = hookedValue
}
})(),
}),
)
await Promise.all(hookPromises)
} else {
const hookedValue = await currentHook({
const hookedValue = await hook({
collection,
context,
currentDepth,
@@ -762,7 +757,7 @@ export const promise = async ({
siblingDoc[field.name] = hookedValue
}
}
}, Promise.resolve())
}
}
break
}

View File

@@ -81,10 +81,9 @@ export const beforeChange = async <T extends JsonObject>({
)
}
await mergeLocaleActions.reduce(async (priorAction, action) => {
await priorAction
for (const action of mergeLocaleActions) {
await action()
}, Promise.resolve())
}
return data
}

View File

@@ -32,7 +32,7 @@ type Args = {
fieldIndex: number
global: null | SanitizedGlobalConfig
id?: number | string
mergeLocaleActions: (() => Promise<void>)[]
mergeLocaleActions: (() => Promise<void> | void)[]
operation: Operation
parentIndexPath: string
parentIsLocalized: boolean
@@ -109,10 +109,8 @@ export const promise = async ({
// Execute hooks
if (field.hooks?.beforeChange) {
await field.hooks.beforeChange.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
for (const hook of field.hooks.beforeChange) {
const hookedValue = await hook({
blockData,
collection,
context,
@@ -136,7 +134,7 @@ export const promise = async ({
if (hookedValue !== undefined) {
siblingData[field.name] = hookedValue
}
}, Promise.resolve())
}
}
// Validate
@@ -193,28 +191,20 @@ export const promise = async ({
// Push merge locale action if applicable
if (localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
mergeLocaleActions.push(async () => {
const localeData = await localization.localeCodes.reduce(
async (localizedValuesPromise: Promise<JsonObject>, locale) => {
const localizedValues = await localizedValuesPromise
const fieldValue =
locale === req.locale
? siblingData[field.name]
: siblingDocWithLocales?.[field.name]?.[locale]
mergeLocaleActions.push(() => {
const localeData = {}
// const result = await localizedValues
// update locale value if it's not undefined
if (typeof fieldValue !== 'undefined') {
return {
...localizedValues,
[locale]: fieldValue,
}
}
for (const locale of localization.localeCodes) {
const fieldValue =
locale === req.locale
? siblingData[field.name]
: siblingDocWithLocales?.[field.name]?.[locale]
return localizedValuesPromise
},
Promise.resolve({}),
)
// update locale value if it's not undefined
if (typeof fieldValue !== 'undefined') {
localeData[locale] = fieldValue
}
}
// If there are locales with data, set the data
if (Object.keys(localeData).length > 0) {
@@ -424,10 +414,8 @@ export const promise = async ({
const editor: RichTextAdapter = field?.editor
if (editor?.hooks?.beforeChange?.length) {
await editor.hooks.beforeChange.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
for (const hook of editor.hooks.beforeChange) {
const hookedValue = await hook({
collection,
context,
data,
@@ -454,7 +442,7 @@ export const promise = async ({
if (hookedValue !== undefined) {
siblingData[field.name] = hookedValue
}
}, Promise.resolve())
}
}
break

View File

@@ -28,7 +28,7 @@ type Args = {
fields: (Field | TabAsField)[]
global: null | SanitizedGlobalConfig
id?: number | string
mergeLocaleActions: (() => Promise<void>)[]
mergeLocaleActions: (() => Promise<void> | void)[]
operation: Operation
parentIndexPath: string
/**

View File

@@ -6,7 +6,6 @@ import type { Block, Field, FieldHookArgs, TabAsField } from '../../config/types
import { fieldAffectsData, fieldShouldBeLocalized } from '../../config/types.js'
import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js'
import { runBeforeDuplicateHooks } from './runHook.js'
import { traverseFields } from './traverseFields.js'
type Args<T> = {
@@ -68,42 +67,37 @@ export const promise = async <T>({
// Run field beforeDuplicate hooks
if (Array.isArray(field.hooks?.beforeDuplicate)) {
if (fieldIsLocalized) {
const localeData = await localization.localeCodes.reduce(
async (localizedValuesPromise: Promise<JsonObject>, locale) => {
const localizedValues = await localizedValuesPromise
const localeData: JsonObject = {}
const beforeDuplicateArgs: FieldHookArgs = {
blockData,
collection,
context,
data: doc,
field,
global: undefined,
indexPath: indexPathSegments,
path: pathSegments,
previousSiblingDoc: siblingDoc,
previousValue: siblingDoc[field.name]?.[locale],
req,
schemaPath: schemaPathSegments,
siblingData: siblingDoc,
siblingDocWithLocales: siblingDoc,
siblingFields,
value: siblingDoc[field.name]?.[locale],
}
for (const locale of localization.localeCodes) {
const beforeDuplicateArgs: FieldHookArgs = {
blockData,
collection,
context,
data: doc,
field,
global: undefined,
indexPath: indexPathSegments,
path: pathSegments,
previousSiblingDoc: siblingDoc,
previousValue: siblingDoc[field.name]?.[locale],
req,
schemaPath: schemaPathSegments,
siblingData: siblingDoc,
siblingDocWithLocales: siblingDoc,
siblingFields,
value: siblingDoc[field.name]?.[locale],
}
const hookResult = await runBeforeDuplicateHooks(beforeDuplicateArgs)
let hookResult
for (const hook of field.hooks.beforeDuplicate) {
hookResult = await hook(beforeDuplicateArgs)
}
if (typeof hookResult !== 'undefined') {
return {
...localizedValues,
[locale]: hookResult,
}
}
return localizedValuesPromise
},
Promise.resolve({}),
)
if (typeof hookResult !== 'undefined') {
localeData[locale] = hookResult
}
}
siblingDoc[field.name] = localeData
} else {
@@ -126,7 +120,11 @@ export const promise = async <T>({
value: siblingDoc[field.name],
}
const hookResult = await runBeforeDuplicateHooks(beforeDuplicateArgs)
let hookResult
for (const hook of field.hooks.beforeDuplicate) {
hookResult = await hook(beforeDuplicateArgs)
}
if (typeof hookResult !== 'undefined') {
siblingDoc[field.name] = hookResult
}

View File

@@ -1,8 +0,0 @@
// @ts-strict-ignore
import type { FieldHookArgs } from '../../config/types.js'
export const runBeforeDuplicateHooks = async (args: FieldHookArgs) =>
await args.field.hooks.beforeDuplicate.reduce(async (priorHook, currentHook) => {
await priorHook
return await currentHook(args)
}, Promise.resolve())

View File

@@ -276,10 +276,8 @@ export const promise = async <T>({
// Execute hooks
if (field.hooks?.beforeValidate) {
await field.hooks.beforeValidate.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
for (const hook of field.hooks.beforeValidate) {
const hookedValue = await hook({
blockData,
collection,
context,
@@ -303,7 +301,7 @@ export const promise = async <T>({
if (hookedValue !== undefined) {
siblingData[field.name] = hookedValue
}
}, Promise.resolve())
}
}
// Execute access control
@@ -493,10 +491,8 @@ export const promise = async <T>({
const editor: RichTextAdapter = field?.editor
if (editor?.hooks?.beforeValidate?.length) {
await editor.hooks.beforeValidate.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
for (const hook of editor.hooks.beforeValidate) {
const hookedValue = await hook({
collection,
context,
data,
@@ -519,7 +515,7 @@ export const promise = async <T>({
if (hookedValue !== undefined) {
siblingData[field.name] = hookedValue
}
}, Promise.resolve())
}
}
break
}

View File

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

View File

@@ -102,17 +102,17 @@ export const findVersionByIDOperation = async <T extends TypeWithVersion<T> = an
// beforeRead - Collection
// /////////////////////////////////////
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())
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
}
}
// /////////////////////////////////////
// afterRead - Fields
@@ -139,18 +139,18 @@ export const findVersionByIDOperation = async <T extends TypeWithVersion<T> = an
// afterRead - Global
// /////////////////////////////////////
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())
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
}
}
return result
} catch (error: unknown) {

View File

@@ -126,15 +126,12 @@ export const findVersionsOperation = async <T extends TypeWithVersion<T>>(
// afterRead - Global
// /////////////////////////////////////
result = {
...result,
docs: await Promise.all(
if (globalConfig.hooks?.afterRead?.length) {
result.docs = await Promise.all(
result.docs.map(async (doc) => {
const docRef = doc
await globalConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
for (const hook of globalConfig.hooks.afterRead) {
docRef.version =
(await hook({
context: req.context,
@@ -144,11 +141,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
// /////////////////////////////////////
await globalConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
req,
})) || result
}, Promise.resolve())
if (globalConfig.hooks?.afterRead?.length) {
for (const hook of globalConfig.hooks.afterRead) {
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
req,
})) || result
}
}
// /////////////////////////////////////
// afterChange - Fields
@@ -174,18 +174,18 @@ export const restoreVersionOperation = async <T extends TypeWithVersion<T> = any
// afterChange - Global
// /////////////////////////////////////
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 (globalConfig.hooks?.afterChange?.length) {
for (const hook of globalConfig.hooks.afterChange) {
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
previousDoc,
req,
})) || result
}
}
if (shouldCommit) {
await commitTransaction(req)

View File

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

View File

@@ -922,11 +922,13 @@ export const getPayload = async (
) {
try {
const port = process.env.PORT || '3000'
const basePath = process.env.NEXT_BASE_PATH || ''
const assetPrefix = process.env.NEXT_ASSET_PREFIX || ''
const path = '/_next/webpack-hmr'
// The __NEXT_ASSET_PREFIX env variable is set for both assetPrefix and basePath (tested in Next.js 15.1.6)
const prefix = process.env.__NEXT_ASSET_PREFIX ?? ''
cached.ws = new WebSocket(
`ws://localhost:${port}${basePath}${assetPrefix}/_next/webpack-hmr`,
process.env.PAYLOAD_HMR_URL_OVERRIDE ?? `ws://localhost:${port}${prefix}${path}`,
)
cached.ws.onmessage = (event) => {
@@ -1372,6 +1374,7 @@ export { restoreVersionOperation as restoreVersionOperationGlobal } from './glob
export { updateOperation as updateOperationGlobal } from './globals/operations/update.js'
export type {
CollapsedPreferences,
ColumnPreference,
DocumentPreferences,
FieldsPreferences,
InsideFieldsPreferences,

View File

@@ -0,0 +1,19 @@
/**
* @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,6 +2,7 @@
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'
@@ -76,6 +77,14 @@ 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,8 +28,12 @@ export type DocumentPreferences = {
fields: FieldsPreferences
}
export type ColumnPreference = {
[key: string]: boolean
}
export type ListPreferences = {
columns?: { accessor: string; active: boolean }[]
columns?: ColumnPreference[]
limit?: number
sort?: string
}

View File

@@ -11,6 +11,7 @@ export const validOperators = [
'less_than',
'less_than_equal',
'like',
'not_like',
'within',
'intersects',
'near',

View File

@@ -19,7 +19,8 @@ export const addDataAndFileToRequest: AddDataAndFileToRequest = async (req) => {
if (contentType === 'application/json') {
let data = {}
try {
data = await req.json()
const text = await req.text()
data = text ? JSON.parse(text) : {}
} catch (error) {
req.payload.logger.error(error)
} finally {

View File

@@ -15,7 +15,6 @@ import type { SanitizedGlobalConfig } from '../globals/config/types.js'
import { MissingEditorProp } from '../errors/MissingEditorProp.js'
import { fieldAffectsData } from '../fields/config/types.js'
import { generateJobsJSONSchemas } from '../queues/config/generateJobsJSONSchemas.js'
import { deepCopyObject } from './deepCopyObject.js'
import { toWords } from './formatLabels.js'
import { getCollectionIDFieldTypes } from './getCollectionIDFieldTypes.js'
@@ -719,7 +718,7 @@ export function fieldsToJSONSchema(
// This function is part of the public API and is exported through payload/utilities
export function entityToJSONSchema(
config: SanitizedConfig,
incomingEntity: SanitizedCollectionConfig | SanitizedGlobalConfig,
entity: SanitizedCollectionConfig | SanitizedGlobalConfig,
interfaceNameDefinitions: Map<string, JSONSchema4>,
defaultIDType: 'number' | 'text',
collectionIDFieldTypes?: { [key: string]: 'number' | 'string' },
@@ -729,25 +728,30 @@ export function entityToJSONSchema(
collectionIDFieldTypes = getCollectionIDFieldTypes({ config, defaultIDType })
}
const entity: SanitizedCollectionConfig | SanitizedGlobalConfig = deepCopyObject(incomingEntity)
const title = entity.typescript?.interface
? entity.typescript.interface
: singular(toWords(entity.slug, true))
let mutableFields = [...entity.flattenedFields]
const idField: FieldAffectingData = { name: 'id', type: defaultIDType as 'text', required: true }
const customIdField = entity.flattenedFields.find(
(field) => field.name === 'id',
) as FieldAffectingData
const customIdField = mutableFields.find((field) => field.name === 'id') as FieldAffectingData
if (customIdField && customIdField.type !== 'group' && customIdField.type !== 'tab') {
customIdField.required = true
mutableFields = mutableFields.map((field) => {
if (field === customIdField) {
return { ...field, required: true }
}
return field
})
} else {
entity.flattenedFields.unshift(idField)
mutableFields.unshift(idField)
}
// mark timestamp fields required
if ('timestamps' in entity && entity.timestamps !== false) {
entity.flattenedFields = entity.flattenedFields.map((field) => {
mutableFields = mutableFields.map((field) => {
if (field.name === 'createdAt' || field.name === 'updatedAt') {
return {
...field,
@@ -765,7 +769,7 @@ export function entityToJSONSchema(
(typeof entity.auth?.disableLocalStrategy === 'object' &&
entity.auth.disableLocalStrategy.enableFields))
) {
entity.flattenedFields.push({
mutableFields.push({
name: 'password',
type: 'text',
})
@@ -777,7 +781,7 @@ export function entityToJSONSchema(
title,
...fieldsToJSONSchema(
collectionIDFieldTypes,
entity.flattenedFields,
mutableFields,
interfaceNameDefinitions,
config,
i18n,
@@ -1152,40 +1156,42 @@ export function configToJSONSchema(
)
: {}
const blocksDefinition: JSONSchema4 = {
const blocksDefinition: JSONSchema4 | undefined = {
type: 'object',
additionalProperties: false,
properties: {},
required: [],
}
for (const block of config.blocks) {
const blockFieldSchemas = fieldsToJSONSchema(
collectionIDFieldTypes,
block.flattenedFields,
interfaceNameDefinitions,
config,
i18n,
)
if (config?.blocks?.length) {
for (const block of config.blocks) {
const blockFieldSchemas = fieldsToJSONSchema(
collectionIDFieldTypes,
block.flattenedFields,
interfaceNameDefinitions,
config,
i18n,
)
const blockSchema: JSONSchema4 = {
type: 'object',
additionalProperties: false,
properties: {
...blockFieldSchemas.properties,
blockType: {
const: block.slug,
const blockSchema: JSONSchema4 = {
type: 'object',
additionalProperties: false,
properties: {
...blockFieldSchemas.properties,
blockType: {
const: block.slug,
},
},
},
required: ['blockType', ...blockFieldSchemas.required],
}
required: ['blockType', ...blockFieldSchemas.required],
}
const interfaceName = block.interfaceName ?? block.slug
interfaceNameDefinitions.set(interfaceName, blockSchema)
const interfaceName = block.interfaceName ?? block.slug
interfaceNameDefinitions.set(interfaceName, blockSchema)
blocksDefinition.properties[block.slug] = {
$ref: `#/definitions/${interfaceName}`,
blocksDefinition.properties[block.slug] = {
$ref: `#/definitions/${interfaceName}`,
}
;(blocksDefinition.required as string[]).push(block.slug)
}
;(blocksDefinition.required as string[]).push(block.slug)
}
let jsonSchema: JSONSchema4 = {
@@ -1225,6 +1231,7 @@ export function configToJSONSchema(
],
title: 'Config',
}
if (jobsSchemas.definitions?.size) {
for (const [key, value] of jobsSchemas.definitions) {
jsonSchema.definitions[key] = value

View File

@@ -1,2 +1,3 @@
export { TenantField } from '../components/TenantField/index.client.js'
export { TenantSelector } from '../components/TenantSelector/index.js'
export { useTenantSelection } from '../providers/TenantSelectionProvider/index.client.js'

View File

@@ -115,7 +115,13 @@ export const afterTenantDelete =
id: user.id,
collection: usersSlug,
data: {
tenants: (user.tenants || []).filter(({ tenant: tenantID }) => tenantID !== id),
[usersTenantsArrayFieldName]: (user[usersTenantsArrayFieldName] || []).filter(
(row: Record<string, string>) => {
if (row[usersTenantsArrayTenantFieldName]) {
return row[usersTenantsArrayTenantFieldName] !== id
}
},
),
},
}),
)

View File

@@ -92,6 +92,8 @@ export const multiTenantPlugin =
addCollectionAccess({
collection: adminUsersCollection,
fieldName: `${tenantsArrayFieldName}.${tenantsArrayTenantFieldName}`,
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
userHasAccessToAllTenants,
})
@@ -130,6 +132,8 @@ export const multiTenantPlugin =
addCollectionAccess({
collection,
fieldName: 'id',
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
userHasAccessToAllTenants,
})
}
@@ -205,6 +209,8 @@ export const multiTenantPlugin =
addCollectionAccess({
collection,
fieldName: tenantFieldName,
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
userHasAccessToAllTenants,
})
}

View File

@@ -9,9 +9,26 @@ import React, { createContext } from 'react'
import { SELECT_ALL } from '../../constants.js'
type ContextType = {
/**
* Array of options to select from
*/
options: OptionObject[]
/**
* The currently selected tenant ID
*/
selectedTenantID: number | string | undefined
/**
* Prevents a refresh when the tenant is changed
*
* If not switching tenants while viewing a "global", set to true
*/
setPreventRefreshOnChange: React.Dispatch<React.SetStateAction<boolean>>
/**
* Sets the selected tenant ID
*
* @param args.id - The ID of the tenant to select
* @param args.refresh - Whether to refresh the page after changing the tenant
*/
setTenant: (args: { id: number | string | undefined; refresh?: boolean }) => void
}

View File

@@ -20,6 +20,8 @@ const collectionAccessKeys: AllAccessKeys<
type Args<ConfigType> = {
collection: CollectionConfig
fieldName: string
tenantsArrayFieldName?: string
tenantsArrayTenantFieldName?: string
userHasAccessToAllTenants: Required<
MultiTenantPluginConfig<ConfigType>
>['userHasAccessToAllTenants']
@@ -32,6 +34,8 @@ type Args<ConfigType> = {
export const addCollectionAccess = <ConfigType>({
collection,
fieldName,
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
userHasAccessToAllTenants,
}: Args<ConfigType>): void => {
collectionAccessKeys.forEach((key) => {
@@ -40,7 +44,11 @@ export const addCollectionAccess = <ConfigType>({
}
collection.access[key] = withTenantAccess<ConfigType>({
accessFunction: collection.access?.[key],
collection,
fieldName: key === 'readVersions' ? `version.${fieldName}` : fieldName,
operation: key,
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
userHasAccessToAllTenants,
})
})

View File

@@ -52,6 +52,7 @@ export async function getGlobalViewRedirect({
depth: 0,
limit: 1,
overrideAccess: false,
pagination: false,
user,
where: {
[tenantFieldName]: {

View File

@@ -2,14 +2,25 @@ import type { Where } from 'payload'
import type { UserWithTenantsField } from '../types.js'
import { defaults } from '../defaults.js'
import { getUserTenantIDs } from './getUserTenantIDs.js'
type Args = {
fieldName: string
tenantsArrayFieldName?: string
tenantsArrayTenantFieldName?: string
user: UserWithTenantsField
}
export function getTenantAccess({ fieldName, user }: Args): Where {
const userAssignedTenantIDs = getUserTenantIDs(user)
export function getTenantAccess({
fieldName,
tenantsArrayFieldName = defaults.tenantsArrayFieldName,
tenantsArrayTenantFieldName = defaults.tenantsArrayTenantFieldName,
user,
}: Args): Where {
const userAssignedTenantIDs = getUserTenantIDs(user, {
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
})
return {
[fieldName]: {

View File

@@ -1,5 +1,6 @@
import type { Tenant, UserWithTenantsField } from '../types.js'
import { defaults } from '../defaults.js'
import { extractID } from './extractID.js'
/**
@@ -9,15 +10,26 @@ import { extractID } from './extractID.js'
*/
export const getUserTenantIDs = <IDType extends number | string>(
user: null | UserWithTenantsField,
options?: {
tenantsArrayFieldName?: string
tenantsArrayTenantFieldName?: string
},
): IDType[] => {
if (!user) {
return []
}
const {
tenantsArrayFieldName = defaults.tenantsArrayFieldName,
tenantsArrayTenantFieldName = defaults.tenantsArrayTenantFieldName,
} = options || {}
return (
user?.tenants?.reduce<IDType[]>((acc, { tenant }) => {
if (tenant) {
acc.push(extractID<IDType>(tenant as Tenant<IDType>))
(Array.isArray(user[tenantsArrayFieldName]) ? user[tenantsArrayFieldName] : [])?.reduce<
IDType[]
>((acc, row) => {
if (row[tenantsArrayTenantFieldName]) {
acc.push(extractID<IDType>(row[tenantsArrayTenantFieldName] as Tenant<IDType>))
}
return acc

View File

@@ -1,4 +1,12 @@
import type { Access, AccessArgs, AccessResult, User } from 'payload'
import type {
Access,
AccessArgs,
AccessResult,
AllOperations,
CollectionConfig,
User,
Where,
} from 'payload'
import type { MultiTenantPluginConfig, UserWithTenantsField } from '../types.js'
@@ -7,15 +15,26 @@ import { getTenantAccess } from './getTenantAccess.js'
type Args<ConfigType> = {
accessFunction?: Access
collection: CollectionConfig
fieldName: string
operation: AllOperations
tenantsArrayFieldName?: string
tenantsArrayTenantFieldName?: string
userHasAccessToAllTenants: Required<
MultiTenantPluginConfig<ConfigType>
>['userHasAccessToAllTenants']
}
export const withTenantAccess =
<ConfigType>({ accessFunction, fieldName, userHasAccessToAllTenants }: Args<ConfigType>) =>
<ConfigType>({
accessFunction,
collection,
fieldName,
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
userHasAccessToAllTenants,
}: Args<ConfigType>) =>
async (args: AccessArgs): Promise<AccessResult> => {
const constraints = []
const constraints: Where[] = []
const accessFn =
typeof accessFunction === 'function'
? accessFunction
@@ -34,12 +53,26 @@ export const withTenantAccess =
args.req.user as ConfigType extends { user: unknown } ? ConfigType['user'] : User,
)
) {
constraints.push(
getTenantAccess({
fieldName,
user: args.req.user as UserWithTenantsField,
}),
)
const tenantConstraint = getTenantAccess({
fieldName,
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
user: args.req.user as UserWithTenantsField,
})
if (collection.slug === args.req.user.collection) {
constraints.push({
or: [
{
id: {
equals: args.req.user.id,
},
},
tenantConstraint,
],
})
} else {
constraints.push(tenantConstraint)
}
return combineWhereConstraints(constraints)
}

Some files were not shown because too many files have changed in this diff Show More