Compare commits
143 Commits
v3.30.0
...
fix/virtua
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
219aebbec0 | ||
|
|
df7a3692f7 | ||
|
|
b750ba4509 | ||
|
|
d55306980e | ||
|
|
34ea6ec14f | ||
|
|
17d5168728 | ||
|
|
ed50a79643 | ||
|
|
0a59707ea0 | ||
|
|
bcbb912d50 | ||
|
|
1c99f46e4f | ||
|
|
c877b1ad43 | ||
|
|
4426625b83 | ||
|
|
23628996d0 | ||
|
|
b9832f40e4 | ||
|
|
a675c04c99 | ||
|
|
e79b20363e | ||
|
|
21599b87f5 | ||
|
|
e90ff72b37 | ||
|
|
babf4f965d | ||
|
|
6572bf4ae1 | ||
|
|
da7be35a15 | ||
|
|
55d00e2b1d | ||
|
|
5b554e5256 | ||
|
|
85e6edf21e | ||
|
|
b354d00aa4 | ||
|
|
c661d33b13 | ||
|
|
6b349378e0 | ||
|
|
39462bc6b9 | ||
|
|
3a7cd717b2 | ||
|
|
3287f7062f | ||
|
|
a9eca3a785 | ||
|
|
71e3c7839b | ||
|
|
a66f90ebb6 | ||
|
|
272914c818 | ||
|
|
466dcd7189 | ||
|
|
a72fa869f3 | ||
|
|
3523c2c6a6 | ||
|
|
112e081d8f | ||
|
|
eab9770315 | ||
|
|
4d7c1d45fa | ||
|
|
37bfc63da2 | ||
|
|
18ff9cbdb1 | ||
|
|
ae9e5e19ad | ||
|
|
7aa3c5ea6b | ||
|
|
a0fb3353c6 | ||
|
|
101f7658f7 | ||
|
|
9853f27667 | ||
|
|
e0046bba59 | ||
|
|
f1d9b44161 | ||
|
|
09916ad18e | ||
|
|
a90ae9d42b | ||
|
|
d19412f62d | ||
|
|
bd557a97d5 | ||
|
|
97e2e77ff4 | ||
|
|
acae547ddf | ||
|
|
ec34e64261 | ||
|
|
f079eced8a | ||
|
|
b809c98966 | ||
|
|
b9ffbc6994 | ||
|
|
09782be0e0 | ||
|
|
b270901fa6 | ||
|
|
c7b14bd44d | ||
|
|
83319be752 | ||
|
|
77210251f4 | ||
|
|
750210fabe | ||
|
|
6d831475a0 | ||
|
|
e109491dbe | ||
|
|
dee9abd5c1 | ||
|
|
5c54d9a567 | ||
|
|
36e7c59b4e | ||
|
|
9adbbde9a8 | ||
|
|
8ad22eb1c0 | ||
|
|
b76844dac9 | ||
|
|
f7ed8e90e1 | ||
|
|
e6aad5adfc | ||
|
|
4ebd3ce668 | ||
|
|
fae113b799 | ||
|
|
e87521a376 | ||
|
|
8880d705e3 | ||
|
|
018bdad247 | ||
|
|
816fb28f55 | ||
|
|
857e984fbb | ||
|
|
d47b753898 | ||
|
|
308cb64b9c | ||
|
|
6c735effff | ||
|
|
fd42ad5f52 | ||
|
|
a58ff57e4f | ||
|
|
06d937e903 | ||
|
|
8e93ad8f5f | ||
|
|
f310c90211 | ||
|
|
dc793d1d14 | ||
|
|
f9c73ad5f2 | ||
|
|
760cfadaad | ||
|
|
d29bdfc10f | ||
|
|
f34eb228c4 | ||
|
|
e5690fcab9 | ||
|
|
4ac6d21ef6 | ||
|
|
d963e6a54c | ||
|
|
968a066f45 | ||
|
|
373f6d1032 | ||
|
|
329cd0b876 | ||
|
|
6badb5ffcf | ||
|
|
5b0e0ab788 | ||
|
|
c844b4c848 | ||
|
|
9c88af4b20 | ||
|
|
9a1c3cf4cc | ||
|
|
a083d47368 | ||
|
|
96289bf555 | ||
|
|
a6f7ef837a | ||
|
|
03d4c5b2ee | ||
|
|
af8c7868d6 | ||
|
|
d1c0989da7 | ||
|
|
70b9cab393 | ||
|
|
4a0bc869dd | ||
|
|
62c4e81a1f | ||
|
|
2b6313ed48 | ||
|
|
21f7ba7b9d | ||
|
|
b863fd0915 | ||
|
|
f34cc637e3 | ||
|
|
59c9feeb45 | ||
|
|
1578cd2425 | ||
|
|
5ae5255ba3 | ||
|
|
98e4db07c3 | ||
|
|
6b56343b97 | ||
|
|
4fc2eec301 | ||
|
|
10ac9893ad | ||
|
|
35e6cfbdfc | ||
|
|
a5c3aa0e4f | ||
|
|
74f935bfb9 | ||
|
|
73fc3c607a | ||
|
|
7fb4b1324e | ||
|
|
61747082ef | ||
|
|
93cc66d745 | ||
|
|
f61f6b73c7 | ||
|
|
1081b4a0ff | ||
|
|
234df54446 | ||
|
|
fe9317a0dd | ||
|
|
eb1434e986 | ||
|
|
de0aaf6e91 | ||
|
|
3c4b3ee527 | ||
|
|
fb01b4046d | ||
|
|
8d374cb57d | ||
|
|
998181b986 |
11
.github/workflows/main.yml
vendored
11
.github/workflows/main.yml
vendored
@@ -294,14 +294,10 @@ jobs:
|
||||
- fields__collections__Email
|
||||
- fields__collections__Indexed
|
||||
- fields__collections__JSON
|
||||
- fields__collections__Lexical__e2e__main
|
||||
- fields__collections__Lexical__e2e__blocks
|
||||
- fields__collections__Lexical__e2e__blocks#config.blockreferences.ts
|
||||
- fields__collections__Number
|
||||
- fields__collections__Point
|
||||
- fields__collections__Radio
|
||||
- fields__collections__Relationship
|
||||
- fields__collections__RichText
|
||||
- fields__collections__Row
|
||||
- fields__collections__Select
|
||||
- fields__collections__Tabs
|
||||
@@ -309,6 +305,12 @@ jobs:
|
||||
- fields__collections__Text
|
||||
- fields__collections__UI
|
||||
- fields__collections__Upload
|
||||
- hooks
|
||||
- lexical__collections__Lexical__e2e__main
|
||||
- lexical__collections__Lexical__e2e__blocks
|
||||
- lexical__collections__Lexical__e2e__blocks#config.blockreferences.ts
|
||||
- lexical__collections__RichText
|
||||
- query-presets
|
||||
- form-state
|
||||
- live-preview
|
||||
- localization
|
||||
@@ -319,6 +321,7 @@ jobs:
|
||||
- plugin-import-export
|
||||
- plugin-nested-docs
|
||||
- plugin-seo
|
||||
- sort
|
||||
- versions
|
||||
- uploads
|
||||
env:
|
||||
|
||||
2
.github/workflows/post-release-templates.yml
vendored
2
.github/workflows/post-release-templates.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
||||
echo "DATABASE_URI=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
|
||||
|
||||
- name: Start MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
uses: supercharge/mongodb-github-action@1.12.0
|
||||
with:
|
||||
mongodb-version: 6.0
|
||||
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -323,3 +323,5 @@ test/databaseAdapter.js
|
||||
test/.localstack
|
||||
test/google-cloud-storage
|
||||
test/azurestoragedata/
|
||||
|
||||
licenses.csv
|
||||
|
||||
@@ -21,10 +21,9 @@ When a user starts editing a document, Payload locks it for that user. If anothe
|
||||
The lock will automatically expire after a set period of inactivity, configurable using the `duration` property in the `lockDocuments` configuration, after which others can resume editing.
|
||||
|
||||
<Banner type="info">
|
||||
{' '}
|
||||
**Note:** If your application does not require document locking, you can
|
||||
disable this feature for any collection or global by setting the
|
||||
`lockDocuments` property to `false`.{' '}
|
||||
`lockDocuments` property to `false`.
|
||||
</Banner>
|
||||
|
||||
### Config Options
|
||||
|
||||
@@ -718,7 +718,7 @@ The `useDocumentInfo` hook provides information about the current document being
|
||||
| **`currentEditor`** | The user currently editing the document. |
|
||||
| **`docConfig`** | Either the Collection or Global config of the document, depending on what is being edited. |
|
||||
| **`docPermissions`** | The current document's permissions. Fallback to collection permissions when no id is present. |
|
||||
| **`documentIsLocked`** | Whether the document is currently locked by another user. [More details](./locked-documents). |
|
||||
| **`documentIsLocked`** | Whether the document is currently locked by another user. [More details](./locked-documents). |
|
||||
| **`getDocPermissions`** | Method to retrieve document-level permissions. |
|
||||
| **`getDocPreferences`** | Method to retrieve document-level user preferences. [More details](./preferences). |
|
||||
| **`globalSlug`** | The slug of the global if editing a global document. |
|
||||
@@ -730,7 +730,7 @@ The `useDocumentInfo` hook provides information about the current document being
|
||||
| **`initialData`** | The initial data of the document. |
|
||||
| **`isEditing`** | Whether the document is being edited (as opposed to created). |
|
||||
| **`isInitializing`** | Whether the document info is still initializing. |
|
||||
| **`isLocked`** | Whether the document is locked. [More details](./locked-documents). |
|
||||
| **`isLocked`** | Whether the document is locked. [More details](./locked-documents). |
|
||||
| **`lastUpdateTime`** | Timestamp of the last update to the document. |
|
||||
| **`mostRecentVersionIsAutosaved`** | Whether the most recent version is an autosaved version. |
|
||||
| **`preferencesKey`** | The `preferences` key to use when interacting with document-level user preferences. [More details](./preferences). |
|
||||
@@ -739,9 +739,9 @@ The `useDocumentInfo` hook provides information about the current document being
|
||||
| **`setDocumentTitle`** | Method to set the document title. |
|
||||
| **`setHasPublishedDoc`** | Method to update whether the document has been published. |
|
||||
| **`title`** | The title of the document. |
|
||||
| **`unlockDocument`** | Method to unlock a document. [More details](./locked-documents). |
|
||||
| **`unlockDocument`** | Method to unlock a document. [More details](./locked-documents). |
|
||||
| **`unpublishedVersionCount`** | The number of unpublished versions of the document. |
|
||||
| **`updateDocumentEditor`** | Method to update who is currently editing the document. [More details](./locked-documents). |
|
||||
| **`updateDocumentEditor`** | Method to update who is currently editing the document. [More details](./locked-documents). |
|
||||
| **`updateSavedDocumentData`** | Method to update the saved document data. |
|
||||
| **`uploadStatus`** | Status of any uploads in progress ('idle', 'uploading', or 'failed'). |
|
||||
| **`versionCount`** | The current version count of the document. |
|
||||
@@ -791,17 +791,18 @@ const MyComponent: React.FC = () => {
|
||||
|
||||
The `useListQuery` hook returns an object with the following properties:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------------- | ------------------------------------------------------------------------ |
|
||||
| **`data`** | The data that is being displayed in the List View. |
|
||||
| **`defaultLimit`** | The default limit of items to display in the List View. |
|
||||
| **`defaultSort`** | The default sort order of items in the List View. |
|
||||
| **`handlePageChange`** | A method to handle page changes in the List View. |
|
||||
| **`handlePerPageChange`** | A method to handle per page changes in the List View. |
|
||||
| **`handleSearchChange`** | A method to handle search changes in the List View. |
|
||||
| **`handleSortChange`** | A method to handle sort changes in the List View. |
|
||||
| **`handleWhereChange`** | A method to handle where changes in the List View. |
|
||||
| **`query`** | The current query that is being used to fetch the data in the List View. |
|
||||
| Property | Description |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------- |
|
||||
| **`data`** | The data that is being displayed in the List View. |
|
||||
| **`defaultLimit`** | The default limit of items to display in the List View. |
|
||||
| **`defaultSort`** | The default sort order of items in the List View. |
|
||||
| **`handlePageChange`** | A method to handle page changes in the List View. |
|
||||
| **`handlePerPageChange`** | A method to handle per page changes in the List View. |
|
||||
| **`handleSearchChange`** | A method to handle search changes in the List View. |
|
||||
| **`handleSortChange`** | A method to handle sort changes in the List View. |
|
||||
| **`handleWhereChange`** | A method to handle where changes in the List View. |
|
||||
| **`modified`** | Whether the query has been changed from its [Query Preset](../query-presets/overview). |
|
||||
| **`query`** | The current query that is being used to fetch the data in the List View. |
|
||||
|
||||
## useSelection
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ mutation {
|
||||
|
||||
```ts
|
||||
const result = await payload.login({
|
||||
collection: '[collection-slug]',
|
||||
collection: 'collection-slug',
|
||||
data: {
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'get-out',
|
||||
@@ -166,6 +166,13 @@ const result = await payload.login({
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Server Functions:** Payload offers a ready-to-use `login` server function
|
||||
that utilizes the Local API. For integration details and examples, check out
|
||||
the [Server Function
|
||||
docs](../local-api/server-functions#reusable-payload-server-functions).
|
||||
</Banner>
|
||||
|
||||
## Logout
|
||||
|
||||
As Payload sets HTTP-only cookies, logging out cannot be done by just removing a cookie in JavaScript, as HTTP-only cookies are inaccessible by JS within the browser. So, Payload exposes a `logout` operation to delete the token in a safe way.
|
||||
@@ -189,6 +196,13 @@ mutation {
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Server Functions:** Payload provides a ready-to-use `logout` server function
|
||||
that manages the user's cookie for a seamless logout. For integration details
|
||||
and examples, check out the [Server Function
|
||||
docs](../local-api/server-functions#reusable-payload-server-functions).
|
||||
</Banner>
|
||||
|
||||
## Refresh
|
||||
|
||||
Allows for "refreshing" JWTs. If your user has a token that is about to expire, but the user is still active and using the app, you might want to use the `refresh` operation to receive a new token by executing this operation via the authenticated user.
|
||||
@@ -240,6 +254,13 @@ mutation {
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Server Functions:** Payload exports a ready-to-use `refresh` server function
|
||||
that automatically renews the user's token and updates the associated cookie.
|
||||
For integration details and examples, check out the [Server Function
|
||||
docs](../local-api/server-functions#reusable-payload-server-functions).
|
||||
</Banner>
|
||||
|
||||
## Verify by Email
|
||||
|
||||
If your collection supports email verification, the Verify operation will be exposed which accepts a verification token and sets the user's `_verified` property to `true`, thereby allowing the user to authenticate with the Payload API.
|
||||
@@ -270,7 +291,7 @@ mutation {
|
||||
|
||||
```ts
|
||||
const result = await payload.verifyEmail({
|
||||
collection: '[collection-slug]',
|
||||
collection: 'collection-slug',
|
||||
token: 'TOKEN_HERE',
|
||||
})
|
||||
```
|
||||
@@ -308,7 +329,7 @@ mutation {
|
||||
|
||||
```ts
|
||||
const result = await payload.unlock({
|
||||
collection: '[collection-slug]',
|
||||
collection: 'collection-slug',
|
||||
})
|
||||
```
|
||||
|
||||
@@ -349,7 +370,7 @@ mutation {
|
||||
|
||||
```ts
|
||||
const token = await payload.forgotPassword({
|
||||
collection: '[collection-slug]',
|
||||
collection: 'collection-slug',
|
||||
data: {
|
||||
email: 'dev@payloadcms.com',
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ keywords: authentication, config, configuration, overview, documentation, Conten
|
||||
title="Simplified Authentication for Headless CMS: Unlocking Reusability in One Line"
|
||||
/>
|
||||
|
||||
Authentication is a critical part of any application. Payload provides a secure, portable way to manage user accounts out of the box. Payload Authentication is designed to be used in both the [Admin Panel](../admin/overview), all well as your own external applications, completely eliminating the need for paid, third-party platforms and services.
|
||||
Authentication is a critical part of any application. Payload provides a secure, portable way to manage user accounts out of the box. Payload Authentication is designed to be used in both the [Admin Panel](../admin/overview), as well as your own external applications, completely eliminating the need for paid, third-party platforms and services.
|
||||
|
||||
Here are some common use cases of Authentication in your own applications:
|
||||
|
||||
|
||||
@@ -60,29 +60,31 @@ export const Posts: CollectionConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
|
||||
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
|
||||
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
|
||||
| `custom` | Extension point for adding custom data (e.g. for plugins) |
|
||||
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
|
||||
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
|
||||
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
|
||||
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
|
||||
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
|
||||
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
|
||||
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
|
||||
| `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
||||
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
|
||||
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
|
||||
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
|
||||
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
|
||||
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
|
||||
| Option | Description |
|
||||
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
|
||||
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
|
||||
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
|
||||
| `custom` | Extension point for adding custom data (e.g. for plugins) |
|
||||
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
|
||||
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
|
||||
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
|
||||
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
|
||||
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
|
||||
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
|
||||
| `orderable` | If true, enables custom ordering for the collection, and documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
|
||||
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| `enableQueryPresets` | Enable query presets for this Collection. [More details](../query-presets/overview). |
|
||||
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
|
||||
| `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
||||
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
|
||||
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
|
||||
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
|
||||
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
|
||||
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
@@ -176,7 +178,7 @@ The following options are available:
|
||||
```ts
|
||||
import type { CollectionCOnfig } from 'payload'
|
||||
|
||||
export const MyCollection: CollectionCOnfig = {
|
||||
export const MyCollection: CollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
|
||||
@@ -84,6 +84,7 @@ The following options are available:
|
||||
| **`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. |
|
||||
| `queryPresets` | An object that to configure Collection Query Presets. [More details](../query-presets/overview). |
|
||||
| **`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). |
|
||||
@@ -146,7 +147,7 @@ _\* Config location detection is different between development and production en
|
||||
|
||||
<Banner type="warning">
|
||||
**Important:** Ensure your `tsconfig.json` is properly configured for Payload
|
||||
to auto-detect your config location. If if does not exist, or does not specify
|
||||
to auto-detect your config location. If it does not exist, or does not specify
|
||||
the proper `compilerOptions`, Payload will default to the current working
|
||||
directory.
|
||||
</Banner>
|
||||
@@ -238,9 +239,9 @@ export default buildConfig({
|
||||
// ...
|
||||
// highlight-start
|
||||
cors: {
|
||||
origins: ['http://localhost:3000']
|
||||
headers: ['x-custom-header']
|
||||
}
|
||||
origins: ['http://localhost:3000'],
|
||||
headers: ['x-custom-header'],
|
||||
},
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
@@ -55,7 +55,7 @@ For more granular control, pass a configuration object instead. Payload exposes
|
||||
| `exact` | Boolean. When true, will only match if the path matches the `usePathname()` exactly. |
|
||||
| `strict` | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. |
|
||||
| `sensitive` | When true, will match if the path is case sensitive. |
|
||||
| `meta` | Page metadata overrides to apply to this view within the Admin Panel. [More details](./metadata). |
|
||||
| `meta` | Page metadata overrides to apply to this view within the Admin Panel. [More details](../admin/metadata). |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc:
|
||||
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Edit View is where users interact with individual [Collection](../collections/overview) and [Global](../globals/overview) Documents within the [Admin Panel](../admin/overview). The Edit View contains the actual form in which submits the data to the server. This is where they can view, edit, and save their content. It contains controls for saving, publishing, and previewing the document, all of which can be customized to a high degree.
|
||||
The Edit View is where users interact with individual [Collection](../configuration/collections) and [Global](../configuration/globals) Documents within the [Admin Panel](../admin/overview). The Edit View contains the actual form in which submits the data to the server. This is where they can view, edit, and save their content. It contains controls for saving, publishing, and previewing the document, all of which can be customized to a high degree.
|
||||
|
||||
The Edit View can be swapped out in its entirety for a Custom View, or it can be injected with a number of Custom Components to add additional functionality or presentational elements without replacing the entire view.
|
||||
|
||||
@@ -101,14 +101,15 @@ export const MyCollection: CollectionConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Path | Description |
|
||||
| ----------------- | -------------------------------------------------------------------------------------- |
|
||||
| `SaveButton` | A button that saves the current document. [More details](#SaveButton). |
|
||||
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#SaveDraftButton). |
|
||||
| `PublishButton` | A button that publishes the current document. [More details](#PublishButton). |
|
||||
| `PreviewButton` | A button that previews the current document. [More details](#PreviewButton). |
|
||||
| `Description` | A description of the Collection. [More details](#Description). |
|
||||
| `Upload` | A file upload component. [More details](#Upload). |
|
||||
| Path | Description |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
|
||||
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). |
|
||||
| `SaveButton` | A button that saves the current document. [More details](#savebutton). |
|
||||
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). |
|
||||
| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). |
|
||||
| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). |
|
||||
| `Description` | A description of the Collection. [More details](#description). |
|
||||
| `Upload` | A file upload component. [More details](#upload). |
|
||||
|
||||
#### Globals
|
||||
|
||||
@@ -133,13 +134,14 @@ export const MyGlobal: GlobalConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Path | Description |
|
||||
| ----------------- | -------------------------------------------------------------------------------------- |
|
||||
| `SaveButton` | A button that saves the current document. [More details](#SaveButton). |
|
||||
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#SaveDraftButton). |
|
||||
| `PublishButton` | A button that publishes the current document. [More details](#PublishButton). |
|
||||
| `PreviewButton` | A button that previews the current document. [More details](#PreviewButton). |
|
||||
| `Description` | A description of the Global. [More details](#Description). |
|
||||
| Path | Description |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
|
||||
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). |
|
||||
| `SaveButton` | A button that saves the current document. [More details](#savebutton). |
|
||||
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). |
|
||||
| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). |
|
||||
| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). |
|
||||
| `Description` | A description of the Global. [More details](#description). |
|
||||
|
||||
### SaveButton
|
||||
|
||||
@@ -191,6 +193,73 @@ export function MySaveButton(props: SaveButtonClientProps) {
|
||||
}
|
||||
```
|
||||
|
||||
### beforeDocumentControls
|
||||
|
||||
The `beforeDocumentControls` property allows you to render custom components just before the default document action buttons (like Save, Publish, or Preview). This is useful for injecting custom buttons, status indicators, or any other UI elements before the built-in controls.
|
||||
|
||||
To add `beforeDocumentControls` components, use the `components.edit.beforeDocumentControls` property in you [Collection Config](../configuration/collections) or `components.elements.beforeDocumentControls` in your [Global Config](../configuration/globals):
|
||||
|
||||
#### Collections
|
||||
|
||||
```
|
||||
export const MyCollection: CollectionConfig = {
|
||||
admin: {
|
||||
components: {
|
||||
edit: {
|
||||
// highlight-start
|
||||
beforeDocumentControls: ['/path/to/CustomComponent'],
|
||||
// highlight-end
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Globals
|
||||
|
||||
```
|
||||
export const MyGlobal: GlobalConfig = {
|
||||
admin: {
|
||||
components: {
|
||||
elements: {
|
||||
// highlight-start
|
||||
beforeDocumentControls: ['/path/to/CustomComponent'],
|
||||
// highlight-end
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Here's an example of a custom `beforeDocumentControls` component:
|
||||
|
||||
#### Server Component
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import type { BeforeDocumentControlsServerProps } from 'payload'
|
||||
|
||||
export function MyCustomDocumentControlButton(
|
||||
props: BeforeDocumentControlsServerProps,
|
||||
) {
|
||||
return <div>This is a custom beforeDocumentControl button (Server)</div>
|
||||
}
|
||||
```
|
||||
|
||||
#### Client Component
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import type { BeforeDocumentControlsClientProps } from 'payload'
|
||||
|
||||
export function MyCustomDocumentControlButton(
|
||||
props: BeforeDocumentControlsClientProps,
|
||||
) {
|
||||
return <div>This is a custom beforeDocumentControl button (Client)</div>
|
||||
}
|
||||
```
|
||||
|
||||
### SaveDraftButton
|
||||
|
||||
The `SaveDraftButton` property allows you to render a custom Save Draft Button in the Edit View.
|
||||
|
||||
@@ -6,13 +6,13 @@ desc:
|
||||
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The List View is where users interact with a list of [Collection](../collections/overview) Documents within the [Admin Panel](../admin/overview). This is where they can view, sort, filter, and paginate their documents to find exactly what they're looking for. This is also where users can perform bulk operations on multiple documents at once, such as deleting, editing, or publishing many.
|
||||
The List View is where users interact with a list of [Collection](../configuration/collections) Documents within the [Admin Panel](../admin/overview). This is where they can view, sort, filter, and paginate their documents to find exactly what they're looking for. This is also where users can perform bulk operations on multiple documents at once, such as deleting, editing, or publishing many.
|
||||
|
||||
The List View can be swapped out in its entirety for a Custom View, or it can be injected with a number of Custom Components to add additional functionality or presentational elements without replacing the entire view.
|
||||
|
||||
<Banner type="info">
|
||||
**Note:** Only [Collections](../collections/overview) have a List View.
|
||||
[Globals](../globals/overview) do not have a List View as they are single
|
||||
**Note:** Only [Collections](../configuration/collections) have a List View.
|
||||
[Globals](../configuration/globals) do not have a List View as they are single
|
||||
documents.
|
||||
</Banner>
|
||||
|
||||
@@ -90,11 +90,11 @@ The following options are available:
|
||||
|
||||
| Path | Description |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `beforeList` | An array of custom components to inject before the list of documents in the List View. [More details](#beforeList). |
|
||||
| `beforeListTable` | An array of custom components to inject before the table of documents in the List View. [More details](#beforeListTable). |
|
||||
| `afterList` | An array of custom components to inject after the list of documents in the List View. [More details](#afterList). |
|
||||
| `afterListTable` | An array of custom components to inject after the table of documents in the List View. [More details](#afterListTable). |
|
||||
| `Description` | A component to render a description of the Collection. [More details](#Description). |
|
||||
| `beforeList` | An array of custom components to inject before the list of documents in the List View. [More details](#beforelist). |
|
||||
| `beforeListTable` | An array of custom components to inject before the table of documents in the List View. [More details](#beforelisttable). |
|
||||
| `afterList` | An array of custom components to inject after the list of documents in the List View. [More details](#afterlist). |
|
||||
| `afterListTable` | An array of custom components to inject after the table of documents in the List View. [More details](#afterlisttable). |
|
||||
| `Description` | A component to render a description of the Collection. [More details](#description). |
|
||||
|
||||
### beforeList
|
||||
|
||||
|
||||
@@ -352,18 +352,20 @@ const config = buildConfig({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
{
|
||||
slug: 'collection2',
|
||||
fields: [
|
||||
{
|
||||
name: 'editor',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
BlocksFeature({
|
||||
// Same reference can be reused anywhere, even in the lexical editor, without incurred performance hit
|
||||
blocks: ['TextBlock'],
|
||||
})
|
||||
})
|
||||
features: [
|
||||
BlocksFeature({
|
||||
// Same reference can be reused anywhere, even in the lexical editor, without incurred performance hit
|
||||
blocks: ['TextBlock'],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -138,6 +138,7 @@ powerful Admin UI.
|
||||
| **`name`** \* | To be used as the property name when retrieved from the database. [More](./overview#field-names) |
|
||||
| **`collection`** \* | The `slug`s having the relationship field or an array of collection slugs. |
|
||||
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. If `collection` is an array, this field must exist for all specified collections |
|
||||
| **`orderable`** | If true, enables custom ordering and joined documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
|
||||
| **`where`** | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
|
||||
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](../queries/depth#max-depth). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
@@ -270,21 +271,6 @@ const result = await payload.find({
|
||||
and blocks.
|
||||
</Banner>
|
||||
|
||||
<Banner type="warning">
|
||||
Currently, querying by the Join Field itself is not supported, meaning:
|
||||
```ts
|
||||
payload.find({
|
||||
collection: 'categories',
|
||||
where: {
|
||||
'relatedPosts.title': { // relatedPosts is a join field
|
||||
equals: "post"
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
does not work yet.
|
||||
</Banner>
|
||||
|
||||
### Rest API
|
||||
|
||||
The REST API supports the same query options as the Local API. You can use the `joins` query parameter to customize the
|
||||
|
||||
@@ -541,6 +541,7 @@ The `ctx` object:
|
||||
| Property | Description |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`blockData`** | The nearest parent block's data. If the field is not inside a block, this will be `undefined`. |
|
||||
| **`operation`** | A string relating to which operation the field type is currently executing within. |
|
||||
| **`path`** | The full path to the field in the schema, represented as an array of string segments, including array indexes. I.e `['group', 'myArray', '1', 'textField']`. |
|
||||
| **`user`** | The currently authenticated user object. |
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ The Relationship Field inherits all of the default options from the base [Field
|
||||
| **`allowCreate`** | Set to `false` if you'd like to disable the ability to create new documents from within the relationship field. |
|
||||
| **`allowEdit`** | Set to `false` if you'd like to disable the ability to edit documents from within the relationship field. |
|
||||
| **`sortOptions`** | Define a default sorting order for the options within a Relationship field's dropdown. [More](#sort-options) |
|
||||
| **`appearance`** | Set to `drawer` or `select` to change the behavior of the field. Defaults to `select`. |
|
||||
|
||||
### Sort Options
|
||||
|
||||
|
||||
@@ -63,10 +63,16 @@ To install a Database Adapter, you can run **one** of the following commands:
|
||||
```
|
||||
|
||||
- To install the [Postgres Adapter](../database/postgres), run:
|
||||
|
||||
```bash
|
||||
pnpm i @payloadcms/db-postgres
|
||||
```
|
||||
|
||||
- To install the [SQLite Adapter](../database/sqlite), run:
|
||||
```bash
|
||||
pnpm i @payloadcms/db-sqlite
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Note:** New [Database Adapters](/docs/database/overview) are becoming
|
||||
available every day. Check the docs for the most up-to-date list of what's
|
||||
|
||||
@@ -28,7 +28,7 @@ Then, you could configure two different runner strategies:
|
||||
|
||||
As mentioned above, you can queue jobs, but the jobs won't run unless a worker picks up your jobs and runs them. This can be done in four ways:
|
||||
|
||||
#### Cron jobs
|
||||
### Cron jobs
|
||||
|
||||
You can use the `jobs.autoRun` property to configure cron jobs:
|
||||
|
||||
@@ -63,7 +63,7 @@ export default buildConfig({
|
||||
and should not be used on serverless platforms like Vercel.
|
||||
</Banner>
|
||||
|
||||
#### Endpoint
|
||||
### Endpoint
|
||||
|
||||
You can execute jobs by making a fetch request to the `/api/payload-jobs/run` endpoint:
|
||||
|
||||
@@ -130,7 +130,7 @@ This works because Vercel automatically makes the `CRON_SECRET` environment vari
|
||||
|
||||
After the project is deployed to Vercel, the Vercel Cron job will automatically trigger the `/api/payload-jobs/run` endpoint in the specified schedule, running the queued jobs in the background.
|
||||
|
||||
#### Local API
|
||||
### Local API
|
||||
|
||||
If you want to process jobs programmatically from your server-side code, you can use the Local API:
|
||||
|
||||
@@ -156,7 +156,7 @@ const results = await payload.jobs.runByID({
|
||||
})
|
||||
```
|
||||
|
||||
#### Bin script
|
||||
### Bin script
|
||||
|
||||
Finally, you can process jobs via the bin script that comes with Payload out of the box.
|
||||
|
||||
@@ -169,3 +169,76 @@ In addition, the bin script allows you to pass a `--cron` flag to the `jobs:run`
|
||||
```sh
|
||||
npx payload jobs:run --cron "*/5 * * * *"
|
||||
```
|
||||
|
||||
## Processing Order
|
||||
|
||||
By default, jobs are processed first in, first out (FIFO). This means that the first job added to the queue will be the first one processed. However, you can also configure the order in which jobs are processed.
|
||||
|
||||
### Jobs Configuration
|
||||
|
||||
You can configure the order in which jobs are processed in the jobs configuration by passing the `processingOrder` property. This mimics the Payload [sort](../queries/sort) property that's used for functionality such as `payload.find()`.
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// Other configurations...
|
||||
jobs: {
|
||||
tasks: [
|
||||
// your tasks here
|
||||
],
|
||||
processingOrder: '-createdAt', // Process jobs in reverse order of creation = LIFO
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You can also set this on a queue-by-queue basis:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// Other configurations...
|
||||
jobs: {
|
||||
tasks: [
|
||||
// your tasks here
|
||||
],
|
||||
processingOrder: {
|
||||
default: 'createdAt', // FIFO
|
||||
queues: {
|
||||
nightly: '-createdAt', // LIFO
|
||||
myQueue: '-createdAt', // LIFO
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
If you need even more control over the processing order, you can pass a function that returns the processing order - this function will be called every time a queue starts processing jobs.
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// Other configurations...
|
||||
jobs: {
|
||||
tasks: [
|
||||
// your tasks here
|
||||
],
|
||||
processingOrder: ({ queue }) => {
|
||||
if (queue === 'myQueue') {
|
||||
return '-createdAt' // LIFO
|
||||
}
|
||||
return 'createdAt' // FIFO
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Local API
|
||||
|
||||
You can configure the order in which jobs are processed in the `payload.jobs.queue` method by passing the `processingOrder` property.
|
||||
|
||||
```ts
|
||||
const createdJob = await payload.jobs.queue({
|
||||
workflow: 'createPostAndUpdate',
|
||||
input: {
|
||||
title: 'my title',
|
||||
},
|
||||
processingOrder: '-createdAt', // Process jobs in reverse order of creation = LIFO
|
||||
})
|
||||
```
|
||||
|
||||
47
docs/local-api/access-control.mdx
Normal file
47
docs/local-api/access-control.mdx
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: Respecting Access Control with Local API Operations
|
||||
label: Access Control
|
||||
order: 40
|
||||
desc: Learn how to implement and enforce access control in Payload's Local API operations, ensuring that the right permissions are respected during data manipulation.
|
||||
keywords: server functions, local API, Payload, CMS, access control, permissions, user context, server-side logic, custom workflows, data management, headless CMS, TypeScript, Node.js, backend
|
||||
---
|
||||
|
||||
In Payload, local API operations **override access control by default**. This means that operations will run without checking if the current user has permission to perform the action. This is useful in certain scenarios where access control is not necessary, but it is important to be aware of when to enforce it for security reasons.
|
||||
|
||||
### Default Behavior: Access Control Skipped
|
||||
|
||||
By default, **local API operations skip access control**. This allows operations to execute without the system checking if the current user has appropriate permissions. This might be helpful in admin or server-side scripts where the user context is not required to perform the operation.
|
||||
|
||||
#### For example:
|
||||
|
||||
```ts
|
||||
// Access control is this operation would be skipped by default
|
||||
const test = await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'test',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Respecting Access Control
|
||||
|
||||
If you want to respect access control and ensure that the operation is performed only if the user has appropriate permissions, you need to explicitly pass the `user` object and set the `overrideAccess` option to `false`.
|
||||
|
||||
- `overrideAccess: false`: This ensures that access control is **not skipped** and the operation respects the current user's permissions.
|
||||
- `user`: Pass the authenticated user context to the operation. This ensures the system checks whether the user has the right permissions to perform the action.
|
||||
|
||||
```ts
|
||||
const authedCreate = await payload.create({
|
||||
collection: 'users',
|
||||
overrideAccess: false, // This ensures access control will be applied
|
||||
user, // Pass the authenticated user to check permissions
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'test',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
This example will only allow the document to be created if the `user` we passed has the appropriate access control permissions.
|
||||
524
docs/local-api/server-functions.mdx
Normal file
524
docs/local-api/server-functions.mdx
Normal file
@@ -0,0 +1,524 @@
|
||||
---
|
||||
title: Using Local API Operations with Server Functions
|
||||
label: Server Functions
|
||||
order: 30
|
||||
desc: Learn to use Local API operations with Server Functions in Payload to manage server-side logic, data interactions, and custom workflows directly within your CMS.
|
||||
keywords: server functions, local API, Payload, CMS, server-side logic, custom workflows, data management, headless CMS, TypeScript, Node.js, backend
|
||||
---
|
||||
|
||||
In Next.js, **server functions** (previously called **server actions**) are special functions that run exclusively on the server, enabling secure backend logic execution while being callable from the frontend. These functions bridge the gap between client and server, allowing frontend components to perform backend operations without exposing sensitive logic.
|
||||
|
||||
### Why Use Server Functions?
|
||||
|
||||
- **Executing Backend Logic from the Frontend**: The Local API is designed for server environments and cannot be directly accessed from client-side code. Server functions enable frontend components to trigger backend operations securely.
|
||||
- **Security Benefits**: Instead of exposing a full REST or GraphQL API, server functions restrict access to only the necessary operations, reducing potential security risks.
|
||||
- **Performance Optimizations**: Next.js handles server functions efficiently, offering benefits like caching, optimized database queries, and reduced network overhead compared to traditional API calls.
|
||||
- **Simplified Development Workflow**: Rather than setting up full API routes with authentication and authorization checks, server functions allow for lightweight, direct execution of necessary operations.
|
||||
|
||||
### When to Use Server Functions
|
||||
|
||||
Use server functions whenever you need to call Local API operations from the frontend. Since the Local API is only accessible from the backend, server functions act as a secure bridge, eliminating the need to expose additional API endpoints.
|
||||
|
||||
## Examples
|
||||
|
||||
All Local API operations can be used within server functions, allowing you to interact with Payload's backend securely.
|
||||
|
||||
For a full list of available operations, see the [Local API](https://payloadcms.com/docs/local-api/overview) overview.
|
||||
|
||||
In the following examples, we'll cover some common use cases, including:
|
||||
|
||||
- Creating a document
|
||||
- Updating a document
|
||||
- Handling file uploads when creating or updating a document
|
||||
- Authenticating a user
|
||||
|
||||
### Creating a Document
|
||||
|
||||
First, let's create our server function. Here are some key points for this process:
|
||||
|
||||
- Begin by adding `'use server'` at the top of the file.
|
||||
- You can still use utilities such as `getPayload()`.
|
||||
- Once the function structure is in place, call the Local API operation `payload.create()` and pass in the relevant data.
|
||||
- It's good practice to wrap this in a `try...catch` block for error handling.
|
||||
- Finally, make sure to return the created document (don't just run the operation).
|
||||
|
||||
```ts
|
||||
'use server'
|
||||
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
|
||||
export async function createPost(data) {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
try {
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data,
|
||||
})
|
||||
return post
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating post: ${error.message}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, let's look at how to call the `createPost` function we just created from the frontend in a React component when a user clicks a button:
|
||||
|
||||
```ts
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { createPost } from '../server/actions'; // import the server function
|
||||
|
||||
export const PostForm: React.FC = () => {
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>{result}</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
// Call the server function
|
||||
const newPost = await createPost({ title: 'Sample Post' });
|
||||
setResult('Post created: ' + newPost.title);
|
||||
}}
|
||||
>
|
||||
Create Post
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Updating a Document
|
||||
|
||||
The key points from the previous example also apply here.
|
||||
|
||||
To update a document instead of creating one, you would use `payload.update()` with the relevant data and **passing the document ID.**
|
||||
|
||||
Here's how the server function would look:
|
||||
|
||||
```ts
|
||||
'use server'
|
||||
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
|
||||
export async function updatePost(id, data) {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
try {
|
||||
const post = await payload.update({
|
||||
collection: 'posts',
|
||||
id, // the document id is required
|
||||
data,
|
||||
})
|
||||
return post
|
||||
} catch (error) {
|
||||
throw new Error(`Error updating post: ${error.message}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here is how you would call the `updatePost` function from a frontend React component:
|
||||
|
||||
```ts
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { updatePost } from '../server/actions'; // import the server function
|
||||
|
||||
export const UpdatePostForm: React.FC = () => {
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>{result}</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
// Call the server function to update the post
|
||||
const updatedPost = await updatePost('your-post-id-123', { title: 'Updated Post' });
|
||||
setResult('Post updated: ' + updatedPost.title);
|
||||
}}
|
||||
>
|
||||
Update Post
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Authenticating a User
|
||||
|
||||
In this example, we will check if a user is authenticated using Payload's authentication system. Here's how it works:
|
||||
|
||||
- First, we use the headers function from `next/headers` to retrieve the request headers.
|
||||
- Next, we pass these headers to `payload.auth()` to fetch the user's authentication details.
|
||||
- If the user is authenticated, their information is returned. If not, handle the unauthenticated case accordingly.
|
||||
|
||||
Here's the server function to authenticate a user:
|
||||
|
||||
```ts
|
||||
'use server'
|
||||
|
||||
import { headers as getHeaders } from 'next/headers'
|
||||
import config from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
export const authenticateUser = async () => {
|
||||
const payload = await getPayload({ config })
|
||||
const headers = await getHeaders()
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
if (user) {
|
||||
return { hello: user.email }
|
||||
}
|
||||
|
||||
return { hello: 'Not authenticated' }
|
||||
}
|
||||
```
|
||||
|
||||
Here's a basic example of how to call the authentication server function from the frontend to test it:
|
||||
|
||||
```ts
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { authenticateUser } from '../server/actions'; // Import the server function
|
||||
|
||||
export const AuthComponent: React.FC = () => {
|
||||
const [userInfo, setUserInfo] = useState<string>('');
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<p>{userInfo}</p>
|
||||
|
||||
<button
|
||||
onClick={async () => {
|
||||
// Call the server function to authenticate the user
|
||||
const result = await authenticateUser();
|
||||
setUserInfo(result.hello);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Check Authentication
|
||||
</button>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Creating a Document with File Upload
|
||||
|
||||
This example demonstrates how to write a server function that creates a document with a file upload. Here are the key steps:
|
||||
|
||||
- Pass two arguments: **data** for the document content and **upload** for the file
|
||||
- Merge the upload file into the document data as the media field
|
||||
- Use `payload.create()` to create a new post document with both the document data and file
|
||||
|
||||
```ts
|
||||
'use server'
|
||||
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
|
||||
export async function createPostWithUpload(data, upload) {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
try {
|
||||
// Prepare the data with the file
|
||||
const postData = {
|
||||
...data,
|
||||
media: upload,
|
||||
}
|
||||
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: postData,
|
||||
})
|
||||
|
||||
return post
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating post: ${error.message}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here is how you would use the server function we just created in a frontend component to allow users to submit a post along with a file upload:
|
||||
|
||||
- The user enters the post title and selects a file to upload.
|
||||
- When the form is submitted, the `handleSubmit` function checks if a file has been chosen.
|
||||
- If a file is selected, it passes both the title and the file to the `createPostWithFile` server function.
|
||||
- And you are done!
|
||||
|
||||
```ts
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { createPostWithUpload } from '../server/actions';
|
||||
|
||||
export const PostForm: React.FC = () => {
|
||||
const [title, setTitle] = useState<string>('');
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
setFile(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!file) {
|
||||
setResult('Please upload a file.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Call the server function to create the post with the file
|
||||
const newPost = await createPostWithUpload({ title }, file);
|
||||
setResult('Post created with file: ' + newPost.title);
|
||||
} catch (error) {
|
||||
setResult('Error: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Post Title"
|
||||
/>
|
||||
<input type="file" onChange={handleFileChange} />
|
||||
<button type="submit">Create Post</button>
|
||||
<p>{result}</p>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Reusable Payload Server Functions
|
||||
|
||||
Managing authentication with the Local API can be tricky as you have to handle cookies and tokens yourself, and there aren't built-in logout or refresh functions since these only modify cookies. To make this easier, we provide `login`, `logout`, and `refresh` as ready-to-use server functions. They take care of the underlying complexity so you don't have to.
|
||||
|
||||
### Login
|
||||
|
||||
Logs in a user by verifying credentials and setting the authentication cookie. This function allows login via username or email, depending on the collection auth configuration.
|
||||
|
||||
#### Importing the `login` function
|
||||
|
||||
```ts
|
||||
import { login } from '@payloadcms/next/auth'
|
||||
```
|
||||
|
||||
The login function needs your Payload config, which cannot be imported in a client component. To work around this, create a simple server function like the one below, and call it from your client.
|
||||
|
||||
```ts
|
||||
'use server'
|
||||
|
||||
import { login } from '@payloadcms/next/auth'
|
||||
import config from '@payload-config'
|
||||
|
||||
export async function loginAction({
|
||||
email,
|
||||
password,
|
||||
}: {
|
||||
email: string
|
||||
password: string
|
||||
}) {
|
||||
try {
|
||||
const result = await login({
|
||||
collection: 'users',
|
||||
config,
|
||||
email,
|
||||
password,
|
||||
})
|
||||
return result
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Login failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Login from the React Client Component
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { loginAction } from '../loginAction'
|
||||
|
||||
export default function LoginForm() {
|
||||
const [email, setEmail] = useState<string>('')
|
||||
const [password, setPassword] = useState<string>('')
|
||||
|
||||
return (
|
||||
<form onSubmit={() => loginAction({ email, password })}>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setEmail(e.target.value)
|
||||
}
|
||||
type="email"
|
||||
value={email}
|
||||
/>
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setPassword(e.target.value)
|
||||
}
|
||||
type="password"
|
||||
value={password}
|
||||
/>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Logout
|
||||
|
||||
Logs out the current user by clearing the authentication cookie.
|
||||
|
||||
#### Importing the `logout` function
|
||||
|
||||
```ts
|
||||
import { logout } from '@payloadcms/next/auth'
|
||||
```
|
||||
|
||||
Similar to the login function, you now need to pass your Payload config to this function and this cannot be done in a client component. Use a helper server function as shown below.
|
||||
|
||||
```ts
|
||||
'use server'
|
||||
|
||||
import { logout } from '@payloadcms/next/auth'
|
||||
import config from '@payload-config'
|
||||
|
||||
export async function logoutAction() {
|
||||
try {
|
||||
return await logout({ config })
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Logout failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Logout from the React Client Component
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { logoutAction } from '../logoutAction'
|
||||
|
||||
export default function LogoutButton() {
|
||||
return <button onClick={() => logoutFunction()}>Logout</button>
|
||||
}
|
||||
```
|
||||
|
||||
### Refresh
|
||||
|
||||
Refreshes the authentication token for the logged-in user.
|
||||
|
||||
#### Importing the `refresh` function
|
||||
|
||||
```ts
|
||||
import { refresh } from '@payloadcms/next/auth'
|
||||
```
|
||||
|
||||
As with login and logout, you need to pass your Payload config to this function. Create a helper server function like the one below. Passing the config directly to the client is not possible and will throw errors.
|
||||
|
||||
```ts
|
||||
'use server'
|
||||
|
||||
import { refresh } from '@payloadcms/next/auth'
|
||||
import config from '@payload-config'
|
||||
|
||||
export async function refreshAction() {
|
||||
try {
|
||||
return await refresh({
|
||||
collection: 'users', // pass your collection slug
|
||||
config,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Using Refresh from the React Client Component
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { refreshAction } from '../actions/refreshAction'
|
||||
|
||||
export default function RefreshTokenButton() {
|
||||
return <button onClick={() => refreshFunction()}>Refresh</button>
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling in Server Functions
|
||||
|
||||
When using server functions, proper error handling is essential to prevent unhandled exceptions and provide meaningful feedback to the frontend.
|
||||
|
||||
### Best Practices#error-handling-best-practices
|
||||
|
||||
- Wrap Local API calls in **try/catch blocks** to catch potential errors.
|
||||
- **Log errors** on the server for debugging purposes.
|
||||
- Return structured **error responses** instead of exposing raw errors to the frontend.
|
||||
|
||||
Example of good error handling:
|
||||
|
||||
```ts
|
||||
export async function createPost(data) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
return await payload.create({ collection: 'posts', data })
|
||||
} catch (error) {
|
||||
console.error('Error creating post:', error)
|
||||
return { error: 'Failed to create post' }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
Using server functions helps prevent direct exposure of Local API operations to the frontend, but additional security best practices should be followed:
|
||||
|
||||
### Best Practices#security-best-practices
|
||||
|
||||
- **Restrict access**: Ensure that sensitive actions (like user management) are only callable by authorized users.
|
||||
- **Avoid passing sensitive data**: Do not return sensitive information such as user data, passwords, etc.
|
||||
- **Use authentication & authorization**: Check user roles before performing actions.
|
||||
|
||||
Example of restricting access based on user role:
|
||||
|
||||
```ts
|
||||
export async function deletePost(postId, user) {
|
||||
if (!user || user.role !== 'admin') {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
return await payload.delete({ collection: 'posts', id: postId })
|
||||
}
|
||||
```
|
||||
@@ -55,18 +55,9 @@ Because _**you**_ are in complete control of who can do what with your data, you
|
||||
wield that power responsibly before deploying to Production.
|
||||
|
||||
<Banner type="error">
|
||||
**
|
||||
By default, all Access Control functions require that a user is successfully logged in to
|
||||
Payload to create, read, update, or delete data.
|
||||
**
|
||||
But, if you allow public user registration, for example, you will want to make sure that your
|
||||
access control functions are more strict - permitting
|
||||
**By default, all Access Control functions require that a user is successfully logged in to Payload to create, read, update, or delete data.**
|
||||
|
||||
**
|
||||
only appropriate users
|
||||
**
|
||||
|
||||
to perform appropriate actions.
|
||||
But, if you allow public user registration, for example, you will want to make sure that your access control functions are more strict - permitting **only appropriate users** to perform appropriate actions.
|
||||
|
||||
</Banner>
|
||||
|
||||
|
||||
171
docs/query-presets/overview.mdx
Normal file
171
docs/query-presets/overview.mdx
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
title: Query Presets
|
||||
label: Overview
|
||||
order: 10
|
||||
desc: Query Presets allow you to save and share filters, columns, and sort orders for your collections.
|
||||
keywords:
|
||||
---
|
||||
|
||||
Query Presets allow you to save and share filters, columns, and sort orders for your [Collections](../configuration/collections). This is useful for reusing common or complex filtering patterns and/or sharing them across your team.
|
||||
|
||||
Each Query Preset is saved as a new record in the database under the `payload-query-presets` collection. This allows for an endless number of preset configurations, where the users of your app define the presets that are most useful to them, rather than being hard coded into the Payload Config.
|
||||
|
||||
Within the [Admin Panel](../admin/overview), Query Presets are applied to the List View. When enabled, new controls are displayed for users to manage presets. Once saved, these presets can be loaded up at any time and optionally shared with others.
|
||||
|
||||
To enable Query Presets on a Collection, use the `enableQueryPresets` property in your [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollection: CollectionConfig = {
|
||||
// ...
|
||||
// highlight-start
|
||||
enableQueryPresets: true,
|
||||
// highlight-end
|
||||
}
|
||||
```
|
||||
|
||||
## Config Options
|
||||
|
||||
While not required, you may want to customize the behavior of Query Presets to suit your needs, such as add custom labels or access control rules.
|
||||
|
||||
Settings for Query Presets are managed on the `queryPresets` property at the root of your [Payload Config](../configuration/overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
// highlight-start
|
||||
queryPresets: {
|
||||
// ...
|
||||
},
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
The following options are available for Query Presets:
|
||||
|
||||
| Option | Description |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `access` | Used to define custom collection-level access control that applies to all presets. [More details](#access-control). |
|
||||
| `constraints` | Used to define custom document-level access control that apply to individual presets. [More details](#document-access-control). |
|
||||
| `labels` | Custom labels to use for the Query Presets collection. |
|
||||
|
||||
## Access Control
|
||||
|
||||
Query Presets are subject to the same [Access Control](../access-control/overview) as the rest of Payload. This means you can use the same patterns you are already familiar with to control who can read, update, and delete presets.
|
||||
|
||||
Access Control for Query Presets can be customized in two ways:
|
||||
|
||||
1. [Collection Access Control](#collection-access-control): Applies to all presets. These rules are not controllable by the user and are statically defined in the config.
|
||||
2. [Document Access Control](#document-access-control): Applies to each individual preset. These rules are controllable by the user and are saved to the document.
|
||||
|
||||
### Collection Access Control
|
||||
|
||||
Collection-level access control applies to _all_ presets within the Query Presets collection. Users cannot control these rules, they are written statically in your config.
|
||||
|
||||
To add Collection Access Control, use the `queryPresets.access` property in your [Payload Config](../configuration/overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
queryPresets: {
|
||||
// ...
|
||||
// highlight-start
|
||||
access: {
|
||||
read: ({ req: { user } }) =>
|
||||
user ? user?.roles?.some((role) => role === 'admin') : false,
|
||||
update: ({ req: { user } }) =>
|
||||
user ? user?.roles?.some((role) => role === 'admin') : false,
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
This example restricts all Query Presets to users with the role of `admin`.
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:** Custom access control will override the defaults on this collection,
|
||||
including the requirement for a user to be authenticated. Be sure to include
|
||||
any necessary checks in your custom rules unless you intend on making these
|
||||
publicly accessible.
|
||||
</Banner>
|
||||
|
||||
### Document Access Control
|
||||
|
||||
You can also define access control rules that apply to each specific preset. Users have the ability to define and modify these rules on the fly as they manage presets. These are saved dynamically in the database on each document.
|
||||
|
||||
When a user manages a preset, document-level access control options will be available to them in the Admin Panel for each operation.
|
||||
|
||||
By default, Payload provides a set of sensible defaults for all Query Presets, but you can customize these rules to suit your needs:
|
||||
|
||||
- **Only Me**: Only the user who created the preset can read, update, and delete it.
|
||||
- **Everyone**: All users can read, update, and delete the preset.
|
||||
- **Specific Users**: Only select users can read, update, and delete the preset.
|
||||
|
||||
#### Custom Access Control
|
||||
|
||||
You can augment the default access control rules with your own custom rules. This can be useful for creating more complex access control patterns that the defaults don't provide, such as for RBAC.
|
||||
|
||||
Adding custom access control rules requires:
|
||||
|
||||
1. A label to display in the dropdown
|
||||
2. A set of fields to conditionally render when that option is selected
|
||||
3. A function that returns the access control rules for that option
|
||||
|
||||
To do this, use the `queryPresets.constraints` property in your [Payload Config](../configuration/payload-config).
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
queryPresets: {
|
||||
// ...
|
||||
// highlight-start
|
||||
constraints: {
|
||||
read: {
|
||||
label: 'Specific Roles',
|
||||
value: 'specificRoles',
|
||||
fields: [
|
||||
{
|
||||
name: 'roles',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: [
|
||||
{ label: 'Admin', value: 'admin' },
|
||||
{ label: 'User', value: 'user' },
|
||||
],
|
||||
},
|
||||
],
|
||||
access: ({ req: { user } }) => ({
|
||||
'access.read.roles': {
|
||||
in: [user?.roles],
|
||||
},
|
||||
}),
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
In this example, we've added a new option called `Specific Roles` that allows users to select from a list of roles. When this option is selected, the user will be prompted to select one or more roles from a list of options. The access control rule for this option is that the user operating on the preset must have one of the selected roles.
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:** Payload places your custom fields into the `access[operation]` field
|
||||
group, so your rules will need to reflect this.
|
||||
</Banner>
|
||||
|
||||
The following options are available for each constraint:
|
||||
|
||||
| Option | Description |
|
||||
| -------- | ------------------------------------------------------------------------ |
|
||||
| `label` | The label to display in the dropdown for this constraint. |
|
||||
| `value` | The value to store in the database when this constraint is selected. |
|
||||
| `fields` | An array of fields to render when this constraint is selected. |
|
||||
| `access` | A function that determines the access control rules for this constraint. |
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
// Your richtext data here
|
||||
const data: SerializedEditorState = {}
|
||||
|
||||
const html = convertLexicalToMarkdown({
|
||||
const markdown = convertLexicalToMarkdown({
|
||||
data,
|
||||
editorConfig: await editorConfigFactory.default({
|
||||
config, // <= make sure you have access to your Payload Config
|
||||
@@ -101,7 +101,7 @@ import {
|
||||
editorConfigFactory,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const html = convertMarkdownToLexical({
|
||||
const lexicalJSON = convertMarkdownToLexical({
|
||||
editorConfig: await editorConfigFactory.default({
|
||||
config, // <= make sure you have access to your Payload Config
|
||||
}),
|
||||
|
||||
@@ -223,7 +223,7 @@ This allows you to add i18n translations scoped to your feature. This specific e
|
||||
|
||||
### Markdown Transformers#server-feature-markdown-transformers
|
||||
|
||||
The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](/docs/rich-text/converters#markdown-lexical).
|
||||
The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](/docs/rich-text/converting-markdown).
|
||||
|
||||
```ts
|
||||
import { createServerFeature } from '@payloadcms/richtext-lexical'
|
||||
@@ -409,7 +409,7 @@ Explore the APIs available through ClientFeature to add the specific functionali
|
||||
|
||||
### Adding a client feature to the server feature
|
||||
|
||||
Inside of your server feature, you can provide an [import path](/docs/admin/custom-components/overview#component-paths) to the client feature like this:
|
||||
Inside of your server feature, you can provide an [import path](/docs/custom-components/overview#component-paths) to the client feature like this:
|
||||
|
||||
```ts
|
||||
import { createServerFeature } from '@payloadcms/richtext-lexical'
|
||||
|
||||
@@ -334,12 +334,28 @@ To upload a file, use your collection's [`create`](/docs/rest-api/overview#colle
|
||||
|
||||
Send your request as a `multipart/form-data` request, using [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) if possible.
|
||||
|
||||
<Banner type="info">
|
||||
**Note:** To include any additional fields (like `title`, `alt`, etc.), append
|
||||
a `_payload` field containing a JSON-stringified object of the required
|
||||
values. These values must match the schema of your upload-enabled collection.
|
||||
</Banner>
|
||||
|
||||
```ts
|
||||
const fileInput = document.querySelector('#your-file-input')
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('file', fileInput.files[0])
|
||||
|
||||
// Replace with the fields defined in your upload-enabled collection.
|
||||
// The example below includes an optional field like 'title'.
|
||||
formData.append(
|
||||
'_payload',
|
||||
JSON.stringify({
|
||||
title: 'Example Title',
|
||||
description: 'An optional description for the file',
|
||||
}),
|
||||
)
|
||||
|
||||
fetch('api/:upload-slug', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
|
||||
@@ -22,6 +22,7 @@ Collections and Globals both support the same options for configuring autosave.
|
||||
| Drafts Autosave Options | Description |
|
||||
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `interval` | Define an `interval` in milliseconds to automatically save progress while documents are edited. Document updates are "debounced" at this interval. Defaults to `800`. |
|
||||
| `showSaveDraftButton` | Set this to `true` to show the "Save as draft" button even while autosave is enabled. Defaults to `false`. |
|
||||
|
||||
**Example config with versions, drafts, and autosave enabled:**
|
||||
|
||||
@@ -50,9 +51,13 @@ export const Pages: CollectionConfig = {
|
||||
drafts: {
|
||||
autosave: true,
|
||||
|
||||
// Alternatively, you can specify an `interval`:
|
||||
// Alternatively, you can specify an object to customize autosave:
|
||||
// autosave: {
|
||||
// Define how often the document should be autosaved (in milliseconds)
|
||||
// interval: 1500,
|
||||
//
|
||||
// Show the "Save as draft" button even while autosave is enabled
|
||||
// showSaveDraftButton: true,
|
||||
// },
|
||||
},
|
||||
},
|
||||
|
||||
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.30.0",
|
||||
"version": "3.35.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -87,6 +87,8 @@
|
||||
"runts": "cross-env NODE_OPTIONS=--no-deprecation node --no-deprecation --import @swc-node/register/esm-register",
|
||||
"script:build-template-with-local-pkgs": "pnpm --filter scripts build-template-with-local-pkgs",
|
||||
"script:gen-templates": "pnpm --filter scripts gen-templates",
|
||||
"script:gen-templates:build": "pnpm --filter scripts gen-templates --build",
|
||||
"script:license-check": "pnpm --filter scripts license-check",
|
||||
"script:list-published": "pnpm --filter releaser list-published",
|
||||
"script:pack": "pnpm --filter scripts pack-all-to-dest",
|
||||
"pretest": "pnpm build",
|
||||
@@ -118,7 +120,7 @@
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@libsql/client": "0.14.0",
|
||||
"@next/bundle-analyzer": "15.2.3",
|
||||
"@next/bundle-analyzer": "15.3.0",
|
||||
"@payloadcms/db-postgres": "workspace:*",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/eslint-plugin": "workspace:*",
|
||||
@@ -133,8 +135,8 @@
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/minimist": "1.2.5",
|
||||
"@types/node": "22.5.4",
|
||||
"@types/react": "19.0.12",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@types/shelljs": "0.8.15",
|
||||
"chalk": "^4.1.2",
|
||||
"comment-json": "^4.2.3",
|
||||
@@ -154,14 +156,14 @@
|
||||
"lint-staged": "15.2.7",
|
||||
"minimist": "1.2.8",
|
||||
"mongodb-memory-server": "^10",
|
||||
"next": "15.2.3",
|
||||
"next": "15.3.0",
|
||||
"open": "^10.1.0",
|
||||
"p-limit": "^5.0.0",
|
||||
"playwright": "1.50.0",
|
||||
"playwright-core": "1.50.0",
|
||||
"prettier": "3.5.3",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"rimraf": "6.0.1",
|
||||
"sharp": "0.32.6",
|
||||
"shelljs": "0.8.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/admin-bar",
|
||||
"version": "3.30.0",
|
||||
"version": "3.35.1",
|
||||
"description": "An admin bar for React apps using Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -42,8 +42,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/react": "19.0.12",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
/* TODO: remove the following lines */
|
||||
"strict": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
},
|
||||
"references": [{ "path": "../payload" }]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.30.0",
|
||||
"version": "3.35.1",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.30.0",
|
||||
"version": "3.35.1",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -17,7 +17,6 @@ import type {
|
||||
TypeWithVersion,
|
||||
UpdateGlobalArgs,
|
||||
UpdateGlobalVersionArgs,
|
||||
UpdateManyArgs,
|
||||
UpdateOneArgs,
|
||||
UpdateVersionArgs,
|
||||
} from 'payload'
|
||||
@@ -55,6 +54,7 @@ import { commitTransaction } from './transactions/commitTransaction.js'
|
||||
import { rollbackTransaction } from './transactions/rollbackTransaction.js'
|
||||
import { updateGlobal } from './updateGlobal.js'
|
||||
import { updateGlobalVersion } from './updateGlobalVersion.js'
|
||||
import { updateJobs } from './updateJobs.js'
|
||||
import { updateMany } from './updateMany.js'
|
||||
import { updateOne } from './updateOne.js'
|
||||
import { updateVersion } from './updateVersion.js'
|
||||
@@ -227,6 +227,7 @@ export function mongooseAdapter({
|
||||
mongoMemoryServer,
|
||||
sessions: {},
|
||||
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
|
||||
updateJobs,
|
||||
updateMany,
|
||||
url,
|
||||
versions: {},
|
||||
@@ -272,6 +273,7 @@ export function mongooseAdapter({
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'mongoose',
|
||||
allowIDOnCreate,
|
||||
defaultIDType: 'text',
|
||||
init: adapter,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { FilterQuery } from 'mongoose'
|
||||
import type { FlattenedField, Operator, PathToQuery, Payload } from 'payload'
|
||||
|
||||
import { Types } from 'mongoose'
|
||||
import { APIError, getLocalizedPaths } from 'payload'
|
||||
import { APIError, getFieldByPath, getLocalizedPaths } from 'payload'
|
||||
import { validOperatorSet } from 'payload/shared'
|
||||
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
@@ -138,7 +138,7 @@ export async function buildSearchParam({
|
||||
throw new APIError(`Collection with the slug ${collectionSlug} was not found.`)
|
||||
}
|
||||
|
||||
const { Model: SubModel } = getCollection({
|
||||
const { collectionConfig, Model: SubModel } = getCollection({
|
||||
adapter: payload.db as MongooseAdapter,
|
||||
collectionSlug,
|
||||
})
|
||||
@@ -154,22 +154,72 @@ export async function buildSearchParam({
|
||||
},
|
||||
})
|
||||
|
||||
const result = await SubModel.find(subQuery, subQueryOptions)
|
||||
const field = paths[0].field
|
||||
|
||||
const select: Record<string, boolean> = {
|
||||
_id: true,
|
||||
}
|
||||
|
||||
let joinPath: null | string = null
|
||||
|
||||
if (field.type === 'join') {
|
||||
const relationshipField = getFieldByPath({
|
||||
fields: collectionConfig.flattenedFields,
|
||||
path: field.on,
|
||||
})
|
||||
if (!relationshipField) {
|
||||
throw new APIError('Relationship field was not found')
|
||||
}
|
||||
|
||||
let path = relationshipField.localizedPath
|
||||
if (relationshipField.pathHasLocalized && payload.config.localization) {
|
||||
path = path.replace('<locale>', locale || payload.config.localization.defaultLocale)
|
||||
}
|
||||
select[path] = true
|
||||
|
||||
joinPath = path
|
||||
}
|
||||
|
||||
if (joinPath) {
|
||||
select[joinPath] = true
|
||||
}
|
||||
|
||||
const result = await SubModel.find(subQuery).lean().limit(50).select(select)
|
||||
|
||||
const $in: unknown[] = []
|
||||
|
||||
result.forEach((doc) => {
|
||||
const stringID = doc._id.toString()
|
||||
$in.push(stringID)
|
||||
result.forEach((doc: any) => {
|
||||
if (joinPath) {
|
||||
let ref = doc
|
||||
|
||||
if (Types.ObjectId.isValid(stringID)) {
|
||||
$in.push(doc._id)
|
||||
for (const segment of joinPath.split('.')) {
|
||||
if (typeof ref === 'object' && ref) {
|
||||
ref = ref[segment]
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(ref)) {
|
||||
for (const item of ref) {
|
||||
if (item instanceof Types.ObjectId) {
|
||||
$in.push(item)
|
||||
}
|
||||
}
|
||||
} else if (ref instanceof Types.ObjectId) {
|
||||
$in.push(ref)
|
||||
}
|
||||
} else {
|
||||
const stringID = doc._id.toString()
|
||||
$in.push(stringID)
|
||||
|
||||
if (Types.ObjectId.isValid(stringID)) {
|
||||
$in.push(doc._id)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (pathsToQuery.length === 1) {
|
||||
return {
|
||||
path,
|
||||
path: joinPath ? '_id' : path,
|
||||
value: { $in },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,19 @@ export async function parseParams({
|
||||
[searchParam.path]: searchParam.value,
|
||||
})
|
||||
} else {
|
||||
result[searchParam.path] = searchParam.value
|
||||
if (result[searchParam.path]) {
|
||||
if (!result.$and) {
|
||||
result.$and = []
|
||||
}
|
||||
|
||||
result.$and.push({ [searchParam.path]: result[searchParam.path] })
|
||||
result.$and.push({
|
||||
[searchParam.path]: searchParam.value,
|
||||
})
|
||||
delete result[searchParam.path]
|
||||
} else {
|
||||
result[searchParam.path] = searchParam.value
|
||||
}
|
||||
}
|
||||
} else if (typeof searchParam?.value === 'object') {
|
||||
result = deepMergeWithCombinedArrays(result, searchParam.value ?? {}, {
|
||||
|
||||
102
packages/db-mongodb/src/updateJobs.ts
Normal file
102
packages/db-mongodb/src/updateJobs.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { MongooseUpdateQueryOptions } from 'mongoose'
|
||||
import type { BaseJob, UpdateJobs, Where } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { handleError } from './utilities/handleError.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const updateJobs: UpdateJobs = async function updateMany(
|
||||
this: MongooseAdapter,
|
||||
{ id, data, limit, req, returning, sort: sortArg, where: whereArg },
|
||||
) {
|
||||
if (!(data?.log as object[])?.length) {
|
||||
delete data.log
|
||||
}
|
||||
const where = id ? { id: { equals: id } } : (whereArg as Where)
|
||||
|
||||
const { collectionConfig, Model } = getCollection({
|
||||
adapter: this,
|
||||
collectionSlug: 'payload-jobs',
|
||||
})
|
||||
|
||||
const sort: Record<string, unknown> | undefined = buildSortParam({
|
||||
adapter: this,
|
||||
config: this.payload.config,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
sort: sortArg || collectionConfig.defaultSort,
|
||||
timestamps: true,
|
||||
})
|
||||
|
||||
const options: MongooseUpdateQueryOptions = {
|
||||
lean: true,
|
||||
new: true,
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
let query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collectionConfig.slug,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
where,
|
||||
})
|
||||
|
||||
transform({ adapter: this, data, fields: collectionConfig.fields, operation: 'write' })
|
||||
|
||||
let result: BaseJob[] = []
|
||||
|
||||
try {
|
||||
if (id) {
|
||||
if (returning === false) {
|
||||
await Model.updateOne(query, data, options)
|
||||
return null
|
||||
} else {
|
||||
const doc = await Model.findOneAndUpdate(query, data, options)
|
||||
result = doc ? [doc] : []
|
||||
}
|
||||
} else {
|
||||
if (typeof limit === 'number' && limit > 0) {
|
||||
const documentsToUpdate = await Model.find(
|
||||
query,
|
||||
{},
|
||||
{ ...options, limit, projection: { _id: 1 }, sort },
|
||||
)
|
||||
if (documentsToUpdate.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
query = { _id: { $in: documentsToUpdate.map((doc) => doc._id) } }
|
||||
}
|
||||
|
||||
await Model.updateMany(query, data, options)
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
result = await Model.find(
|
||||
query,
|
||||
{},
|
||||
{
|
||||
...options,
|
||||
sort,
|
||||
},
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
handleError({ collection: collectionConfig.slug, error, req })
|
||||
}
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result,
|
||||
fields: collectionConfig.fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.30.0",
|
||||
"version": "3.35.1",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -25,9 +25,9 @@
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./src/types.ts",
|
||||
"types": "./src/types.ts",
|
||||
"default": "./src/types.ts"
|
||||
"import": "./src/exports/types-deprecated.ts",
|
||||
"types": "./src/exports/types-deprecated.ts",
|
||||
"default": "./src/exports/types-deprecated.ts"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./src/exports/migration-utils.ts",
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/types.ts",
|
||||
"types": "./src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"mock.js"
|
||||
@@ -102,9 +102,9 @@
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./dist/types.js",
|
||||
"types": "./dist/types.d.ts",
|
||||
"default": "./dist/types.js"
|
||||
"import": "./dist/exports/types-deprecated.js",
|
||||
"types": "./dist/exports/types-deprecated.d.ts",
|
||||
"default": "./dist/exports/types-deprecated.js"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./dist/exports/migration-utils.js",
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { Connect, Migration, Payload } from 'payload'
|
||||
|
||||
import { pushDevSchema } from '@payloadcms/drizzle'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import pg from 'pg'
|
||||
|
||||
import type { PostgresAdapter } from './types.js'
|
||||
|
||||
@@ -61,7 +60,7 @@ export const connect: Connect = async function connect(
|
||||
|
||||
try {
|
||||
if (!this.pool) {
|
||||
this.pool = new pg.Pool(this.poolOptions)
|
||||
this.pool = new this.pg.Pool(this.poolOptions)
|
||||
await connectWithReconnect({ adapter: this, payload: this.payload })
|
||||
}
|
||||
|
||||
|
||||
20
packages/db-postgres/src/exports/types-deprecated.ts
Normal file
20
packages/db-postgres/src/exports/types-deprecated.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type {
|
||||
Args as _Args,
|
||||
GeneratedDatabaseSchema as _GeneratedDatabaseSchema,
|
||||
PostgresAdapter as _PostgresAdapter,
|
||||
} from '../types.js'
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-postgres` instead
|
||||
*/
|
||||
export type Args = _Args
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-postgres` instead
|
||||
*/
|
||||
export type GeneratedDatabaseSchema = _GeneratedDatabaseSchema
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-postgres` instead
|
||||
*/
|
||||
export type PostgresAdapter = _PostgresAdapter
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateJobs,
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
@@ -53,6 +54,7 @@ import {
|
||||
} from '@payloadcms/drizzle/postgres'
|
||||
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createDatabaseAdapter, defaultBeginTransaction } from 'payload'
|
||||
import pgDependency from 'pg'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { Args, PostgresAdapter } from './types.js'
|
||||
@@ -129,6 +131,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
localesSuffix: args.localesSuffix || '_locales',
|
||||
logger: args.logger,
|
||||
operators: operatorMap,
|
||||
pg: args.pg || pgDependency,
|
||||
pgSchema: adapterSchema,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
pool: undefined,
|
||||
@@ -172,6 +175,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
find,
|
||||
findGlobal,
|
||||
findGlobalVersions,
|
||||
updateJobs,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
findOne,
|
||||
findVersions,
|
||||
@@ -206,12 +210,18 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'postgres',
|
||||
allowIDOnCreate,
|
||||
defaultIDType: payloadIDType,
|
||||
init: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
export type {
|
||||
Args as PostgresAdapterArgs,
|
||||
GeneratedDatabaseSchema,
|
||||
PostgresAdapter,
|
||||
} from './types.js'
|
||||
export type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/drizzle/postgres'
|
||||
export { geometryColumn } from '@payloadcms/drizzle/postgres'
|
||||
export { sql } from 'drizzle-orm'
|
||||
|
||||
@@ -12,6 +12,8 @@ import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
||||
import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-core'
|
||||
import type { Pool, PoolConfig } from 'pg'
|
||||
|
||||
type PgDependency = typeof import('pg')
|
||||
|
||||
export type Args = {
|
||||
/**
|
||||
* Transform the schema after it's built.
|
||||
@@ -45,6 +47,7 @@ export type Args = {
|
||||
localesSuffix?: string
|
||||
logger?: DrizzleConfig['logger']
|
||||
migrationDir?: string
|
||||
pg?: PgDependency
|
||||
pool: PoolConfig
|
||||
prodMigrations?: {
|
||||
down: (args: MigrateDownArgs) => Promise<void>
|
||||
@@ -74,6 +77,7 @@ type ResolveSchemaType<T> = 'schema' extends keyof T
|
||||
type Drizzle = NodePgDatabase<ResolveSchemaType<GeneratedDatabaseSchema>>
|
||||
export type PostgresAdapter = {
|
||||
drizzle: Drizzle
|
||||
pg: PgDependency
|
||||
pool: Pool
|
||||
poolOptions: PoolConfig
|
||||
} & BasePostgresAdapter
|
||||
@@ -98,6 +102,8 @@ declare module 'payload' {
|
||||
initializing: Promise<void>
|
||||
localesSuffix?: string
|
||||
logger: DrizzleConfig['logger']
|
||||
/** Optionally inject your own node-postgres. This is required if you wish to instrument the driver with @payloadcms/plugin-sentry. */
|
||||
pg?: PgDependency
|
||||
pgSchema?: { table: PgTableFn } | PgSchema
|
||||
pool: Pool
|
||||
poolOptions: Args['pool']
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.30.0",
|
||||
"version": "3.35.1",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -25,9 +25,9 @@
|
||||
"types": "./src/index.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./src/types.ts",
|
||||
"require": "./src/types.ts",
|
||||
"types": "./src/types.ts"
|
||||
"import": "./src/exports/types-deprecated.ts",
|
||||
"require": "./src/exports/types-deprecated.ts",
|
||||
"types": "./src/exports/types-deprecated.ts"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./src/exports/migration-utils.ts",
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/types.ts",
|
||||
"types": "./src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"mock.js"
|
||||
@@ -85,6 +85,7 @@
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/pg": "8.10.2",
|
||||
"@types/to-snake-case": "1.0.0",
|
||||
"@types/uuid": "10.0.0",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -98,9 +99,9 @@
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./dist/types.js",
|
||||
"require": "./dist/types.js",
|
||||
"types": "./dist/types.d.ts"
|
||||
"import": "./dist/exports/types-deprecated.js",
|
||||
"require": "./dist/exports/types-deprecated.js",
|
||||
"types": "./dist/exports/types-deprecated.d.ts"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./dist/exports/migration-utils.js",
|
||||
|
||||
@@ -23,6 +23,9 @@ export const columnToCodeConverter: ColumnToCodeConverter = ({
|
||||
case 'enum': {
|
||||
let options: string[]
|
||||
if ('locale' in column) {
|
||||
if (!locales?.length) {
|
||||
throw new Error('Locales must be defined for locale columns')
|
||||
}
|
||||
options = locales
|
||||
} else {
|
||||
options = column.options
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { Connect } from 'payload'
|
||||
import type { Connect, Migration } from 'payload'
|
||||
|
||||
import { createClient } from '@libsql/client'
|
||||
import { pushDevSchema } from '@payloadcms/drizzle'
|
||||
@@ -36,7 +36,8 @@ export const connect: Connect = async function connect(
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.payload.logger.error({ err, msg: `Error: cannot connect to SQLite: ${err.message}` })
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
this.payload.logger.error({ err, msg: `Error: cannot connect to SQLite: ${message}` })
|
||||
if (typeof this.rejectInitializing === 'function') {
|
||||
this.rejectInitializing()
|
||||
}
|
||||
@@ -57,6 +58,6 @@ export const connect: Connect = async function connect(
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && this.prodMigrations) {
|
||||
await this.migrate({ migrations: this.prodMigrations })
|
||||
await this.migrate({ migrations: this.prodMigrations as Migration[] })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ChainedMethods } from '@payloadcms/drizzle/types'
|
||||
import type { SQLiteSelect } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
import { chainMethods } from '@payloadcms/drizzle'
|
||||
import { count, sql } from 'drizzle-orm'
|
||||
|
||||
import type { CountDistinct, SQLiteAdapter } from './types.js'
|
||||
@@ -17,33 +16,28 @@ export const countDistinct: CountDistinct = async function countDistinct(
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where)
|
||||
return Number(countResult[0].count)
|
||||
return Number(countResult[0]?.count)
|
||||
}
|
||||
|
||||
const chainedMethods: ChainedMethods = []
|
||||
let query: SQLiteSelect = db
|
||||
.select({
|
||||
count: sql`COUNT(1) OVER()`,
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where)
|
||||
.groupBy(this.tables[tableName].id)
|
||||
.limit(1)
|
||||
.$dynamic()
|
||||
|
||||
joins.forEach(({ condition, table }) => {
|
||||
chainedMethods.push({
|
||||
args: [table, condition],
|
||||
method: 'leftJoin',
|
||||
})
|
||||
query = query.leftJoin(table, condition)
|
||||
})
|
||||
|
||||
// When we have any joins, we need to count each individual ID only once.
|
||||
// COUNT(*) doesn't work for this well in this case, as it also counts joined tables.
|
||||
// SELECT (COUNT DISTINCT id) has a very slow performance on large tables.
|
||||
// Instead, COUNT (GROUP BY id) can be used which is still slower than COUNT(*) but acceptable.
|
||||
const countResult = await chainMethods({
|
||||
methods: chainedMethods,
|
||||
query: db
|
||||
.select({
|
||||
count: sql`COUNT(1) OVER()`,
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where)
|
||||
.groupBy(this.tables[tableName].id)
|
||||
.limit(1),
|
||||
})
|
||||
const countResult = await query
|
||||
|
||||
return Number(countResult[0].count)
|
||||
return Number(countResult[0]?.count)
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export const createJSONQuery = ({
|
||||
treatAsArray,
|
||||
value,
|
||||
}: CreateJSONQueryArgs): string => {
|
||||
if (treatAsArray.includes(pathSegments[1])) {
|
||||
if (treatAsArray?.includes(pathSegments[1]!) && table) {
|
||||
return fromArray({
|
||||
operator,
|
||||
pathSegments,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { DeleteWhere } from './types.js'
|
||||
import type { DeleteWhere, SQLiteAdapter } from './types.js'
|
||||
|
||||
export const deleteWhere: DeleteWhere = async function deleteWhere({ db, tableName, where }) {
|
||||
export const deleteWhere: DeleteWhere = async function (
|
||||
// Here 'this' is not a parameter. See:
|
||||
// https://www.typescriptlang.org/docs/handbook/2/classes.html#this-parameters
|
||||
this: SQLiteAdapter,
|
||||
{ db, tableName, where },
|
||||
) {
|
||||
const table = this.tables[tableName]
|
||||
await db.delete(table).where(where)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import type { DropDatabase } from './types.js'
|
||||
import type { Row } from '@libsql/client'
|
||||
|
||||
const getTables = (adapter) => {
|
||||
import type { DropDatabase, SQLiteAdapter } from './types.js'
|
||||
|
||||
const getTables = (adapter: SQLiteAdapter) => {
|
||||
return adapter.client.execute(`SELECT name
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table'
|
||||
AND name NOT LIKE 'sqlite_%';`)
|
||||
}
|
||||
|
||||
const dropTables = (adapter, rows) => {
|
||||
const dropTables = (adapter: SQLiteAdapter, rows: Row[]) => {
|
||||
const multi = `
|
||||
PRAGMA foreign_keys = OFF;\n
|
||||
${rows.map(({ name }) => `DROP TABLE IF EXISTS ${name}`).join(';\n ')};\n
|
||||
${rows.map(({ name }) => `DROP TABLE IF EXISTS ${name as string}`).join(';\n ')};\n
|
||||
PRAGMA foreign_keys = ON;`
|
||||
return adapter.client.executeMultiple(multi)
|
||||
}
|
||||
|
||||
export const dropDatabase: DropDatabase = async function dropDatabase({ adapter }) {
|
||||
export const dropDatabase: DropDatabase = async function ({ adapter }) {
|
||||
const result = await getTables(adapter)
|
||||
await dropTables(adapter, result.rows)
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ import { sql } from 'drizzle-orm'
|
||||
import type { Execute } from './types.js'
|
||||
|
||||
export const execute: Execute<any> = function execute({ db, drizzle, raw, sql: statement }) {
|
||||
const executeFrom = db ?? drizzle
|
||||
const executeFrom = (db ?? drizzle)!
|
||||
|
||||
if (raw) {
|
||||
const result = executeFrom.run(sql.raw(raw))
|
||||
return result
|
||||
} else {
|
||||
const result = executeFrom.run(statement)
|
||||
const result = executeFrom.run(statement!)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
79
packages/db-sqlite/src/exports/types-deprecated.ts
Normal file
79
packages/db-sqlite/src/exports/types-deprecated.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type {
|
||||
Args as _Args,
|
||||
CountDistinct as _CountDistinct,
|
||||
DeleteWhere as _DeleteWhere,
|
||||
DropDatabase as _DropDatabase,
|
||||
Execute as _Execute,
|
||||
GeneratedDatabaseSchema as _GeneratedDatabaseSchema,
|
||||
GenericColumns as _GenericColumns,
|
||||
GenericRelation as _GenericRelation,
|
||||
GenericTable as _GenericTable,
|
||||
IDType as _IDType,
|
||||
Insert as _Insert,
|
||||
MigrateDownArgs as _MigrateDownArgs,
|
||||
MigrateUpArgs as _MigrateUpArgs,
|
||||
SQLiteAdapter as _SQLiteAdapter,
|
||||
SQLiteSchemaHook as _SQLiteSchemaHook,
|
||||
} from '../types.js'
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type SQLiteAdapter = _SQLiteAdapter
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type Args = _Args
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type CountDistinct = _CountDistinct
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type DeleteWhere = _DeleteWhere
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type DropDatabase = _DropDatabase
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type Execute<T> = _Execute<T>
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type GeneratedDatabaseSchema = _GeneratedDatabaseSchema
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type GenericColumns = _GenericColumns
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type GenericRelation = _GenericRelation
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type GenericTable = _GenericTable
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type IDType = _IDType
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type Insert = _Insert
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type MigrateDownArgs = _MigrateDownArgs
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type MigrateUpArgs = _MigrateUpArgs
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type SQLiteSchemaHook = _SQLiteSchemaHook
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateJobs,
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
@@ -57,10 +58,6 @@ import { init } from './init.js'
|
||||
import { insert } from './insert.js'
|
||||
import { requireDrizzleKit } from './requireDrizzleKit.js'
|
||||
|
||||
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
|
||||
|
||||
export { sql } from 'drizzle-orm'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
|
||||
export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
@@ -92,9 +89,11 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
allowIDOnCreate,
|
||||
autoIncrement: args.autoIncrement ?? false,
|
||||
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
client: undefined,
|
||||
clientConfig: args.client,
|
||||
defaultDrizzleSnapshot,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
drizzle: undefined,
|
||||
features: {
|
||||
json: true,
|
||||
@@ -112,6 +111,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
logger: args.logger,
|
||||
operators,
|
||||
prodMigrations: args.prodMigrations,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
push: args.push,
|
||||
rawRelations: {},
|
||||
rawTables: {},
|
||||
@@ -122,7 +122,9 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
sessions: {},
|
||||
tableNameMap: new Map<string, string>(),
|
||||
tables: {},
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
transactionOptions: args.transactionOptions || undefined,
|
||||
updateJobs,
|
||||
updateMany,
|
||||
versionsSuffix: args.versionsSuffix || '_v',
|
||||
|
||||
@@ -160,6 +162,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
find,
|
||||
findGlobal,
|
||||
findGlobalVersions,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
findOne,
|
||||
findVersions,
|
||||
indexes: new Set<string>(),
|
||||
@@ -175,8 +178,10 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
packageName: '@payloadcms/db-sqlite',
|
||||
payload,
|
||||
queryDrafts,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
rejectInitializing,
|
||||
requireDrizzleKit,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
resolveInitializing,
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
@@ -188,8 +193,32 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'sqlite',
|
||||
allowIDOnCreate,
|
||||
defaultIDType: payloadIDType,
|
||||
init: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo deprecate /types subpath export in 4.0
|
||||
*/
|
||||
export type {
|
||||
Args as SQLiteAdapterArgs,
|
||||
CountDistinct,
|
||||
DeleteWhere,
|
||||
DropDatabase,
|
||||
Execute,
|
||||
GeneratedDatabaseSchema,
|
||||
GenericColumns,
|
||||
GenericRelation,
|
||||
GenericTable,
|
||||
IDType,
|
||||
Insert,
|
||||
MigrateDownArgs,
|
||||
MigrateUpArgs,
|
||||
SQLiteAdapter,
|
||||
SQLiteSchemaHook,
|
||||
} from './types.js'
|
||||
|
||||
export { sql } from 'drizzle-orm'
|
||||
|
||||
@@ -28,7 +28,7 @@ export const init: Init = async function init(this: SQLiteAdapter) {
|
||||
await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this })
|
||||
|
||||
for (const tableName in this.rawTables) {
|
||||
buildDrizzleTable({ adapter, locales, rawTable: this.rawTables[tableName] })
|
||||
buildDrizzleTable({ adapter, locales: locales!, rawTable: this.rawTables[tableName]! })
|
||||
}
|
||||
|
||||
buildDrizzleRelations({
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import type { Insert } from './types.js'
|
||||
import type { Insert, SQLiteAdapter } from './types.js'
|
||||
|
||||
export const insert: Insert = async function insert({
|
||||
db,
|
||||
onConflictDoUpdate,
|
||||
tableName,
|
||||
values,
|
||||
}): Promise<Record<string, unknown>[]> {
|
||||
export const insert: Insert = async function (
|
||||
// Here 'this' is not a parameter. See:
|
||||
// https://www.typescriptlang.org/docs/handbook/2/classes.html#this-parameters
|
||||
this: SQLiteAdapter,
|
||||
{ db, onConflictDoUpdate, tableName, values },
|
||||
): Promise<Record<string, unknown>[]> {
|
||||
const table = this.tables[tableName]
|
||||
let result
|
||||
|
||||
if (onConflictDoUpdate) {
|
||||
result = db.insert(table).values(values).onConflictDoUpdate(onConflictDoUpdate).returning()
|
||||
} else {
|
||||
result = db.insert(table).values(values).returning()
|
||||
}
|
||||
result = await result
|
||||
return result
|
||||
const result = onConflictDoUpdate
|
||||
? await db.insert(table).values(values).onConflictDoUpdate(onConflictDoUpdate).returning()
|
||||
: await db.insert(table).values(values).returning()
|
||||
|
||||
// See https://github.com/payloadcms/payload/pull/11831#discussion_r2010431908
|
||||
return result as Record<string, unknown>[]
|
||||
}
|
||||
|
||||
@@ -81,8 +81,9 @@ export const buildDrizzleTable: BuildDrizzleTable = ({ adapter, locales, rawTabl
|
||||
}
|
||||
|
||||
if (column.reference) {
|
||||
columns[key].references(() => adapter.tables[column.reference.table][column.reference.name], {
|
||||
onDelete: column.reference.onDelete,
|
||||
const ref = column.reference
|
||||
columns[key].references(() => adapter.tables[ref.table][ref.name], {
|
||||
onDelete: ref.onDelete,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
/* TODO: remove the following lines */
|
||||
"strict": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../payload"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.30.0",
|
||||
"version": "3.35.1",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -25,9 +25,9 @@
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./src/types.ts",
|
||||
"types": "./src/types.ts",
|
||||
"default": "./src/types.ts"
|
||||
"import": "./src/exports/types-deprecated.ts",
|
||||
"types": "./src/exports/types-deprecated.ts",
|
||||
"default": "./src/exports/types-deprecated.ts"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./src/exports/migration-utils.ts",
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/types.ts",
|
||||
"types": "./src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"mock.js"
|
||||
@@ -103,9 +103,9 @@
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./dist/types.js",
|
||||
"types": "./dist/types.d.ts",
|
||||
"default": "./dist/types.js"
|
||||
"import": "./dist/exports/types-deprecated.js",
|
||||
"types": "./dist/exports/types-deprecated.d.ts",
|
||||
"default": "./dist/exports/types-deprecated.js"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./dist/exports/migration-utils.js",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { Connect } from 'payload'
|
||||
import type { Connect, Migration } from 'payload'
|
||||
|
||||
import { pushDevSchema } from '@payloadcms/drizzle'
|
||||
import { sql, VercelPool } from '@vercel/postgres'
|
||||
@@ -60,7 +60,8 @@ export const connect: Connect = async function connect(
|
||||
this.payload.logger.info('---- DROPPED TABLES ----')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error))
|
||||
if (err.message?.match(/database .* does not exist/i) && !this.disableCreateDatabase) {
|
||||
// capitalize first char of the err msg
|
||||
this.payload.logger.info(
|
||||
@@ -69,7 +70,7 @@ export const connect: Connect = async function connect(
|
||||
const isCreated = await this.createDatabase()
|
||||
|
||||
if (isCreated) {
|
||||
await this.connect(options)
|
||||
await this.connect?.(options)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@@ -101,6 +102,6 @@ export const connect: Connect = async function connect(
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && this.prodMigrations) {
|
||||
await this.migrate({ migrations: this.prodMigrations })
|
||||
await this.migrate({ migrations: this.prodMigrations as Migration[] })
|
||||
}
|
||||
}
|
||||
|
||||
20
packages/db-vercel-postgres/src/exports/types-deprecated.ts
Normal file
20
packages/db-vercel-postgres/src/exports/types-deprecated.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type {
|
||||
Args as _Args,
|
||||
GeneratedDatabaseSchema as _GeneratedDatabaseSchema,
|
||||
VercelPostgresAdapter as _VercelPostgresAdapter,
|
||||
} from '../types.js'
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-vercel-postgres` instead
|
||||
*/
|
||||
export type Args = _Args
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-vercel-postgres` instead
|
||||
*/
|
||||
export type GeneratedDatabaseSchema = _GeneratedDatabaseSchema
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-vercel-postgres` instead
|
||||
*/
|
||||
export type VercelPostgresAdapter = _VercelPostgresAdapter
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PgTableFn } from 'drizzle-orm/pg-core'
|
||||
import type { DatabaseAdapterObj, Payload } from 'payload'
|
||||
|
||||
import {
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateJobs,
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
@@ -80,10 +82,10 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
if (args.schemaName) {
|
||||
adapterSchema = pgSchema(args.schemaName)
|
||||
} else {
|
||||
adapterSchema = { enum: pgEnum, table: pgTable }
|
||||
adapterSchema = { enum: pgEnum, table: pgTable as unknown as PgTableFn<string> }
|
||||
}
|
||||
|
||||
const extensions = (args.extensions ?? []).reduce((acc, name) => {
|
||||
const extensions = (args.extensions ?? []).reduce<Record<string, boolean>>((acc, name) => {
|
||||
acc[name] = true
|
||||
return acc
|
||||
}, {})
|
||||
@@ -97,6 +99,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
createExtensions,
|
||||
defaultDrizzleSnapshot,
|
||||
disableCreateDatabase: args.disableCreateDatabase ?? false,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
drizzle: undefined,
|
||||
enums: {},
|
||||
extensions,
|
||||
@@ -123,6 +126,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
pool: undefined,
|
||||
poolOptions: args.pool,
|
||||
prodMigrations: args.prodMigrations,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
push: args.push,
|
||||
rawRelations: {},
|
||||
rawTables: {},
|
||||
@@ -135,6 +139,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
tables: {},
|
||||
tablesFilter: args.tablesFilter,
|
||||
transactionOptions: args.transactionOptions || undefined,
|
||||
updateJobs,
|
||||
versionsSuffix: args.versionsSuffix || '_v',
|
||||
|
||||
// DatabaseAdapter
|
||||
@@ -169,6 +174,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
find,
|
||||
findGlobal,
|
||||
findGlobalVersions,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
findOne,
|
||||
findVersions,
|
||||
init,
|
||||
@@ -183,8 +189,10 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
packageName: '@payloadcms/db-vercel-postgres',
|
||||
payload,
|
||||
queryDrafts,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
rejectInitializing,
|
||||
requireDrizzleKit,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
resolveInitializing,
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
@@ -197,12 +205,21 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'postgres',
|
||||
allowIDOnCreate,
|
||||
defaultIDType: payloadIDType,
|
||||
init: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo deprecate /types subpath export in 4.0
|
||||
*/
|
||||
export type {
|
||||
Args as VercelPostgresAdapterArgs,
|
||||
GeneratedDatabaseSchema,
|
||||
VercelPostgresAdapter,
|
||||
} from './types.js'
|
||||
export type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/drizzle/postgres'
|
||||
export { geometryColumn } from '@payloadcms/drizzle/postgres'
|
||||
export { sql } from 'drizzle-orm'
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
/* TODO: remove the following lines */
|
||||
"strict": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../payload"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.30.0",
|
||||
"version": "3.35.1",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -30,13 +30,13 @@
|
||||
"default": "./src/exports/postgres.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./src/types.ts",
|
||||
"types": "./src/types.ts",
|
||||
"default": "./src/types.ts"
|
||||
"import": "./src/exports/types-deprecated.ts",
|
||||
"types": "./src/exports/types-deprecated.ts",
|
||||
"default": "./src/exports/types-deprecated.ts"
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/types.ts",
|
||||
"types": "./src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"mock.js"
|
||||
@@ -81,9 +81,9 @@
|
||||
"default": "./dist/exports/postgres.js"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./dist/types.js",
|
||||
"types": "./dist/types.d.ts",
|
||||
"default": "./dist/types.js"
|
||||
"import": "./dist/exports/types-deprecated.js",
|
||||
"types": "./dist/exports/types-deprecated.d.ts",
|
||||
"default": "./dist/exports/types-deprecated.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -78,7 +78,9 @@ export const createTableName = ({
|
||||
|
||||
if (result.length > 63) {
|
||||
throw new APIError(
|
||||
`Exceeded max identifier length for table or enum name of 63 characters. Invalid name: ${result}`,
|
||||
`Exceeded max identifier length for table or enum name of 63 characters. Invalid name: ${result}.
|
||||
Tip: You can use the dbName property to reduce the table name length.
|
||||
`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const deleteOne: DeleteOne = async function deleteOne(
|
||||
this: DrizzleAdapter,
|
||||
{ collection: collectionSlug, req, select, where: whereArg, returning },
|
||||
{ collection: collectionSlug, req, returning, select, where: whereArg },
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
@@ -32,9 +32,9 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter: this,
|
||||
chainedMethods: [{ args: [1], method: 'limit' }],
|
||||
db,
|
||||
joins,
|
||||
query: ({ query }) => query.limit(1),
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
@@ -59,6 +59,10 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
docToDelete = await db.query[tableName].findFirst(findManyArgs)
|
||||
}
|
||||
|
||||
if (!docToDelete) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result =
|
||||
returning === false
|
||||
? null
|
||||
@@ -68,6 +72,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
data: docToDelete,
|
||||
fields: collection.flattenedFields,
|
||||
joinQuery: false,
|
||||
tableName,
|
||||
})
|
||||
|
||||
await this.deleteWhere({
|
||||
|
||||
188
packages/drizzle/src/exports/types-deprecated.ts
Normal file
188
packages/drizzle/src/exports/types-deprecated.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import type {
|
||||
BaseRawColumn as _BaseRawColumn,
|
||||
BuildDrizzleTable as _BuildDrizzleTable,
|
||||
BuildQueryJoinAliases as _BuildQueryJoinAliases,
|
||||
ChainedMethods as _ChainedMethods,
|
||||
ColumnToCodeConverter as _ColumnToCodeConverter,
|
||||
CountDistinct as _CountDistinct,
|
||||
CreateJSONQueryArgs as _CreateJSONQueryArgs,
|
||||
DeleteWhere as _DeleteWhere,
|
||||
DrizzleAdapter as _DrizzleAdapter,
|
||||
DrizzleTransaction as _DrizzleTransaction,
|
||||
DropDatabase as _DropDatabase,
|
||||
EnumRawColumn as _EnumRawColumn,
|
||||
Execute as _Execute,
|
||||
GenericColumn as _GenericColumn,
|
||||
GenericColumns as _GenericColumns,
|
||||
GenericPgColumn as _GenericPgColumn,
|
||||
GenericRelation as _GenericRelation,
|
||||
GenericTable as _GenericTable,
|
||||
IDType as _IDType,
|
||||
Insert as _Insert,
|
||||
IntegerRawColumn as _IntegerRawColumn,
|
||||
Migration as _Migration,
|
||||
PostgresDB as _PostgresDB,
|
||||
RawColumn as _RawColumn,
|
||||
RawForeignKey as _RawForeignKey,
|
||||
RawIndex as _RawIndex,
|
||||
RawRelation as _RawRelation,
|
||||
RawTable as _RawTable,
|
||||
RelationMap as _RelationMap,
|
||||
RequireDrizzleKit as _RequireDrizzleKit,
|
||||
SetColumnID as _SetColumnID,
|
||||
SQLiteDB as _SQLiteDB,
|
||||
TimestampRawColumn as _TimestampRawColumn,
|
||||
TransactionPg as _TransactionPg,
|
||||
TransactionSQLite as _TransactionSQLite,
|
||||
UUIDRawColumn as _UUIDRawColumn,
|
||||
VectorRawColumn as _VectorRawColumn,
|
||||
} from '../types.js'
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type BaseRawColumn = _BaseRawColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type BuildDrizzleTable = _BuildDrizzleTable
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type BuildQueryJoinAliases = _BuildQueryJoinAliases
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type ChainedMethods = _ChainedMethods
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type ColumnToCodeConverter = _ColumnToCodeConverter
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type CountDistinct = _CountDistinct
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type CreateJSONQueryArgs = _CreateJSONQueryArgs
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type DeleteWhere = _DeleteWhere
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type DrizzleAdapter = _DrizzleAdapter
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type DrizzleTransaction = _DrizzleTransaction
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type DropDatabase = _DropDatabase
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type EnumRawColumn = _EnumRawColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type Execute<T> = _Execute<T>
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type GenericColumn = _GenericColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type GenericColumns<T> = _GenericColumns<T>
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type GenericPgColumn = _GenericPgColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type GenericRelation = _GenericRelation
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type GenericTable = _GenericTable
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type IDType = _IDType
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type Insert = _Insert
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type IntegerRawColumn = _IntegerRawColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type Migration = _Migration
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type PostgresDB = _PostgresDB
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type RawColumn = _RawColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type RawForeignKey = _RawForeignKey
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type RawIndex = _RawIndex
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type RawRelation = _RawRelation
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type RawTable = _RawTable
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type RelationMap = _RelationMap
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type RequireDrizzleKit = _RequireDrizzleKit
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type SetColumnID = _SetColumnID
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type SQLiteDB = _SQLiteDB
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type TimestampRawColumn = _TimestampRawColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type TransactionPg = _TransactionPg
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type TransactionSQLite = _TransactionSQLite
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type UUIDRawColumn = _UUIDRawColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type VectorRawColumn = _VectorRawColumn
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* @deprecated - will be removed in 4.0. Use query + $dynamic() instead: https://orm.drizzle.team/docs/dynamic-query-building
|
||||
*/
|
||||
export type ChainedMethods = {
|
||||
args: unknown[]
|
||||
method: string
|
||||
@@ -7,6 +10,8 @@ export type ChainedMethods = {
|
||||
* Call and returning methods that would normally be chained together but cannot be because of control logic
|
||||
* @param methods
|
||||
* @param query
|
||||
*
|
||||
* @deprecated - will be removed in 4.0. Use query + $dynamic() instead: https://orm.drizzle.team/docs/dynamic-query-building
|
||||
*/
|
||||
const chainMethods = <T>({ methods, query }: { methods: ChainedMethods; query: T }): T => {
|
||||
return methods.reduce((query, { args, method }) => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { FindArgs, FlattenedField, TypeWithID } from 'payload'
|
||||
import { inArray } from 'drizzle-orm'
|
||||
|
||||
import type { DrizzleAdapter } from '../types.js'
|
||||
import type { ChainedMethods } from './chainMethods.js'
|
||||
|
||||
import buildQuery from '../queries/buildQuery.js'
|
||||
import { selectDistinct } from '../queries/selectDistinct.js'
|
||||
@@ -62,15 +61,6 @@ export const findMany = async function find({
|
||||
const orderedIDMap: Record<number | string, number> = {}
|
||||
let orderedIDs: (number | string)[]
|
||||
|
||||
const selectDistinctMethods: ChainedMethods = []
|
||||
|
||||
if (orderBy) {
|
||||
selectDistinctMethods.push({
|
||||
args: [() => orderBy.map(({ column, order }) => order(column))],
|
||||
method: 'orderBy',
|
||||
})
|
||||
}
|
||||
|
||||
const findManyArgs = buildFindManyArgs({
|
||||
adapter,
|
||||
collectionSlug,
|
||||
@@ -84,15 +74,16 @@ export const findMany = async function find({
|
||||
tableName,
|
||||
versions,
|
||||
})
|
||||
|
||||
selectDistinctMethods.push({ args: [offset], method: 'offset' })
|
||||
selectDistinctMethods.push({ args: [limit], method: 'limit' })
|
||||
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter,
|
||||
chainedMethods: selectDistinctMethods,
|
||||
db,
|
||||
joins,
|
||||
query: ({ query }) => {
|
||||
if (orderBy) {
|
||||
query = query.orderBy(() => orderBy.map(({ column, order }) => order(column)))
|
||||
}
|
||||
return query.offset(offset).limit(limit)
|
||||
},
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
@@ -167,6 +158,7 @@ export const findMany = async function find({
|
||||
data,
|
||||
fields,
|
||||
joinQuery,
|
||||
tableName,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import type { SQLiteSelectBase } from 'drizzle-orm/sqlite-core'
|
||||
import type { SQLiteSelect, SQLiteSelectBase } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
import { and, asc, count, desc, eq, or, sql } from 'drizzle-orm'
|
||||
import {
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { BuildQueryJoinAliases, ChainedMethods, DrizzleAdapter } from '../types.js'
|
||||
import type { BuildQueryJoinAliases, DrizzleAdapter } from '../types.js'
|
||||
import type { Result } from './buildFindManyArgs.js'
|
||||
|
||||
import buildQuery from '../queries/buildQuery.js'
|
||||
@@ -25,7 +25,6 @@ import { operatorMap } from '../queries/operatorMap.js'
|
||||
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
|
||||
import { jsonAggBuildObject } from '../utilities/json.js'
|
||||
import { rawConstraint } from '../utilities/rawConstraint.js'
|
||||
import { chainMethods } from './chainMethods.js'
|
||||
|
||||
const flattenAllWherePaths = (where: Where, paths: string[]) => {
|
||||
for (const k in where) {
|
||||
@@ -197,7 +196,8 @@ export const traverseFields = ({
|
||||
}
|
||||
}
|
||||
|
||||
currentArgs.with[`${path}${field.name}`] = withArray
|
||||
const relationName = field.dbName ? `_${arrayTableName}` : `${path}${field.name}`
|
||||
currentArgs.with[relationName] = withArray
|
||||
|
||||
traverseFields({
|
||||
_locales: withArray.with._locales,
|
||||
@@ -612,34 +612,6 @@ export const traverseFields = ({
|
||||
where: joinQueryWhere,
|
||||
})
|
||||
|
||||
const chainedMethods: ChainedMethods = []
|
||||
|
||||
joins.forEach(({ type, condition, table }) => {
|
||||
chainedMethods.push({
|
||||
args: [table, condition],
|
||||
method: type ?? 'leftJoin',
|
||||
})
|
||||
})
|
||||
|
||||
if (page && limit !== 0) {
|
||||
const offset = (page - 1) * limit - 1
|
||||
if (offset > 0) {
|
||||
chainedMethods.push({
|
||||
args: [offset],
|
||||
method: 'offset',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (limit !== 0) {
|
||||
chainedMethods.push({
|
||||
args: [limit],
|
||||
method: 'limit',
|
||||
})
|
||||
}
|
||||
|
||||
const db = adapter.drizzle as LibSQLDatabase
|
||||
|
||||
for (let key in selectFields) {
|
||||
const val = selectFields[key]
|
||||
|
||||
@@ -654,14 +626,29 @@ export const traverseFields = ({
|
||||
selectFields.parent = newAliasTable.parent
|
||||
}
|
||||
|
||||
const subQuery = chainMethods({
|
||||
methods: chainedMethods,
|
||||
query: db
|
||||
.select(selectFields as any)
|
||||
.from(newAliasTable)
|
||||
.where(subQueryWhere)
|
||||
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
|
||||
}).as(subQueryAlias)
|
||||
let query: SQLiteSelect = db
|
||||
.select(selectFields as any)
|
||||
.from(newAliasTable)
|
||||
.where(subQueryWhere)
|
||||
.orderBy(() => orderBy.map(({ column, order }) => order(column)))
|
||||
.$dynamic()
|
||||
|
||||
joins.forEach(({ type, condition, table }) => {
|
||||
query = query[type ?? 'leftJoin'](table, condition)
|
||||
})
|
||||
|
||||
if (page && limit !== 0) {
|
||||
const offset = (page - 1) * limit - 1
|
||||
if (offset > 0) {
|
||||
query = query.offset(offset)
|
||||
}
|
||||
}
|
||||
|
||||
if (limit !== 0) {
|
||||
query = query.limit(limit)
|
||||
}
|
||||
|
||||
const subQuery = query.as(subQueryAlias)
|
||||
|
||||
if (shouldCount) {
|
||||
currentArgs.extras[`${columnName}_count`] = sql`${db
|
||||
|
||||
@@ -23,16 +23,58 @@ export { migrateFresh } from './migrateFresh.js'
|
||||
export { migrateRefresh } from './migrateRefresh.js'
|
||||
export { migrateReset } from './migrateReset.js'
|
||||
export { migrateStatus } from './migrateStatus.js'
|
||||
export { default as buildQuery } from './queries/buildQuery.js'
|
||||
export { operatorMap } from './queries/operatorMap.js'
|
||||
export type { Operators } from './queries/operatorMap.js'
|
||||
export { parseParams } from './queries/parseParams.js'
|
||||
export { queryDrafts } from './queryDrafts.js'
|
||||
export { buildDrizzleRelations } from './schema/buildDrizzleRelations.js'
|
||||
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 type {
|
||||
BaseRawColumn,
|
||||
BuildDrizzleTable,
|
||||
BuildQueryJoinAliases,
|
||||
ChainedMethods,
|
||||
ColumnToCodeConverter,
|
||||
CountDistinct,
|
||||
CreateJSONQueryArgs,
|
||||
DeleteWhere,
|
||||
DrizzleAdapter,
|
||||
DrizzleTransaction,
|
||||
DropDatabase,
|
||||
EnumRawColumn,
|
||||
Execute,
|
||||
GenericColumn,
|
||||
GenericColumns,
|
||||
GenericPgColumn,
|
||||
GenericRelation,
|
||||
GenericTable,
|
||||
IDType,
|
||||
Insert,
|
||||
IntegerRawColumn,
|
||||
Migration,
|
||||
PostgresDB,
|
||||
RawColumn,
|
||||
RawForeignKey,
|
||||
RawIndex,
|
||||
RawRelation,
|
||||
RawTable,
|
||||
RelationMap,
|
||||
RequireDrizzleKit,
|
||||
SetColumnID,
|
||||
SQLiteDB,
|
||||
TimestampRawColumn,
|
||||
TransactionPg,
|
||||
TransactionSQLite,
|
||||
UUIDRawColumn,
|
||||
VectorRawColumn,
|
||||
} from './types.js'
|
||||
export { updateGlobal } from './updateGlobal.js'
|
||||
export { updateGlobalVersion } from './updateGlobalVersion.js'
|
||||
export { updateJobs } from './updateJobs.js'
|
||||
export { updateMany } from './updateMany.js'
|
||||
export { updateOne } from './updateOne.js'
|
||||
export { updateVersion } from './updateVersion.js'
|
||||
|
||||
@@ -50,7 +50,8 @@ export async function migrateDown(this: DrizzleAdapter): Promise<void> {
|
||||
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
|
||||
})
|
||||
|
||||
const tableExists = await migrationTableExists(this)
|
||||
const tableExists = await migrationTableExists(this, db)
|
||||
|
||||
if (tableExists) {
|
||||
await payload.delete({
|
||||
id: migration.id,
|
||||
|
||||
@@ -54,7 +54,7 @@ export async function migrateRefresh(this: DrizzleAdapter) {
|
||||
msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`,
|
||||
})
|
||||
|
||||
const tableExists = await migrationTableExists(this)
|
||||
const tableExists = await migrationTableExists(this, db)
|
||||
if (tableExists) {
|
||||
await payload.delete({
|
||||
collection: 'payload-migrations',
|
||||
|
||||
@@ -45,7 +45,7 @@ export async function migrateReset(this: DrizzleAdapter): Promise<void> {
|
||||
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
|
||||
})
|
||||
|
||||
const tableExists = await migrationTableExists(this)
|
||||
const tableExists = await migrationTableExists(this, db)
|
||||
if (tableExists) {
|
||||
await payload.delete({
|
||||
id: migration.id,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { PgTableWithColumns } from 'drizzle-orm/pg-core'
|
||||
|
||||
import { count, sql } from 'drizzle-orm'
|
||||
|
||||
import type { ChainedMethods } from '../types.js'
|
||||
import type { BasePostgresAdapter, CountDistinct } from './types.js'
|
||||
|
||||
import { chainMethods } from '../find/chainMethods.js'
|
||||
|
||||
export const countDistinct: CountDistinct = async function countDistinct(
|
||||
this: BasePostgresAdapter,
|
||||
{ db, joins, tableName, where },
|
||||
@@ -20,30 +19,25 @@ export const countDistinct: CountDistinct = async function countDistinct(
|
||||
return Number(countResult[0].count)
|
||||
}
|
||||
|
||||
const chainedMethods: ChainedMethods = []
|
||||
let query = db
|
||||
.select({
|
||||
count: sql`COUNT(1) OVER()`,
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where)
|
||||
.groupBy(this.tables[tableName].id)
|
||||
.limit(1)
|
||||
.$dynamic()
|
||||
|
||||
joins.forEach(({ condition, table }) => {
|
||||
chainedMethods.push({
|
||||
args: [table, condition],
|
||||
method: 'leftJoin',
|
||||
})
|
||||
query = query.leftJoin(table as PgTableWithColumns<any>, condition)
|
||||
})
|
||||
|
||||
// When we have any joins, we need to count each individual ID only once.
|
||||
// COUNT(*) doesn't work for this well in this case, as it also counts joined tables.
|
||||
// SELECT (COUNT DISTINCT id) has a very slow performance on large tables.
|
||||
// Instead, COUNT (GROUP BY id) can be used which is still slower than COUNT(*) but acceptable.
|
||||
const countResult = await chainMethods({
|
||||
methods: chainedMethods,
|
||||
query: db
|
||||
.select({
|
||||
count: sql`COUNT(1) OVER()`,
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where)
|
||||
.groupBy(this.tables[tableName].id)
|
||||
.limit(1),
|
||||
})
|
||||
const countResult = await query
|
||||
|
||||
return Number(countResult[0].count)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import type { SQL } from 'drizzle-orm'
|
||||
import type { SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core'
|
||||
import type { FlattenedBlock, FlattenedField, NumberField, TextField } from 'payload'
|
||||
import type {
|
||||
FlattenedBlock,
|
||||
FlattenedField,
|
||||
NumberField,
|
||||
RelationshipField,
|
||||
TextField,
|
||||
} from 'payload'
|
||||
|
||||
import { and, eq, like, sql } from 'drizzle-orm'
|
||||
import { type PgTableWithColumns } from 'drizzle-orm/pg-core'
|
||||
import { APIError } from 'payload'
|
||||
import { APIError, getFieldByPath } from 'payload'
|
||||
import { fieldShouldBeLocalized, tabHasName } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
import { validate as uuidValidate } from 'uuid'
|
||||
@@ -338,6 +344,112 @@ export const getTableColumnFromPath = ({
|
||||
})
|
||||
}
|
||||
|
||||
case 'join': {
|
||||
if (Array.isArray(field.collection)) {
|
||||
throw new APIError('Not supported')
|
||||
}
|
||||
|
||||
const newCollectionPath = pathSegments.slice(1).join('.')
|
||||
|
||||
if (field.hasMany) {
|
||||
const relationTableName = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${adapter.relationshipsSuffix}`
|
||||
const { newAliasTable: aliasRelationshipTable } = getTableAlias({
|
||||
adapter,
|
||||
tableName: relationTableName,
|
||||
})
|
||||
|
||||
const relationshipField = getFieldByPath({
|
||||
fields: adapter.payload.collections[field.collection].config.flattenedFields,
|
||||
path: field.on,
|
||||
})
|
||||
if (!relationshipField) {
|
||||
throw new APIError('Relationship was not found')
|
||||
}
|
||||
|
||||
addJoinTable({
|
||||
condition: and(
|
||||
eq(
|
||||
adapter.tables[rootTableName].id,
|
||||
aliasRelationshipTable[
|
||||
`${(relationshipField.field as RelationshipField).relationTo as string}ID`
|
||||
],
|
||||
),
|
||||
like(aliasRelationshipTable.path, field.on),
|
||||
),
|
||||
joins,
|
||||
queryPath: field.on,
|
||||
table: aliasRelationshipTable,
|
||||
})
|
||||
|
||||
const relationshipConfig = adapter.payload.collections[field.collection].config
|
||||
const relationshipTableName = adapter.tableNameMap.get(
|
||||
toSnakeCase(relationshipConfig.slug),
|
||||
)
|
||||
|
||||
// parent to relationship join table
|
||||
const relationshipFields = relationshipConfig.flattenedFields
|
||||
|
||||
const { newAliasTable: relationshipTable } = getTableAlias({
|
||||
adapter,
|
||||
tableName: relationshipTableName,
|
||||
})
|
||||
|
||||
joins.push({
|
||||
condition: eq(aliasRelationshipTable.parent, relationshipTable.id),
|
||||
table: relationshipTable,
|
||||
})
|
||||
|
||||
return getTableColumnFromPath({
|
||||
adapter,
|
||||
aliasTable: relationshipTable,
|
||||
collectionPath: newCollectionPath,
|
||||
constraints,
|
||||
// relationshipFields are fields from a different collection => no parentIsLocalized
|
||||
fields: relationshipFields,
|
||||
joins,
|
||||
locale,
|
||||
parentIsLocalized: false,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName: relationshipTableName,
|
||||
selectFields,
|
||||
selectLocale,
|
||||
tableName: relationshipTableName,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
const newTableName = adapter.tableNameMap.get(
|
||||
toSnakeCase(adapter.payload.collections[field.collection].config.slug),
|
||||
)
|
||||
const { newAliasTable } = getTableAlias({ adapter, tableName: newTableName })
|
||||
|
||||
joins.push({
|
||||
condition: eq(
|
||||
newAliasTable[field.on.replaceAll('.', '_')],
|
||||
aliasTable ? aliasTable.id : adapter.tables[tableName].id,
|
||||
),
|
||||
table: newAliasTable,
|
||||
})
|
||||
|
||||
return getTableColumnFromPath({
|
||||
adapter,
|
||||
aliasTable: newAliasTable,
|
||||
collectionPath: newCollectionPath,
|
||||
constraintPath: '',
|
||||
constraints,
|
||||
fields: adapter.payload.collections[field.collection].config.flattenedFields,
|
||||
joins,
|
||||
locale,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
selectFields,
|
||||
tableName: newTableName,
|
||||
value,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'number':
|
||||
case 'text': {
|
||||
if (field.hasMany) {
|
||||
@@ -381,7 +493,6 @@ export const getTableColumnFromPath = ({
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
const newCollectionPath = pathSegments.slice(1).join('.')
|
||||
@@ -645,6 +756,7 @@ export const getTableColumnFromPath = ({
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ type Args = {
|
||||
aliasTable?: Table
|
||||
fields: FlattenedField[]
|
||||
joins: BuildQueryJoinAliases
|
||||
locale: string
|
||||
locale?: string
|
||||
parentIsLocalized: boolean
|
||||
selectFields: Record<string, GenericColumn>
|
||||
selectLocale?: boolean
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { QueryPromise, SQL } from 'drizzle-orm'
|
||||
import type { SQLiteColumn } from 'drizzle-orm/sqlite-core'
|
||||
import type { PgSelect } from 'drizzle-orm/pg-core'
|
||||
import type { SQLiteColumn, SQLiteSelect } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
import type { ChainedMethods } from '../find/chainMethods.js'
|
||||
import type {
|
||||
DrizzleAdapter,
|
||||
DrizzleTransaction,
|
||||
@@ -12,13 +12,11 @@ import type {
|
||||
} from '../types.js'
|
||||
import type { BuildQueryJoinAliases } from './buildQuery.js'
|
||||
|
||||
import { chainMethods } from '../find/chainMethods.js'
|
||||
|
||||
type Args = {
|
||||
adapter: DrizzleAdapter
|
||||
chainedMethods?: ChainedMethods
|
||||
db: DrizzleAdapter['drizzle'] | DrizzleTransaction
|
||||
joins: BuildQueryJoinAliases
|
||||
query?: (args: { query: SQLiteSelect }) => SQLiteSelect
|
||||
selectFields: Record<string, GenericColumn>
|
||||
tableName: string
|
||||
where: SQL
|
||||
@@ -29,42 +27,40 @@ type Args = {
|
||||
*/
|
||||
export const selectDistinct = ({
|
||||
adapter,
|
||||
chainedMethods = [],
|
||||
db,
|
||||
joins,
|
||||
query: queryModifier = ({ query }) => query,
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
}: Args): QueryPromise<{ id: number | string }[] & Record<string, GenericColumn>> => {
|
||||
if (Object.keys(joins).length > 0) {
|
||||
if (where) {
|
||||
chainedMethods.push({ args: [where], method: 'where' })
|
||||
}
|
||||
|
||||
joins.forEach(({ condition, table }) => {
|
||||
chainedMethods.push({
|
||||
args: [table, condition],
|
||||
method: 'leftJoin',
|
||||
})
|
||||
})
|
||||
|
||||
let query
|
||||
let query: SQLiteSelect
|
||||
const table = adapter.tables[tableName]
|
||||
|
||||
if (adapter.name === 'postgres') {
|
||||
query = (db as TransactionPg)
|
||||
.selectDistinct(selectFields as Record<string, GenericPgColumn>)
|
||||
.from(table)
|
||||
.$dynamic() as unknown as SQLiteSelect
|
||||
}
|
||||
if (adapter.name === 'sqlite') {
|
||||
query = (db as TransactionSQLite)
|
||||
.selectDistinct(selectFields as Record<string, SQLiteColumn>)
|
||||
.from(table)
|
||||
.$dynamic()
|
||||
}
|
||||
|
||||
return chainMethods({
|
||||
methods: chainedMethods,
|
||||
query,
|
||||
if (where) {
|
||||
query = query.where(where)
|
||||
}
|
||||
|
||||
joins.forEach(({ condition, table }) => {
|
||||
query = query.leftJoin(table, condition)
|
||||
})
|
||||
|
||||
return queryModifier({
|
||||
query,
|
||||
}) as unknown as QueryPromise<{ id: number | string }[] & Record<string, GenericColumn>>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CompoundIndex, FlattenedField } from 'payload'
|
||||
|
||||
import { InvalidConfiguration } from 'payload'
|
||||
import {
|
||||
array,
|
||||
fieldAffectsData,
|
||||
fieldIsVirtual,
|
||||
fieldShouldBeLocalized,
|
||||
@@ -287,7 +288,9 @@ export const traverseFields = ({
|
||||
}
|
||||
}
|
||||
|
||||
relationsToBuild.set(fieldName, {
|
||||
const relationName = field.dbName ? `_${arrayTableName}` : fieldName
|
||||
|
||||
relationsToBuild.set(relationName, {
|
||||
type: 'many',
|
||||
// arrays have their own localized table, independent of the base table.
|
||||
localized: false,
|
||||
@@ -304,7 +307,7 @@ export const traverseFields = ({
|
||||
},
|
||||
],
|
||||
references: ['id'],
|
||||
relationName: fieldName,
|
||||
relationName,
|
||||
to: parentTableName,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type TransformArgs = {
|
||||
joinQuery?: JoinQuery
|
||||
locale?: string
|
||||
parentIsLocalized?: boolean
|
||||
tableName: string
|
||||
}
|
||||
|
||||
// This is the entry point to transform Drizzle output data
|
||||
@@ -26,6 +27,7 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
|
||||
fields,
|
||||
joinQuery,
|
||||
parentIsLocalized,
|
||||
tableName,
|
||||
}: TransformArgs): T => {
|
||||
let relationships: Record<string, Record<string, unknown>[]> = {}
|
||||
let texts: Record<string, Record<string, unknown>[]> = {}
|
||||
@@ -53,6 +55,7 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
|
||||
adapter,
|
||||
blocks,
|
||||
config,
|
||||
currentTableName: tableName,
|
||||
dataRef: {
|
||||
id: data.id,
|
||||
},
|
||||
@@ -65,7 +68,9 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
|
||||
path: '',
|
||||
relationships,
|
||||
table: data,
|
||||
tablePath: '',
|
||||
texts,
|
||||
topLevelTableName: tableName,
|
||||
})
|
||||
|
||||
deletions.forEach((deletion) => deletion())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FlattenedBlock, FlattenedField, JoinQuery, SanitizedConfig } from 'payload'
|
||||
|
||||
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
|
||||
@@ -22,6 +23,7 @@ type TraverseFieldsArgs = {
|
||||
* The full Payload config
|
||||
*/
|
||||
config: SanitizedConfig
|
||||
currentTableName: string
|
||||
/**
|
||||
* The data reference to be mutated within this recursive function
|
||||
*/
|
||||
@@ -59,10 +61,12 @@ type TraverseFieldsArgs = {
|
||||
* Data structure representing the nearest table from db
|
||||
*/
|
||||
table: Record<string, unknown>
|
||||
tablePath: string
|
||||
/**
|
||||
* All hasMany text fields, as returned by Drizzle, keyed on an object by field path
|
||||
*/
|
||||
texts: Record<string, Record<string, unknown>[]>
|
||||
topLevelTableName: string
|
||||
/**
|
||||
* Set to a locale if this group of fields is within a localized array or block.
|
||||
*/
|
||||
@@ -75,6 +79,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
adapter,
|
||||
blocks,
|
||||
config,
|
||||
currentTableName,
|
||||
dataRef,
|
||||
deletions,
|
||||
fieldPrefix,
|
||||
@@ -85,7 +90,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
path,
|
||||
relationships,
|
||||
table,
|
||||
tablePath,
|
||||
texts,
|
||||
topLevelTableName,
|
||||
withinArrayOrBlockLocale,
|
||||
}: TraverseFieldsArgs): T => {
|
||||
const sanitizedPath = path ? `${path}.` : path
|
||||
@@ -110,6 +117,14 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
const isLocalized = fieldShouldBeLocalized({ field, parentIsLocalized })
|
||||
|
||||
if (field.type === 'array') {
|
||||
const arrayTableName = adapter.tableNameMap.get(
|
||||
`${currentTableName}_${tablePath}${toSnakeCase(field.name)}`,
|
||||
)
|
||||
|
||||
if (field.dbName) {
|
||||
fieldData = table[`_${arrayTableName}`]
|
||||
}
|
||||
|
||||
if (Array.isArray(fieldData)) {
|
||||
if (isLocalized) {
|
||||
result[field.name] = fieldData.reduce((arrayResult, row) => {
|
||||
@@ -129,6 +144,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
adapter,
|
||||
blocks,
|
||||
config,
|
||||
currentTableName: arrayTableName,
|
||||
dataRef: data,
|
||||
deletions,
|
||||
fieldPrefix: '',
|
||||
@@ -138,7 +154,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
path: `${sanitizedPath}${field.name}.${row._order - 1}`,
|
||||
relationships,
|
||||
table: row,
|
||||
tablePath: '',
|
||||
texts,
|
||||
topLevelTableName,
|
||||
withinArrayOrBlockLocale: locale,
|
||||
})
|
||||
|
||||
@@ -175,6 +193,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
adapter,
|
||||
blocks,
|
||||
config,
|
||||
currentTableName: arrayTableName,
|
||||
dataRef: row,
|
||||
deletions,
|
||||
fieldPrefix: '',
|
||||
@@ -184,7 +203,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
path: `${sanitizedPath}${field.name}.${i}`,
|
||||
relationships,
|
||||
table: row,
|
||||
tablePath: '',
|
||||
texts,
|
||||
topLevelTableName,
|
||||
withinArrayOrBlockLocale,
|
||||
}),
|
||||
)
|
||||
@@ -228,11 +249,16 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
(block) => typeof block !== 'string' && block.slug === row.blockType,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
const tableName = adapter.tableNameMap.get(
|
||||
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
)
|
||||
|
||||
if (block) {
|
||||
const blockResult = traverseFields<T>({
|
||||
adapter,
|
||||
blocks,
|
||||
config,
|
||||
currentTableName: tableName,
|
||||
dataRef: row,
|
||||
deletions,
|
||||
fieldPrefix: '',
|
||||
@@ -242,7 +268,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
path: `${blockFieldPath}.${row._order - 1}`,
|
||||
relationships,
|
||||
table: row,
|
||||
tablePath: '',
|
||||
texts,
|
||||
topLevelTableName,
|
||||
withinArrayOrBlockLocale: locale,
|
||||
})
|
||||
|
||||
@@ -300,11 +328,16 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
delete row._index
|
||||
}
|
||||
|
||||
const tableName = adapter.tableNameMap.get(
|
||||
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
)
|
||||
|
||||
acc.push(
|
||||
traverseFields<T>({
|
||||
adapter,
|
||||
blocks,
|
||||
config,
|
||||
currentTableName: tableName,
|
||||
dataRef: row,
|
||||
deletions,
|
||||
fieldPrefix: '',
|
||||
@@ -314,7 +347,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
path: `${blockFieldPath}.${i}`,
|
||||
relationships,
|
||||
table: row,
|
||||
tablePath: '',
|
||||
texts,
|
||||
topLevelTableName,
|
||||
withinArrayOrBlockLocale,
|
||||
}),
|
||||
)
|
||||
@@ -614,6 +649,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
adapter,
|
||||
blocks,
|
||||
config,
|
||||
currentTableName,
|
||||
dataRef: groupData as Record<string, unknown>,
|
||||
deletions,
|
||||
fieldPrefix: groupFieldPrefix,
|
||||
@@ -624,7 +660,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
path: `${sanitizedPath}${field.name}`,
|
||||
relationships,
|
||||
table,
|
||||
tablePath: `${tablePath}${toSnakeCase(field.name)}_`,
|
||||
texts,
|
||||
topLevelTableName,
|
||||
withinArrayOrBlockLocale: locale || withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
|
||||
@@ -496,6 +496,10 @@ export const traverseFields = ({
|
||||
formattedValue = sql`ST_GeomFromGeoJSON(${JSON.stringify(value)})`
|
||||
}
|
||||
|
||||
if (field.type === 'text' && value && typeof value !== 'string') {
|
||||
formattedValue = JSON.stringify(value)
|
||||
}
|
||||
|
||||
if (field.type === 'date') {
|
||||
if (typeof value === 'number' && !Number.isNaN(value)) {
|
||||
formattedValue = new Date(value).toISOString()
|
||||
|
||||
@@ -37,11 +37,8 @@ import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
|
||||
import type { SQLiteRaw } from 'drizzle-orm/sqlite-core/query-builders/raw'
|
||||
import type { QueryResult } from 'pg'
|
||||
|
||||
import type { ChainedMethods } from './find/chainMethods.js'
|
||||
import type { Operators } from './queries/operatorMap.js'
|
||||
|
||||
export { ChainedMethods }
|
||||
|
||||
export type PostgresDB = NodePgDatabase<Record<string, unknown>>
|
||||
|
||||
export type SQLiteDB = LibSQLDatabase<
|
||||
@@ -377,3 +374,8 @@ export type RelationMap = Map<
|
||||
type: 'many' | 'one'
|
||||
}
|
||||
>
|
||||
|
||||
/**
|
||||
* @deprecated - will be removed in 4.0. Use query + $dynamic() instead: https://orm.drizzle.team/docs/dynamic-query-building
|
||||
*/
|
||||
export type { ChainedMethods } from './find/chainMethods.js'
|
||||
|
||||
70
packages/drizzle/src/updateJobs.ts
Normal file
70
packages/drizzle/src/updateJobs.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { UpdateJobs, Where } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { findMany } from './find/findMany.js'
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const updateJobs: UpdateJobs = async function updateMany(
|
||||
this: DrizzleAdapter,
|
||||
{ id, data, limit: limitArg, req, returning, sort: sortArg, where: whereArg },
|
||||
) {
|
||||
if (!(data?.log as object[])?.length) {
|
||||
delete data.log
|
||||
}
|
||||
const whereToUse: Where = id ? { id: { equals: id } } : whereArg
|
||||
const limit = id ? 1 : limitArg
|
||||
|
||||
const db = await getTransaction(this, req)
|
||||
const collection = this.payload.collections['payload-jobs'].config
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
const sort = sortArg !== undefined && sortArg !== null ? sortArg : collection.defaultSort
|
||||
|
||||
const jobs = await findMany({
|
||||
adapter: this,
|
||||
collectionSlug: 'payload-jobs',
|
||||
fields: collection.flattenedFields,
|
||||
limit: id ? 1 : limit,
|
||||
pagination: false,
|
||||
req,
|
||||
sort,
|
||||
tableName,
|
||||
where: whereToUse,
|
||||
})
|
||||
if (!jobs.docs.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const results = []
|
||||
|
||||
// TODO: We need to batch this to reduce the amount of db calls. This can get very slow if we are updating a lot of rows.
|
||||
for (const job of jobs.docs) {
|
||||
const updateData = {
|
||||
...job,
|
||||
...data,
|
||||
}
|
||||
|
||||
const result = await upsertRow({
|
||||
id: job.id,
|
||||
adapter: this,
|
||||
data: updateData,
|
||||
db,
|
||||
fields: collection.flattenedFields,
|
||||
ignoreResult: returning === false,
|
||||
operation: 'update',
|
||||
req,
|
||||
tableName,
|
||||
})
|
||||
|
||||
results.push(result)
|
||||
}
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
@@ -3,9 +3,8 @@ import type { UpdateMany } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { ChainedMethods, DrizzleAdapter } from './types.js'
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { chainMethods } from './find/chainMethods.js'
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { selectDistinct } from './queries/selectDistinct.js'
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
@@ -45,16 +44,10 @@ export const updateMany: UpdateMany = async function updateMany(
|
||||
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter: this,
|
||||
chainedMethods: orderBy
|
||||
? [
|
||||
{
|
||||
args: [() => orderBy.map(({ column, order }) => order(column))],
|
||||
method: 'orderBy',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
db,
|
||||
joins,
|
||||
query: ({ query }) =>
|
||||
orderBy ? query.orderBy(() => orderBy.map(({ column, order }) => order(column))) : query,
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
@@ -69,28 +62,17 @@ export const updateMany: UpdateMany = async function updateMany(
|
||||
|
||||
const table = this.tables[tableName]
|
||||
|
||||
const query = _db.select({ id: table.id }).from(table).where(where)
|
||||
|
||||
const chainedMethods: ChainedMethods = []
|
||||
let query = _db.select({ id: table.id }).from(table).where(where).$dynamic()
|
||||
|
||||
if (typeof limit === 'number' && limit > 0) {
|
||||
chainedMethods.push({
|
||||
args: [limit],
|
||||
method: 'limit',
|
||||
})
|
||||
query = query.limit(limit)
|
||||
}
|
||||
|
||||
if (orderBy) {
|
||||
chainedMethods.push({
|
||||
args: [() => orderBy.map(({ column, order }) => order(column))],
|
||||
method: 'orderBy',
|
||||
})
|
||||
query = query.orderBy(() => orderBy.map(({ column, order }) => order(column)))
|
||||
}
|
||||
|
||||
const docsToUpdate = await chainMethods({
|
||||
methods: chainedMethods,
|
||||
query,
|
||||
})
|
||||
const docsToUpdate = await query
|
||||
|
||||
idsToUpdate = docsToUpdate?.map((doc) => doc.id)
|
||||
}
|
||||
|
||||
@@ -19,50 +19,51 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
joins: joinQuery,
|
||||
locale,
|
||||
req,
|
||||
returning,
|
||||
select,
|
||||
where: whereArg,
|
||||
returning,
|
||||
},
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
const whereToUse = whereArg || { id: { equals: id } }
|
||||
let idToUpdate = id
|
||||
|
||||
const { joins, selectFields, where } = buildQuery({
|
||||
adapter: this,
|
||||
fields: collection.flattenedFields,
|
||||
locale,
|
||||
tableName,
|
||||
where: whereToUse,
|
||||
})
|
||||
if (!idToUpdate) {
|
||||
const { joins, selectFields, where } = buildQuery({
|
||||
adapter: this,
|
||||
fields: collection.flattenedFields,
|
||||
locale,
|
||||
tableName,
|
||||
where: whereArg,
|
||||
})
|
||||
|
||||
// selectDistinct will only return if there are joins
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter: this,
|
||||
chainedMethods: [{ args: [1], method: 'limit' }],
|
||||
db,
|
||||
joins,
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
})
|
||||
// selectDistinct will only return if there are joins
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter: this,
|
||||
db,
|
||||
joins,
|
||||
query: ({ query }) => query.limit(1),
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
})
|
||||
|
||||
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 table = this.tables[tableName]
|
||||
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 table = this.tables[tableName]
|
||||
|
||||
const docsToUpdate = await (db as LibSQLDatabase)
|
||||
.select({
|
||||
id: table.id,
|
||||
})
|
||||
.from(table)
|
||||
.where(where)
|
||||
.limit(1)
|
||||
idToUpdate = docsToUpdate?.[0]?.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({
|
||||
@@ -71,12 +72,12 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
data,
|
||||
db,
|
||||
fields: collection.flattenedFields,
|
||||
ignoreResult: returning === false,
|
||||
joinQuery,
|
||||
operation: 'update',
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
ignoreResult: returning === false,
|
||||
})
|
||||
|
||||
if (returning === false) {
|
||||
|
||||
@@ -423,6 +423,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
path: fieldName,
|
||||
},
|
||||
],
|
||||
req,
|
||||
},
|
||||
req?.t,
|
||||
)
|
||||
@@ -466,6 +467,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
data: doc,
|
||||
fields,
|
||||
joinQuery: false,
|
||||
tableName,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
@@ -131,7 +131,7 @@ export const createSchemaGenerator = ({
|
||||
let foreignKeyDeclaration = `${sanitizeObjectKey(key)}: foreignKey({
|
||||
columns: [${foreignKey.columns.map((col) => `columns['${col}']`).join(', ')}],
|
||||
foreignColumns: [${foreignKey.foreignColumns.map((col) => `${accessProperty(col.table, col.name)}`).join(', ')}],
|
||||
name: '${foreignKey.name}'
|
||||
name: '${foreignKey.name}'
|
||||
})`
|
||||
|
||||
if (foreignKey.onDelete) {
|
||||
@@ -167,11 +167,11 @@ ${Object.entries(table.columns)
|
||||
}${
|
||||
extrasDeclarations.length
|
||||
? `, (columns) => ({
|
||||
${extrasDeclarations.join('\n ')}
|
||||
${extrasDeclarations.join('\n ')}
|
||||
})`
|
||||
: ''
|
||||
}
|
||||
)
|
||||
)
|
||||
`
|
||||
|
||||
tableDeclarations.push(tableCode)
|
||||
@@ -250,7 +250,7 @@ type DatabaseSchema = {
|
||||
`
|
||||
|
||||
const finalDeclaration = `
|
||||
declare module '${this.packageName}/types' {
|
||||
declare module '${this.packageName}' {
|
||||
export interface GeneratedDatabaseSchema {
|
||||
schema: DatabaseSchema
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { DrizzleAdapter } from '../types.js'
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
|
||||
export const migrationTableExists = async (adapter: DrizzleAdapter): Promise<boolean> => {
|
||||
import type { DrizzleAdapter, PostgresDB } from '../types.js'
|
||||
|
||||
export const migrationTableExists = async (
|
||||
adapter: DrizzleAdapter,
|
||||
db?: LibSQLDatabase | PostgresDB,
|
||||
): Promise<boolean> => {
|
||||
let statement
|
||||
|
||||
if (adapter.name === 'postgres') {
|
||||
@@ -20,7 +25,7 @@ export const migrationTableExists = async (adapter: DrizzleAdapter): Promise<boo
|
||||
}
|
||||
|
||||
const result = await adapter.execute({
|
||||
drizzle: adapter.drizzle,
|
||||
drizzle: db ?? adapter.drizzle,
|
||||
raw: statement,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.30.0",
|
||||
"version": "3.35.1",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.30.0",
|
||||
"version": "3.35.1",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.30.0",
|
||||
"version": "3.35.1",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { GraphQLBoolean, GraphQLInt, GraphQLList, GraphQLObjectType } from 'graphql'
|
||||
import { GraphQLBoolean, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType } from 'graphql'
|
||||
|
||||
export const buildPaginatedListType = (name, docType) =>
|
||||
new GraphQLObjectType({
|
||||
name,
|
||||
fields: {
|
||||
docs: {
|
||||
type: new GraphQLList(docType),
|
||||
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(docType))),
|
||||
},
|
||||
hasNextPage: { type: GraphQLBoolean },
|
||||
hasPrevPage: { type: GraphQLBoolean },
|
||||
limit: { type: GraphQLInt },
|
||||
nextPage: { type: GraphQLInt },
|
||||
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
|
||||
hasPrevPage: { type: new GraphQLNonNull(GraphQLBoolean) },
|
||||
limit: { type: new GraphQLNonNull(GraphQLInt) },
|
||||
nextPage: { type: new GraphQLNonNull(GraphQLInt) },
|
||||
offset: { type: GraphQLInt },
|
||||
page: { type: GraphQLInt },
|
||||
pagingCounter: { type: GraphQLInt },
|
||||
prevPage: { type: GraphQLInt },
|
||||
totalDocs: { type: GraphQLInt },
|
||||
totalPages: { type: GraphQLInt },
|
||||
page: { type: new GraphQLNonNull(GraphQLInt) },
|
||||
pagingCounter: { type: new GraphQLNonNull(GraphQLInt) },
|
||||
prevPage: { type: new GraphQLNonNull(GraphQLInt) },
|
||||
totalDocs: { type: new GraphQLNonNull(GraphQLInt) },
|
||||
totalPages: { type: new GraphQLNonNull(GraphQLInt) },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -348,11 +348,15 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
|
||||
name: joinName,
|
||||
fields: {
|
||||
docs: {
|
||||
type: Array.isArray(field.collection)
|
||||
? GraphQLJSON
|
||||
: new GraphQLList(graphqlResult.collections[field.collection].graphQL.type),
|
||||
type: new GraphQLNonNull(
|
||||
Array.isArray(field.collection)
|
||||
? GraphQLJSON
|
||||
: new GraphQLList(
|
||||
new GraphQLNonNull(graphqlResult.collections[field.collection].graphQL.type),
|
||||
),
|
||||
),
|
||||
},
|
||||
hasNextPage: { type: GraphQLBoolean },
|
||||
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
|
||||
},
|
||||
}),
|
||||
args: {
|
||||
@@ -379,6 +383,8 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
|
||||
const { limit, page, sort, where } = args
|
||||
const { req } = context
|
||||
|
||||
const draft = Boolean(args.draft ?? context.req.query?.draft)
|
||||
|
||||
const fullWhere = combineQueries(where, {
|
||||
[field.on]: { equals: parent._id ?? parent.id },
|
||||
})
|
||||
@@ -390,6 +396,7 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
|
||||
return await req.payload.find({
|
||||
collection,
|
||||
depth: 0,
|
||||
draft,
|
||||
fallbackLocale: req.fallbackLocale,
|
||||
limit,
|
||||
locale: req.locale,
|
||||
@@ -425,7 +432,7 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
|
||||
...objectTypeConfig,
|
||||
[formatName(field.name)]: formattedNameResolver({
|
||||
type: withNullableType({
|
||||
type: field?.hasMany === true ? new GraphQLList(type) : type,
|
||||
type: field?.hasMany === true ? new GraphQLList(new GraphQLNonNull(type)) : type,
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
@@ -853,7 +860,10 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
|
||||
...objectTypeConfig,
|
||||
[formatName(field.name)]: formattedNameResolver({
|
||||
type: withNullableType({
|
||||
type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
|
||||
type:
|
||||
field.hasMany === true
|
||||
? new GraphQLList(new GraphQLNonNull(GraphQLString))
|
||||
: GraphQLString,
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.30.0",
|
||||
"version": "3.35.1",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -45,8 +45,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/react": "19.0.12",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.30.0",
|
||||
"version": "3.35.1",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.30.0",
|
||||
"version": "3.35.1",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import type { FieldSchemaJSON } from 'payload'
|
||||
|
||||
import type { LivePreviewMessageEvent } from './types.js'
|
||||
|
||||
import { isLivePreviewEvent } from './isLivePreviewEvent.js'
|
||||
import { mergeData } from './mergeData.js'
|
||||
|
||||
const _payloadLivePreview = {
|
||||
const _payloadLivePreview: {
|
||||
fieldSchema: FieldSchemaJSON | undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
previousData: any
|
||||
} = {
|
||||
/**
|
||||
* For performance reasons, `fieldSchemaJSON` will only be sent once on the initial message
|
||||
* We need to cache this value so that it can be used across subsequent messages
|
||||
@@ -18,7 +24,7 @@ const _payloadLivePreview = {
|
||||
previousData: undefined,
|
||||
}
|
||||
|
||||
export const handleMessage = async <T>(args: {
|
||||
export const handleMessage = async <T extends Record<string, any>>(args: {
|
||||
apiRoute?: string
|
||||
depth?: number
|
||||
event: LivePreviewMessageEvent<T>
|
||||
|
||||
@@ -4,7 +4,15 @@ import type { PopulationsByCollection } from './types.js'
|
||||
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
const defaultRequestHandler = ({ apiPath, endpoint, serverURL }) => {
|
||||
const defaultRequestHandler = ({
|
||||
apiPath,
|
||||
endpoint,
|
||||
serverURL,
|
||||
}: {
|
||||
apiPath: string
|
||||
endpoint: string
|
||||
serverURL: string
|
||||
}) => {
|
||||
const url = `${serverURL}${apiPath}/${endpoint}`
|
||||
return fetch(url, {
|
||||
credentials: 'include',
|
||||
@@ -19,7 +27,7 @@ const defaultRequestHandler = ({ apiPath, endpoint, serverURL }) => {
|
||||
// Instead, we keep track of the old locale ourselves and trigger a re-population when it changes
|
||||
let prevLocale: string | undefined
|
||||
|
||||
export const mergeData = async <T>(args: {
|
||||
export const mergeData = async <T extends Record<string, any>>(args: {
|
||||
apiRoute?: string
|
||||
collectionPopulationRequestHandler?: ({
|
||||
apiPath,
|
||||
@@ -86,7 +94,7 @@ export const mergeData = async <T>(args: {
|
||||
|
||||
if (res?.docs?.length > 0) {
|
||||
res.docs.forEach((doc) => {
|
||||
populationsByCollection[collection].forEach((population) => {
|
||||
populationsByCollection[collection]?.forEach((population) => {
|
||||
if (population.id === doc.id) {
|
||||
population.ref[population.accessor] = doc
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { handleMessage } from './handleMessage.js'
|
||||
|
||||
export const subscribe = <T>(args: {
|
||||
export const subscribe = <T extends Record<string, any>>(args: {
|
||||
apiRoute?: string
|
||||
callback: (data: T) => void
|
||||
depth?: number
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import type { DocumentEvent } from 'payload'
|
||||
import type { fieldSchemaToJSON } from 'payload/shared'
|
||||
import type { DocumentEvent, FieldSchemaJSON } from 'payload'
|
||||
|
||||
import type { PopulationsByCollection } from './types.js'
|
||||
|
||||
import { traverseRichText } from './traverseRichText.js'
|
||||
|
||||
export const traverseFields = <T>(args: {
|
||||
export const traverseFields = <T extends Record<string, any>>(args: {
|
||||
externallyUpdatedRelationship?: DocumentEvent
|
||||
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
|
||||
fieldSchema: FieldSchemaJSON
|
||||
incomingData: T
|
||||
localeChanged: boolean
|
||||
populationsByCollection: PopulationsByCollection
|
||||
result: T
|
||||
result: Record<string, any>
|
||||
}): void => {
|
||||
const {
|
||||
externallyUpdatedRelationship,
|
||||
@@ -48,7 +47,7 @@ export const traverseFields = <T>(args: {
|
||||
|
||||
traverseFields({
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: fieldSchema.fields,
|
||||
fieldSchema: fieldSchema.fields!,
|
||||
incomingData: incomingRow,
|
||||
localeChanged,
|
||||
populationsByCollection,
|
||||
@@ -64,7 +63,7 @@ export const traverseFields = <T>(args: {
|
||||
case 'blocks':
|
||||
if (Array.isArray(incomingData[fieldName])) {
|
||||
result[fieldName] = incomingData[fieldName].map((incomingBlock, i) => {
|
||||
const incomingBlockJSON = fieldSchema.blocks[incomingBlock.blockType]
|
||||
const incomingBlockJSON = fieldSchema.blocks?.[incomingBlock.blockType]
|
||||
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = []
|
||||
@@ -82,7 +81,7 @@ export const traverseFields = <T>(args: {
|
||||
|
||||
traverseFields({
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: incomingBlockJSON.fields,
|
||||
fieldSchema: incomingBlockJSON!.fields!,
|
||||
incomingData: incomingBlock,
|
||||
localeChanged,
|
||||
populationsByCollection,
|
||||
@@ -106,7 +105,7 @@ export const traverseFields = <T>(args: {
|
||||
|
||||
traverseFields({
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: fieldSchema.fields,
|
||||
fieldSchema: fieldSchema.fields!,
|
||||
incomingData: incomingData[fieldName] || {},
|
||||
localeChanged,
|
||||
populationsByCollection,
|
||||
@@ -166,11 +165,11 @@ export const traverseFields = <T>(args: {
|
||||
incomingRelation === externallyUpdatedRelationship?.id
|
||||
|
||||
if (hasChanged || hasUpdated || localeChanged) {
|
||||
if (!populationsByCollection[fieldSchema.relationTo]) {
|
||||
populationsByCollection[fieldSchema.relationTo] = []
|
||||
if (!populationsByCollection[fieldSchema.relationTo!]) {
|
||||
populationsByCollection[fieldSchema.relationTo!] = []
|
||||
}
|
||||
|
||||
populationsByCollection[fieldSchema.relationTo].push({
|
||||
populationsByCollection[fieldSchema.relationTo!]?.push({
|
||||
id: incomingRelation,
|
||||
accessor: i,
|
||||
ref: result[fieldName],
|
||||
@@ -265,11 +264,11 @@ export const traverseFields = <T>(args: {
|
||||
// if the new value is not empty, populate it
|
||||
// otherwise set the value to null
|
||||
if (newID) {
|
||||
if (!populationsByCollection[fieldSchema.relationTo]) {
|
||||
populationsByCollection[fieldSchema.relationTo] = []
|
||||
if (!populationsByCollection[fieldSchema.relationTo!]) {
|
||||
populationsByCollection[fieldSchema.relationTo!] = []
|
||||
}
|
||||
|
||||
populationsByCollection[fieldSchema.relationTo].push({
|
||||
populationsByCollection[fieldSchema.relationTo!]?.push({
|
||||
id: newID,
|
||||
accessor: fieldName,
|
||||
ref: result as Record<string, unknown>,
|
||||
|
||||
@@ -79,7 +79,7 @@ export const traverseRichText = ({
|
||||
populationsByCollection[incomingData.relationTo] = []
|
||||
}
|
||||
|
||||
populationsByCollection[incomingData.relationTo].push({
|
||||
populationsByCollection[incomingData.relationTo]?.push({
|
||||
id:
|
||||
incomingData[key] && typeof incomingData[key] === 'object'
|
||||
? incomingData[key].id
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
/* TODO: remove the following lines */
|
||||
"strict": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
},
|
||||
"references": [{ "path": "../payload" }]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.30.0",
|
||||
"version": "3.35.1",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -37,6 +37,11 @@
|
||||
"types": "./src/exports/routes.ts",
|
||||
"default": "./src/exports/routes.ts"
|
||||
},
|
||||
"./auth": {
|
||||
"import": "./src/exports/auth.ts",
|
||||
"types": "./src/exports/auth.ts",
|
||||
"default": "./src/exports/auth.ts"
|
||||
},
|
||||
"./templates": {
|
||||
"import": "./src/exports/templates.ts",
|
||||
"types": "./src/exports/templates.ts",
|
||||
@@ -104,16 +109,16 @@
|
||||
"@babel/preset-env": "7.26.7",
|
||||
"@babel/preset-react": "7.26.3",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@next/eslint-plugin-next": "15.2.3",
|
||||
"@next/eslint-plugin-next": "15.3.0",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/busboy": "1.5.4",
|
||||
"@types/react": "19.0.12",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@types/uuid": "10.0.0",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
|
||||
"esbuild": "0.24.2",
|
||||
"esbuild-sass-plugin": "3.3.1",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
|
||||
"payload": "workspace:*",
|
||||
"swc-plugin-transform-remove-imports": "3.1.0"
|
||||
},
|
||||
@@ -151,6 +156,11 @@
|
||||
"types": "./dist/exports/templates.d.ts",
|
||||
"default": "./dist/exports/templates.js"
|
||||
},
|
||||
"./auth": {
|
||||
"import": "./dist/exports/auth.js",
|
||||
"types": "./dist/exports/auth.d.ts",
|
||||
"default": "./dist/exports/auth.js"
|
||||
},
|
||||
"./utilities": {
|
||||
"import": "./dist/exports/utilities.js",
|
||||
"types": "./dist/exports/utilities.d.ts",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user