Compare commits

...

119 Commits

Author SHA1 Message Date
Sasha
1af94901a3 inputschema 2024-12-21 03:12:07 +02:00
Sasha
d6f44f9c94 start 2024-12-21 02:55:14 +02:00
Sasha
a7109ed048 refactor: consistent caps for acronyms in variable names (#10101)
Improves consistency for variable names like `docID`, `insertedID`,
`parentID`
For example: `docId` -> `docID`
2024-12-20 18:10:34 +00:00
Shrinidhi Shastri
dec87e971a chore(templates): remove console.log that logs payload secret for security (#10095)
I noticed that payload.secret was getting logged via console.log, adding
a significant security risk.
Removed the console.log statements from three preview/route.ts files.
2024-12-20 17:01:47 +00:00
James Mikrut
a501e604d6 chore(richtext-slate): exports useElementButton, useLeafButton (#10047)
Exports `useElementButton` and `useLeafButton` from the Slate package
for custom Slate components.
2024-12-20 09:48:49 -07:00
Dan Ribbens
6bba7bec4e feat(ui): export RelationshipTable component (#10084)
Fixes https://github.com/payloadcms/payload/discussions/9049
2024-12-20 11:47:22 -05:00
Elliot DeNolf
2a6bac97f5 chore(release): eslint/3.9.0 2024-12-20 11:35:52 -05:00
Patrik
52b1a9a720 templates: removes DATABASE_URI env var from with-vercel-website template .env.example (#10098)
### What?

Previously, the `with-vercel-website` template included a `DATABASE_URI`
env var in the `.env.example` file - which was unneeded.

### Why?

The `with-vercel-website` template uses a `POSTGRES_URL` env var for the
db connection string env var instead.

### How?

Removes the `DATABASE_URI` env var from the .env.example file.

Also, updates the `DATABASE_URI` db string names in the following
templates from `payloadtests` to `your-database-name` for a more generic
/ clear name:
- with-postgres
- with-vercel-mongodb
- with-vercel-postgres
- with-vercel-website
2024-12-20 11:02:06 -05:00
Jacob Fletcher
7bedd6d822 fix(examples): awaits getHeaders in auth example (#10100)
The auth example was not properly awaiting `getHeaders` from
`next/navigation`. This was simply outdated, as this function was
changed to async over the course of the various RC versions during our
beta phase.
2024-12-20 10:38:38 -05:00
Sasha
59fc9d094e fix(ui): close copy locale modal after locale is changed (#10096)
Ensures we close the modal _only_ after locale is changed. This caused
our localization e2e's to flake:

![image](https://github.com/user-attachments/assets/41205afe-0e45-499b-9aa6-07734a7f26fc)
2024-12-20 08:29:53 -05:00
Sasha
7c4ea5b86e refactor: optimize database schema generation bin script (#10086)
* Avoids additional file system writes (1 for `await writeFile` and then
`npx prettier --write`) instead prettier now formats the javascript
string directly. Went from 650 MS to 250 MS for the prettify block.
* Disables database connection, since the `db.generateSchema` doesn't
need connection, this also disables Drizzle schema push.
* Properly exits the bin script process.
2024-12-20 14:43:11 +02:00
Jacob Fletcher
7292220109 chore(examples): updates auth example to latest (#10090)
The auth example was still on `v3.0.0-beta.24`, was missing its users
collection config, and was not yet using the component paths pattern
established here: #7246. This updates to latest and fixes these issues.
This example can still use further improvements and housekeeping which
will come in future PRs.
2024-12-19 23:54:24 -05:00
Mason Yekta
dd3c2eb42b fix(examples): add missing header component in auth example (#10088) 2024-12-20 04:16:03 +00:00
Alessio Gravili
b3308736c4 feat: jsdocs for generated types, by using admin.description (#9917)
This makes use of admin.description to generate JSDocs for field,
collection and global generated types.


![image](https://github.com/user-attachments/assets/980d825f-49a2-426d-933a-2ff3d205ea24)


![image](https://github.com/user-attachments/assets/d0b1f288-1ea1-4d80-8c05-003d59a4e41a)

For the future, we should add a dedicated property to override these
JSDocs.

You can view the effect of this PR on our test suite generated types
here:
05f552bbbc
2024-12-19 22:22:43 -05:00
Elliot DeNolf
46e50c4572 templates: bump for v3.9.0 (#10087)
🤖 Automated bump of templates for v3.9.0

Triggered by user: @paulpopus

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-19 22:50:37 +00:00
Dan Ribbens
d03658de01 feat: join field with polymorphic relationships (#9990)
### What?
The join field had a limitation imposed that prevents it from targeting
polymorphic relationship fields. With this change we can support any
relationship fields.

### Why?
Improves the functionality of join field.

### How?
Extended the database adapters and removed the config sanitization that
would throw an error when polymorphic relationships were used.

Fixes #
2024-12-19 22:34:52 +00:00
Sasha
07be617963 fix(db-postgres): relationships v2-v3 migration errors when migrating from v2 to stable v3 (#10080)
When migrating from stable v2 to v3, the provided relationships
migration fails because of not having `payload_locked_documents` and
`payload_locked_documents_rels` tables in the migration generated in v2.


![image](https://github.com/user-attachments/assets/0dc57e6a-5e6e-4b74-bcab-70e660f4e939)
2024-12-19 19:09:07 +00:00
Alessio Gravili
d8c106cb2b fix(templates): broken preview if alternative auth strategy was used, invalid error handling (#9785)
Previously, live preview did not work with oauth, as no token is present
2024-12-19 13:23:47 -05:00
Sasha
e468292039 perf(db-mongodb): improve performance of all operations, up to 50% faster (#9594)
This PR improves speed and memory efficiency across all operations with
the Mongoose adapter.

### How?

- Removes Mongoose layer from all database calls, instead uses MongoDB
directly. (this doesn't remove building mongoose schema since it's still
needed for indexes + users in theory can use it)
- Replaces deep copying of read results using
`JSON.parse(JSON.stringify(data))` with the `transform` `operation:
'read'` function which converts Date's, ObjectID's in relationships /
joins to strings. As before, it also handles transformations for write
operations.
- Faster `hasNearConstraint` for potentially large `where`'s
- `traverseFields` now can accept `flattenedFields` which we use in
`transform`. Less recursive calls with tabs/rows/collapsible

Additional fixes
- Uses current transaction for querying nested relationships properties
in `buildQuery`, previously it wasn't used which could've led to wrong
results
- Allows to clear not required point fields with passing `null` from the
Local API. Previously it didn't work in both, MongoDB and Postgres

Benchmarks using this file
https://github.com/payloadcms/payload/blob/chore/db-benchmark/test/_community/int.spec.ts

### Small Dataset Performance

| Metric | Before Optimization | After Optimization | Improvement (%) |

|---------------------------|---------------------|--------------------|-----------------|
| Average FULL (ms) | 1170 | 844 | 27.86% |
| `payload.db.create` (ms) | 1413 | 691 | 51.12% |
| `payload.db.find` (ms) | 2856 | 2204 | 22.83% |
| `payload.db.deleteMany` (ms) | 15206 | 8439 | 44.53% |
| `payload.db.updateOne` (ms) | 21444 | 12162 | 43.30% |
| `payload.db.findOne` (ms) | 159 | 112 | 29.56% |
| `payload.db.deleteOne` (ms) | 3729 | 2578 | 30.89% |
| DB small FULL (ms) | 64473 | 46451 | 27.93% |

---

### Medium Dataset Performance

| Metric | Before Optimization | After Optimization | Improvement (%) |

|---------------------------|---------------------|--------------------|-----------------|
| Average FULL (ms) | 9407 | 6210 | 33.99% |
| `payload.db.create` (ms) | 10270 | 4321 | 57.93% |
| `payload.db.find` (ms) | 20814 | 16036 | 22.93% |
| `payload.db.deleteMany` (ms) | 126351 | 61789 | 51.11% |
| `payload.db.updateOne` (ms) | 201782 | 99943 | 50.49% |
| `payload.db.findOne` (ms) | 1081 | 817 | 24.43% |
| `payload.db.deleteOne` (ms) | 28534 | 23363 | 18.12% |
| DB medium FULL (ms) | 519518 | 342194 | 34.13% |

---

### Large Dataset Performance

| Metric | Before Optimization | After Optimization | Improvement (%) |

|---------------------------|---------------------|--------------------|-----------------|
| Average FULL (ms) | 26575 | 17509 | 34.14% |
| `payload.db.create` (ms) | 29085 | 12196 | 58.08% |
| `payload.db.find` (ms) | 58497 | 43838 | 25.04% |
| `payload.db.deleteMany` (ms) | 372195 | 173218 | 53.47% |
| `payload.db.updateOne` (ms) | 544089 | 288350 | 47.00% |
| `payload.db.findOne` (ms) | 3058 | 2197 | 28.14% |
| `payload.db.deleteOne` (ms) | 82444 | 64730 | 21.49% |
| DB large FULL (ms) | 1461097 | 969714 | 33.62% |
2024-12-19 13:20:39 -05:00
Sasha
034b442699 test: revert default db adapter in integration tests to mongodb (#10079) 2024-12-19 18:20:28 +00:00
Andrzej Kłapeć
6a8aecadf8 fix(richtext-lexical): incorrect string interpolation in the upload converter (#10069)
This fixes the incorrect `<source>` `media` attribute value generation
within the upload converter.

<img width="919" alt="Zrzut ekranu 2024-12-19 o 12 21 10"
src="https://github.com/user-attachments/assets/6f26de7e-26e0-446a-83c5-6e5a776fac1e"
/>
2024-12-19 11:03:51 -07:00
Sasha
23f1ed4a48 feat(db-postgres, db-sqlite): drizzle schema generation (#9953)
This PR allows to have full type safety on `payload.drizzle` with a
single command
```sh
pnpm payload generate:db-schema
```
Which generates TypeScript code with Drizzle declarations based on the
current database schema.

Example of generated file with the website template: 
https://gist.github.com/r1tsuu/b8687f211b51d9a3a7e78ba41e8fbf03

Video that shows the power:


https://github.com/user-attachments/assets/3ced958b-ec1d-49f5-9f51-d859d5fae236


We also now proxy drizzle package the same way we do for Lexical so you
don't have to install it (and you shouldn't because you may have version
mismatch).
Instead, you can import from Drizzle like this:
```ts
import {
  pgTable,
  index,
  foreignKey,
  integer,
  text,
  varchar,
  jsonb,
  boolean,
  numeric,
  serial,
  timestamp,
  uniqueIndex,
  pgEnum,
} from '@payloadcms/db-postgres/drizzle/pg-core'
import { sql } from '@payloadcms/db-postgres/drizzle'
import { relations } from '@payloadcms/db-postgres/drizzle/relations'
```


Fixes https://github.com/payloadcms/payload/discussions/4318

In the future we can also support types generation for mongoose / raw
mongodb results.
2024-12-19 11:08:17 -05:00
Elliot DeNolf
ba0e7aeee5 chore: proper docker-compose postgres url 2024-12-19 10:42:41 -05:00
Sasha
5753efb0a4 fix(db-mongodb): querying by localized polymorphic relationships using objects (#10037)
Previously, queries like this didn't work:
```ts
const res = await payload.find({
  collection: 'polymorphic-relationships',
  where: {
    polymorphicLocalized: {
      equals: {
        relationTo: 'movies',
        value: movie.id,
      },
    },
  },
})
```

This was due to the incorrectly passed path to MongoDB without
`.{locale}` suffix.
Additionally, to MongoDB now we send:
```

{
  $or: [
    {
      polymorphic: {
        $eq: {
          relationTo: formattedValue.relationTo,
          value: formattedValue.value,
        },
      },
    },
    {
      polymorphic: {
        $eq: {
          relationTo: 'movies',
          value: 'some-id',
        },
      },
    },
  ],
},
```

Instead of:
```
{
  $and: [
    {
      'polymorphic.relationTo': {
        $eq: 'movies ',
      },
    },
    {
      'polymorphic.value': {
        $eq: 'some-id ',
      },
    },
  ],
}
```

To match the _exact_ value. This is essential when we do querying by
relationships with `hasMany: true` and custom IDs that can be repeated.
`$or` is needed if for some reason keys are stored in the DB in a
different order
2024-12-19 10:42:15 -05:00
Germán Jabloñski
12dad35cf9 fix(richtext-lexical): ui bug when zooming in Safari (#10072)
Fix #10043
2024-12-19 15:00:31 +00:00
Elliot DeNolf
997aed346f templates: update dockerfiles (#10073)
- Dockerfiles needed to be updated to the Next.js `with-docker` example.
- docker-compose for postgres templates have been updated.
2024-12-19 09:48:38 -05:00
Sasha
0c57eef621 fix: unique error message regression (#10064)
Regression from https://github.com/payloadcms/payload/pull/9935, we need
to call `req.t` with `''error:valueMustBeUnique''` instead of just
`''error:valueMustBeUnique''`
2024-12-19 08:13:44 +00:00
Sasha
1d46b6d738 fix(ui): join field "add new" calculate initial drawer data with relationship inside blocks (#10057)
We merged https://github.com/payloadcms/payload/pull/9773 that adds
support for join field with relationships inside arrays, it just happens
that now it's also true for relationships inside blocks, added a test
case with blocks.
This just corrects initial data drawer calculation with the "add new"
button if we encounter a blocks field in join's `on`.
2024-12-19 08:45:22 +02:00
Sasha
03ff77544e feat(db-sqlite): add idType: 'uuid' support (#10016)
Adds `idType: 'uuid'` to the SQLite adapter support:
```ts
sqliteAdapter({
  idType: 'uuid',
})
```

Achieved through Drizzle's `$defaultFn()`
https://orm.drizzle.team/docs/latest-releases/drizzle-orm-v0283#-added-defaultfn--default-methods-to-column-builders
as SQLite doesn't have native UUID support. Added `sqlite-uuid` to CI.
2024-12-18 22:44:04 -05:00
Sasha
0e5bda9a74 feat: make req partial and optional in DB / Local API operations (#9935)
### What?
Previously, the `req` argument:
In database operations (e.g `payload.db`) was required and you needed to
pass the whole `req` with all the properties. This is confusing because
in database operations we never use its properties outside of
`req.transactionID` and `req.t`, both of which should be optional as
well.

Now, you don't have to do that cast:
```ts
payload.db.findOne({
  collection: 'posts',
  req: {} as PayloadRequest,
  where: {
    id: {
      equals: 1,
    },
  },
})
```
Becomes:
```ts
payload.db.findOne({
  collection: 'posts',
  where: {
    id: {
      equals: 1,
    },
  },
})
```

If you need to use transactions, you're not required to do the `as` cast
as well now, as the `req` not only optional but also partial -
`Partial<PayloadRequest>`.
`initTransaction`, `commitTransaction`, `killTransaction` utilities are
typed better now as well. They do not require to you pass all the
properties of `req`, but only `payload` -
`MarkRequired<Partial<PayloadRequest>, 'payload'>`
```ts
const req = { payload }
await initTransaction(req)
await payload.db.create({
  collection: "posts",
  data: {},
  req
})
await commitTransaction(req)
```

The same for the Local API. Internal operations (for example
`packages/payload/src/collections/operations/find.ts`) still accept the
whole `req`, but local ones
(`packages/payload/src/collections/operations/local/find.ts`) which are
used through `payload.` now accept `Partial<PayloadRequest>`, as they
pass it through to internal operations with `createLocalReq`.

So now, this is also valid, while previously you had to do `as` cast for
`req`.
```ts
const req = { payload }
await initTransaction(req)
await payload.create({
  collection: "posts",
  data: {},
  req
})
await commitTransaction(req)
```


Marked as deprecated `PayloadRequest['transactionIDPromise']` to remove
in the next major version. It was never used anywhere.
Refactored `withSession` that returns an object to `getSession` that
returns just `ClientSession`. Better type safety for arguments
Deduplicated in all drizzle operations to `getTransaction(this, req)`
utility:
```ts
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
```
Added fallback for throwing unique validation errors in database
operations when `req.t` is not available.

In migration `up` and `down` functions our `req` is not partial, while
we used to passed `req` with only 2 properties - `payload` and
`transactionID`. This is misleading and you can't access for example
`req.t`.
Now, to achieve "real" full `req` - we generate it with `createLocalReq`
in all migration functions.

This all is backwards compatible. In all public API places where you
expect the full `req` (like hooks) you still have it.

### Why?
Better DX, more expected types, less errors because of types casting.
2024-12-18 22:43:37 -05:00
Sasha
eee6432715 fix(db-postgres): query has many relationships nested in row fields (#9944) (#9944)
### What?
Querying by nested to rows fields in has many relationships like this:
```ts
const result = await payload.find({
  collection: 'relationship-fields',
  where: {
    'relationToRowMany.title': { equals: 'some-title' },
  },
})
```
Where the related collection:
```ts
const RowFields: CollectionConfig = {
  slug: rowFieldsSlug,
  fields: [
    {
      type: 'row',
      fields: [
        {
          name: 'title',
          label: 'Title within a row',
          type: 'text',
          required: true,
        },
      ],
    },
  ],
}
```

was broken

### Why?
We migrated to use `flattenedFields`, but not in this specific case.
This error would be caught earlier we used `noImplictAny` typescript
rule. https://www.typescriptlang.org/tsconfig/#noImplicitAny which
wouldn't allow us to create variable like this:
```ts
let relationshipFields // relationshipFields is any here
```
Instead, we should write:
```ts
let relationshipFields: FlattenedField[]
```
We should migrate to it and `strictNullChecks` as well.

Fixes https://github.com/payloadcms/payload/issues/9534
2024-12-18 22:26:08 -05:00
Elliot DeNolf
044c22de54 chore(deps): bump turbo 2024-12-18 20:47:45 -05:00
Paul
ce74f1b238 fix(storage-vercel-blob): fixes issue where files with spaces in their name would not be retrieved correctly (#10062)
URI encodes filenames so that they're retrieved correctly from Vercel's
blob storage. Sometimes spaces would cause problems only in certain file
names
2024-12-19 00:32:59 +00:00
Paul
605cf42cbe templates: add Posts to internal links in website template (#10063)
Posts were previously not selectable as part of the internal links
(reference fields) in the website template.
2024-12-19 00:28:46 +00:00
Sasha
97c120ab28 fix(richtext-*): use correct "for" attribute for label (#10036)
Fixes https://github.com/payloadcms/payload/issues/10034
2024-12-18 16:58:59 -07:00
Jacob Fletcher
97a1f4afa9 test: consolidates custom id e2e tests (#10061)
Although we have a dedicated e2e test suite for custom IDs, tests for
custom unnamed tab and row IDs were still located within the admin test
suite. This consolidates these tests into the appropriate test suite as
expected.
2024-12-18 22:44:46 +00:00
Paul
439dd04ce9 fix: commit transaction if a user isnt found in forgotPassword operation (#10055)
The forgotPassword operation exits silently if no user is found, but
because we don't throw an error the transaction never gets committed
leading to a timeout.
2024-12-18 15:53:53 -06:00
Elliot DeNolf
a3457af36d templates: bump for v3.9.0 (#10060)
🤖 Automated bump of templates for v3.9.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-18 16:20:32 -05:00
Elliot DeNolf
d0d7b51ed5 chore(release): v3.9.0 [skip ci] 2024-12-18 15:58:11 -05:00
Paul
ef90ebb395 feat(storage-*): add support for browser-based caching via etags (#10014)
This PR makes changes to every storage adapter in order to add
browser-based caching by returning etags, then checking for them into
incoming requests and responding a status code of `304` so the data
doesn't have to be returned again.

Performance improvements for cached subsequent requests:

![image](https://github.com/user-attachments/assets/e51b812c-63a0-4bdb-a396-0f172982cb07)


This respects `disableCache` in the dev tools.



Also fixes a bug with getting the latest image when using the Vercel
Blob Storage adapter.
2024-12-18 15:54:36 -05:00
Alessio Gravili
194a8c189a feat: add shouldRestore config to job queue tasks (#10059)
By default, if a task has passed previously and a workflow is re-run,
the task will not be re-run. Instead, the output from the previous task
run will be returned. This is to prevent unnecessary re-runs of tasks
that have already passed.

This PR allows you to configure this behavior through the
`retries.shouldRestore` property. This property accepts a boolean or a
function for more complex restore behaviors.
2024-12-18 13:16:13 -07:00
Patrik
1446fe4694 fix: encodes upload filename urls (#10048)
### What?

Previously, upload files urls were not being encoded.

### Why?

As a result, this could lead to discrepancies where upload filenames
with spaces - the spaces would not be encoded as %20 in the URL.

### How?

To address this issue, we simply need to encode the filename of the
upload media.

Fixes #9698
2024-12-18 14:37:29 -05:00
Elliot DeNolf
f29e6335f6 fix(payload-cloud): improve not found logging (#10058)
In Payload Cloud, an unhelpful message would be surfaced if attempting
to retrieve a non-existent file. This improves the log message and
response to be more helpful.
2024-12-18 14:34:43 -05:00
Paul
93dde52fa9 ci: add email-resend and email-* to scopes for pr-title workflow (#10053) 2024-12-18 14:33:31 -05:00
Javier
198763a24e feat(db-mongodb): allow to customize mongoose schema options with collectionsSchemaOptions (#9885)
Adds the ability to pass additional schema options for collections with:
```ts
mongooseAdapter({
  collectionsSchemaOptions: {
    posts: {
      strict: false,
    },
  },
})
``` 

This changes relates to these:

-   https://github.com/payloadcms/payload/issues/4533
-   https://github.com/payloadcms/payload/discussions/4534

It is a proposal to set custom schema options for mongoose driver. 

I understand this got introduced into `main` v2 after `beta` branch was
created so this feature got lost.

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->

---------

Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
2024-12-18 15:46:39 +00:00
Jacob Fletcher
b8de401195 chore: fixes package license file casing (#10021)
Fixes file casing for all package licenses where applicable, i.e.
`license.md` → `LICENSE.md` to comply with standards outlined
[here](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-a-license-to-a-repository).
2024-12-18 10:36:42 -05:00
Sasha
2ee3e30b50 fix(db-postgres): select query on upload fields with hasMany: true (#10029)
Previously, if you selected only upload `hasMany: true` field, you would
receive an empty arrays always, because the `_rels` table wasn't joined
in this case. Fixes the condition to count `field.type === 'upload'` .
2024-12-18 06:33:12 +02:00
Patrik
61c5e0d3e0 fix(ui): properly allows configuring rows for the textarea field (#10031)
### What?

Previously, setting the `admin.rows` property did not change the height
of the `textarea` field input.

### Why?

Although `rows` was being properly set on the textarea element - it's
absolute positioning prevented the height from actually changing.

### How?

Updates the styles of the textarea field component to properly allow the
rows prop to change the height of the field.

Example w/:

```
{
  name: 'someTextArea',
  type: 'textarea',
  admin: {
    rows: 5,
  }
}
```
Before:
![Screenshot 2024-12-17 at 2 50
19 PM](https://github.com/user-attachments/assets/35d433c3-2fad-4930-9da6-b47b5e180af8)

After:
![Screenshot 2024-12-17 at 2 50
00 PM](https://github.com/user-attachments/assets/1a413639-ac29-46ce-b774-e1a30c5a21f0)

Fixes #10017
2024-12-17 15:46:11 -05:00
Elliot DeNolf
4bfa329fa4 templates: document local development (#10032)
Provide docker-compose for spinning up postgres locally along w/
relevant info in README.
2024-12-17 15:17:18 -05:00
Alessio Gravili
f5c13deb24 build: fix tsconfig monorepo setup (#10028)
Should fix messed up import suggestions and simplifies all tsconfigs
through inheritance.

One main issue was that packages were inheriting `baseURL: "."` from the
root tsconfig. This caused incorrect import suggestions that start with
"packages/...".

This PR ensures that packages do not inherit this baseURL: "." property,
while ensuring the root, non-inherited tsconfig still keeps it to get
tests to work (the importMap needs it)
2024-12-17 14:49:29 -05:00
Patrik
70666a0f7b fix(cpa): updates CPAs w/ vercel-postgres db types to use POSTGRES_URL & updates .env.example to use generic env var strings (#10027)
CPA projects generated with the `vercel-postgres` db type were not
receiving the proper DB env vars in the .env.example & .env files

With the `vercel-postgres` db type, the DB env var needs to be
`POSTGRES_URL` not `DATABASE_URI`.

Additionally, updates the generated .env.example file to show generic
env var strings.

#### Blank w/ MongoDB:
- `.env.example`:
```
DATABASE_URI=mongodb://127.0.0.1/your-database-name
PAYLOAD_SECRET=YOUR_SECRET_HERE
```
- `.env`:
```
# Added by Payload
DATABASE_URI=mongodb://127.0.0.1/test-cpa-blank-mongodb
PAYLOAD_SECRET=aef857429edc7f42a90bb374
```

#### Blank w/ Postgres:
- `.env.example`:
```
DATABASE_URI=postgres://postgres:<password>@127.0.0.1:5432/your-database-name
PAYLOAD_SECRET=YOUR_SECRET_HERE
```
- `.env`:
```
# Added by Payload
DATABASE_URI=postgres://postgres:<password>@127.0.0.1:5432/test-cpa-blank-postgres
PAYLOAD_SECRET=241bfe11fbe0a56dd9757019
```

#### Blank w/ SQLite:
- `.env.example`:
```
DATABASE_URI=file:./your-database-name.db
PAYLOAD_SECRET=YOUR_SECRET_HERE
```
- `.env`:
```
# Added by Payload
DATABASE_URI=file:./test-cpa-blank-sqlite.db
PAYLOAD_SECRET=a7808731b93240a73a11930c
```

#### Blank w/ vercel-postgres:
- `.env.example`:
```
POSTGRES_URL=postgres://postgres:<password>@127.0.0.1:5432/your-database-name
PAYLOAD_SECRET=YOUR_SECRET_HERE
```
- `.env`:
```
# Added by Payload
POSTGRES_URL=postgres://postgres:<password>@127.0.0.1:5432/test-cpa-blank-vercel-postgres
PAYLOAD_SECRET=af3951e923e8e4662c9c3d9e
```

Fixes #9996
2024-12-17 14:13:00 -05:00
Dan Ribbens
b0b2fc6c47 feat: join field support relationships inside arrays (#9773)
### What?

Allow the join field to have a configuration `on` relationships inside
of an array, ie `on: 'myArray.myRelationship'`.

### Why?

This is a more powerful and expressive way to use the join field and not
be limited by usage of array data. For example, if you have a roles
array for multinant sites, you could add a join field on the sites to
show who the admins are.

### How?

This fixes the traverseFields function to allow the configuration to
pass sanitization. In addition, the function for querying the drizzle
tables needed to be ehanced.

Additional changes from https://github.com/payloadcms/payload/pull/9995:

- Significantly improves traverseFields and the 'join' case with a raw
query injection pattern, right now it's internal but we could expose it
at some point, for example for querying vectors.
- Fixes potential issues with not passed locale to traverseFields (it
was undefined always)
- Adds an empty array fallback for joins with localized relationships

Fixes #
https://github.com/payloadcms/payload/discussions/9643

---------

Co-authored-by: Because789 <thomas@because789.ch>
Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
2024-12-17 13:14:43 -05:00
Jacob Fletcher
eb037a0cc6 fix: passes field permissions to custom fields (#10024)
Fixes #9888. Field permissions were not being passed into custom
components. This led to custom components, such as arrays and blocks,
unable to render default Payload fields. This was because their props
lacked the permissions object required for rendering. For example:

```ts
'use client'
import type { ArrayFieldClientComponent } from 'payload'
import { ArrayField } from '@payloadcms/ui'

export const MyArray: ArrayFieldClientComponent = (props) => <ArrayField {...props} />
```

In this example the array field itself would render, but the fields
within each row would not, because the array field did not pass its
permissions down to the rows.
2024-12-17 12:17:42 -05:00
Jarrod Flesch
99ca1babc6 fix: beforeValidate previousValue argument (#10022)
### What?
`previousValue` was incorrect. It would always return the current value.

### Why?
It was accessing siblingData instead of siblingDoc. Other hooks use
siblingDoc, but this one was using siblingData.
2024-12-17 12:08:40 -05:00
urquico
13e050582b docs: fixes typo removeTokenFromRepsonse to removeTokenFromResponse (#10026) 2024-12-17 16:52:06 +00:00
Said Akhrarov
77871050b8 fix(ui): properly sync field values in bulk upload preventing stale data overriding old docs (#9918)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR fixes an issue where bulk upload on an upload field with
hasMany, which had errors on sequential uploads, caused only the last
successful upload to be saved to the field value.

### Why?
To save all successful uploads to the field value and sync what was
shown in the ui to the actual field data.

### How?
By triggering a rerender that syncs `populatedDocs` to the fields
`value` on each sequential successful upload after form errors were
resolved.

Fixes #9890

Before:

[Bulk-upload-before--Post---Payload.webm](https://github.com/user-attachments/assets/6396a88b-21c2-4037-b1ef-fd7f8d16103f)

After:

[Bulk-upload-after---Payload.webm](https://github.com/user-attachments/assets/8566a022-6e86-46c7-87fe-78d01e6dd8c9)

Notes:
- The core issue was that onSuccess function was not properly syncing
the correct field values resulting in stale values that would overwrite
old docs.

---------

Co-authored-by: Patrik Kozak <patrik@payloadcms.com>
2024-12-17 10:08:57 -05:00
Elliot DeNolf
e04be4bf62 templates: improve gen-templates script (#10015)
- Allow gen templates script to take template dir name arg
- All `skipDockerCompose` and `skipConfig`
2024-12-17 10:00:17 -05:00
Hugo Knorr
7037983de0 fix(templates): prevent image priority and lazy loading incompatibility (#10023)
This PR fixes an issue in the hero banner of website templates where
`priority` was passed to `ImageMedia` component but was incompatible with
NextImage `loading="lazy"`, causing error. The fix is to add a ternary
condition to check if `priority` prop is passed before setting `loading.
2024-12-17 14:56:53 +00:00
Said Akhrarov
29ad1fcb77 fix(plugin-search): prevent error on undefined value in linkToDoc component (#9932)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR fixes a runtime error encountered when navigating into a search
doc that had its' related collection doc deleted, but it itself remained
(if for example `deleteFromSearch` deletion failed for some reason).

### Why?
To prevent runtime errors for end-users using `plugin-search`.

### How?
By returning earlier if the field value is undefined or missing required
values in `LinkToDoc`.

Fixes #9443 (partially, see also: #9623)
2024-12-17 04:39:15 +00:00
Elliot DeNolf
2d2a52b00f templates: bump for v3.8.0 (#10013)
🤖 Automated bump of templates for v3.8.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-16 21:31:27 -05:00
Elliot DeNolf
3f35d36934 chore(release): v3.8.0 [skip ci] 2024-12-16 21:12:02 -05:00
Sasha
41167bfbeb feat(db-vercel-postgres): allow to use a local database using pg instead of @vercel/postgres (#9771)
### What?
This PR allows you to use a local database when using
`vercelPostgresAdapter`. This adapter doesn't work with them because it
requires an SSL connection and Neon's WS proxy. Instead we fallback to
using pool from `pg` if `hostname` is either `127.0.0.1` or `localhost`.

If you still want to use `@vercel/postgres` even locally you can pass
`disableUsePgForLocalDatabase: true` here and you'd have to spin up the
DB with a special Neon's Docker Compose setup -
https://vercel.com/docs/storage/vercel-postgres/local-development#option-2:-local-postgres-instance-with-docker

### Why?
Forcing people to use a cloud database locally isn't great. Not only
they are slow but also paid.

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2024-12-16 16:55:08 -05:00
Jacob Fletcher
ed44ec0a9c fix(ui): does not render row labels until form state returns (#10002)
Extension of #9933. Custom components are returned by form state,
meaning that if we don't wait for form state to return before rendering
row labels, the default row label component will render in briefly
before being swapped by a custom component (if applicable). Using the
new `isLoading` prop on array and block rows, we can conditionally
render them just as we currently do for the row fields themselves.
2024-12-16 21:44:19 +00:00
Elliot DeNolf
fa49e04cf8 feat(storage-vercel-blob): allow fallback to disk if token not set (#10005)
Previously with `@payloadcms/plugin-storage-blob`, if token was not set,
the plugin would throw an error. This caused a less-than-ideal developer
experience.

With this change, if the `token` value is undefined:
- Local storage will be used as a fallback
- The error will no longer be thrown.
2024-12-16 21:40:22 +00:00
Jacob Fletcher
f54e180370 fix(templates): adds priority to hero images (#10003)
Hero images should use the `priority` property so that browsers will
preload them. This is because hero images, by definition, are rendered
"above the fold" and should be treated as such, optimizing LCP. This
also means these images should _not_ define a `loading` strategy, as
this disregards the priority flag.
2024-12-16 16:21:29 -05:00
Said Akhrarov
c50f4237a4 docs: fix links in rich-text referencing old lexical sections (#9972) 2024-12-16 14:42:56 -05:00
zuccs
b0d648bf30 docs: broken lexical link (#9991) 2024-12-16 19:40:04 +00:00
Paul
8258d5c943 templates: fix missing ts-ignore in seed script causing build errors (#10001) 2024-12-16 19:24:04 +00:00
Elliot DeNolf
0f63db055b templates: bump for v3.7.0 (#10000)
🤖 Automated bump of templates for v3.7.0

Triggered by user: @paulpopus

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-16 13:08:50 -06:00
Paul
12fa4fd2f9 templates: add hero image field to posts instead of using the meta image for the hero as well (#9999)
Adds a hero image field instead of using the meta image for the hero on
posts collection
2024-12-16 18:29:21 +00:00
Sasha
6dea111d28 feat: expose req to defaultValue function arguments (#9937)
Rework of https://github.com/payloadcms/payload/pull/5912

### What?
Now, when `defaultValue` is defined as function you can receive the
`req` argument:
```ts
{
  name: 'defaultValueFromReq',
  type: 'text',
  defaultValue: async ({ req, user, locale }) => {
    return Promise.resolve(req.context.defaultValue)
  },
},
```

`user` and `locale` even though are repeated in `req`, this potentially
leaves some room to add more args in the future without removing them
now.
This also improves type for `defaultValue`:
```ts
type SerializableValue = boolean | number | object | string
export type DefaultValue =
  | ((args: {
      locale?: TypedLocale
      req: PayloadRequest
      user: PayloadRequest['user']
    }) => SerializableValue)
  | SerializableValue
```

### Why?
To access the current URL / search params / Local API and other things
directly in `defaultValue`.

### How?
Passes `req` through everywhere where we call `defaultValue()`
2024-12-16 13:18:11 -05:00
Sasha
26a10ed071 perf: reduce generated types for select by respecting interfaceName (#9870)
This PR can significantly reduce your `payload-types.ts` file if you
have sharable fields / blocks that use the `interfaceName` property.
Previously we didn't respect it for select types.

Before:
```ts
export interface Collection1Select<T extends boolean = true> {
  testing?: T;
  title?: T;
  meta?:
    | T
    | {
        title?: T;
        description?: T;
        id?: T;
      };
  blocks?:
    | T
    | {
        block1?:
          | T
          | {
              b1title?: T;
              b1description?: T;
              id?: T;
              blockName?: T;
            };
        block2?:
          | T
          | {
              b2title?: T;
              b2description?: T;
              id?: T;
              blockName?: T;
            };
      };
  updatedAt?: T;
  createdAt?: T;
}
```
After:
```ts
export interface Collection1Select<T extends boolean = true> {
  testing?: T;
  title?: T;
  meta?: T | SharedMetaArraySelect<T>;
  blocks?:
    | T
    | {
        block1?: T | SharedMetaBlockSelect<T>;
        block2?: T | AnotherSharedBlockSelect<T>;
      };
  updatedAt?: T;
  createdAt?: T;
}

/**
 * This interface was referenced by `Config`'s JSON-Schema
 * via the `definition` "SharedMetaArray_select".
 */
export interface SharedMetaArraySelect<T extends boolean = true> {
  title?: T;
  description?: T;
  id?: T;
}
/**
 * This interface was referenced by `Config`'s JSON-Schema
 * via the `definition` "SharedMetaBlock_select".
 */
export interface SharedMetaBlockSelect<T extends boolean = true> {
  b1title?: T;
  b1description?: T;
  id?: T;
  blockName?: T;
}
/**
 * This interface was referenced by `Config`'s JSON-Schema
 * via the `definition` "AnotherSharedBlock_select".
 */
export interface AnotherSharedBlockSelect<T extends boolean = true> {
  b2title?: T;
  b2description?: T;
  id?: T;
  blockName?: T;
}
```

Regenerated all the types in `/test`. The diff is noticeable for
`fields` -
https://github.com/payloadcms/payload/pull/9870/files#diff-95beaac24c72c7bd60933e325cdcd94a4c3630a1ce22fabad624ec80cc74fc8c
2024-12-16 17:22:17 +02:00
Sasha
727fba7b1c refactor: deduplicate and abstract SQL schema building (#9987)
### What?
Abstracts SQL schema building, significantly reducing code duplication
for SQLite / Postgres

db-sqlite lines count From:
```sh
 wc -l **/*.ts
      62 src/connect.ts
      32 src/countDistinct.ts
       9 src/createJSONQuery/convertPathToJSONTraversal.ts
      86 src/createJSONQuery/index.ts
      15 src/defaultSnapshot.ts
       6 src/deleteWhere.ts
      21 src/dropDatabase.ts
      15 src/execute.ts
     178 src/index.ts
     139 src/init.ts
      19 src/insert.ts
      19 src/requireDrizzleKit.ts
     544 src/schema/build.ts
      27 src/schema/createIndex.ts
      38 src/schema/getIDColumn.ts
      13 src/schema/idToUUID.ts
      28 src/schema/setColumnID.ts
     787 src/schema/traverseFields.ts
      18 src/schema/withDefault.ts
     248 src/types.ts
    2304 total
```

To:
```sh
wc -l **/*.ts
      62 src/connect.ts
      32 src/countDistinct.ts
       9 src/createJSONQuery/convertPathToJSONTraversal.ts
      86 src/createJSONQuery/index.ts
      15 src/defaultSnapshot.ts
       6 src/deleteWhere.ts
      21 src/dropDatabase.ts
      15 src/execute.ts
     180 src/index.ts
      39 src/init.ts
      19 src/insert.ts
      19 src/requireDrizzleKit.ts
     149 src/schema/buildDrizzleTable.ts
      32 src/schema/setColumnID.ts
     258 src/types.ts
     942 total
```

Builds abstract schema in shared drizzle package that later gets
converted by SQLite/ Postgres specific implementation into drizzle
schema.
This, apparently can also help here
https://github.com/payloadcms/payload/pull/9953. It should be very
trivial to implement a MySQL adapter with this as well.
2024-12-16 17:17:18 +02:00
Sasha
c187bff581 fix: remove localized property from RowField and CollapsibleField (#9672)
### What?
Removes the `localized` property from typescript suggestion for row and
collapsible fields.

### Why?
Currently, this property doesn't do anything for them. This may be
changed when/if we support `name` for them, but it'll work only with
`name`.

Fixes https://github.com/payloadcms/payload/issues/4720
2024-12-16 09:57:21 -05:00
Sasha
00909ec5c4 fix(db-sqlite): working point field CRUD and default value (#9989)
Previously, the point field with SQLite was incorrectly built to the
schema and not parsed to the result.
Now, it works with SQLite, just doesn't support queries (`near` etc.).
Fixes
https://github.com/payloadcms/payload/pull/9987#discussion_r1885674299
2024-12-16 07:37:47 +02:00
Dan Ribbens
2ec4d0c2ef feat: join field admin.defaultColumns (#9982)
Add the ability to specify which columns should appear in the
relationship table of a join fields

The new property is in the Join field `admin.defaultColumns` and can be
set to an array of strings containing the field names in the desired
order.
2024-12-15 03:31:31 +00:00
Dan Ribbens
f5516b96da fix: edit join field not rendering (#9971)
In PR #9930 we added `overrideAccess: false` to the find operation and
failed to pass the user. This caused
https://github.com/payloadcms/payload/issues/9974 where any access
control causes the edit view to error.

The fix was to pass the user through.

This change also adds Join Field e2e tests to the CI pipeline which was
previously missing and would have caught the error.
2024-12-14 13:19:53 +00:00
Paul
050ff8409c templates: conditionally render the live preview listener component (#9973)
Conditionally render the live preview listener component so that we
don't make unnecessary requests to the API without draft mode being
enabled.
2024-12-13 21:18:13 +00:00
Jacob Fletcher
4dc50030b6 chore(deps): bumps react-select to v5.9.0 to surpress react 19 warnings (#9967)
When installing Payload, `react-select` currently throws a dependency
warning because `v5.8.0` does not include React 19 in its peer deps. As
of `v5.9.0`, it now does thanks to
https://github.com/JedWatson/react-select/pull/5984.
2024-12-13 20:25:21 +00:00
Elliot DeNolf
e073183ea8 ci: wait until version resolves in post-release-templates (#9968)
The post-release-templates workflow gets triggered whenever we create a
github release. It is fed the git tag. A script is then run to update
the templates' migrations and lockfile (if applicable).

There was a scenario where despite the packages already being published
to npm a few minutes prior, this process would error out saying that the
latest version was not available.

This PR adds a script that polls for 5 minutes against npm to wait for
the newly published version to resolve and match the git release tag.
2024-12-13 15:14:22 -05:00
Paul
c2adf38593 templates: fixes formatting issue with authors and footer not being at the bottom in the website template (#9969)
Fixes:
- formatting of authors when multiple, or missing or don't have names
- footer not sticking to the bottom of the page
2024-12-13 20:10:31 +00:00
Jarrod Flesch
36e21f182a feat(graphql): graphQL custom field complexity and validationRules (#9955)
### What?
Adds the ability to set custom validation rules on the root `graphQL`
config property and the ability to define custom complexity on
relationship, join and upload type fields.

### Why?
**Validation Rules**

These give you the option to add your own validation rules. For example,
you may want to prevent introspection queries in production. You can now
do that with the following:

```ts
import { GraphQL } from '@payloadcms/graphql/types'
import { buildConfig } from 'payload'

export default buildConfig({
  // ...
  graphQL: {
    validationRules: (args) => [
      NoProductionIntrospection
    ]
  },
  // ...
})

const NoProductionIntrospection: GraphQL.ValidationRule = (context) => ({
  Field(node) {
    if (process.env.NODE_ENV === 'production') {
      if (node.name.value === '__schema' || node.name.value === '__type') {
        context.reportError(
          new GraphQL.GraphQLError(
            'GraphQL introspection is not allowed, but the query contained __schema or __type',
            { nodes: [node] }
          )
        );
      }	
    }
  }
})
```

**Custom field complexity**

You can now increase the complexity of a field, this will help users
from running queries that are too expensive. A higher number will make
the `maxComplexity` trigger sooner.

```ts
const fieldWithComplexity = {
  name: 'authors',
  type: 'relationship',
  relationship: 'authors',
  graphQL: {
    complexity: 100, // highlight-line
  }
}
```
2024-12-13 15:03:57 -05:00
Alessio Gravili
c1673652a8 refactor(plugin-seo): strongly type collection and global slugs in plugin config (#9962) 2024-12-13 19:46:40 +00:00
Elliot DeNolf
1d6a9358d9 templates: bump for v3.7.0 (#9966)
🤖 Automated bump of templates for v3.7.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-13 19:31:52 +00:00
Alessio Gravili
f48f9810a0 fix: job collection was not added if job config only has workflows and no predefined tasks (#9963) 2024-12-13 19:30:38 +00:00
Jacob Fletcher
1502e09581 fix(ui): automatically subscribes custom fields to conditional logic (#9928)
Currently, custom components do not respect `admin.condition` unless
manually wrapped with the `withCondition` HOC, like all default fields
currently do. This should not be a requirement of component authors.
Instead, we can automatically detect custom client and server fields and
wrap them with the underlying `WatchCondition` component which will
subscribe to the `passesCondition` property within client-side form
state.

For my future self: there are potentially multiple instances where
fields subscribe to conditions duplicately, such as when rendering a
default Payload field within a custom field component. This was always a
problem and it is non-breaking, but needs to be reevaluated and removed
in the future for performance. Only the default fields that Payload
renders client-side need to subscribe to field conditions in this way.
When importing a Payload field into your custom field component, for
example, it should not include the HOC, because custom components now
watch conditions themselves.
2024-12-13 14:12:37 -05:00
Jacob Fletcher
33d5482e9d test: splits remaining field tests (#9959)
As field tests grow in size, they need to be moved out of the greater
fields test spec and into their own standalone files for readability,
maintainability, and speed. This way they we can write field tests in a
more isolated environment, and they can run in parallel in CI.
2024-12-13 18:49:18 +00:00
Elliot DeNolf
4c3e41beb1 chore(release): v3.7.0 [skip ci] 2024-12-13 12:06:46 -05:00
Jessica Chowdhury
50e7c24b17 fix(plugin-nested-docs): publishing parent doc should not publish child doc (#9958)
## Bug Fix

### Issue
Draft children documents get overwritten when the parent document is
published.

### Fix
Correctly retrieve all documents, including drafts, during the resave
process. Add test to ensure parent documents can be published without
impacting the state of any children docs.
2024-12-13 12:04:07 -05:00
Jacob Fletcher
796df37461 fix(ui): awaits form state before rendering conditional fields (#9933)
When a condition exists on a field and it resolves to `false`, it
currently "blinks" in and out when rendered within an array or block
row. This is because when add rows to form state, we iterate over the
_fields_ of that row and render their respective components. Then when
conditions are checked for that field, we're expecting `passesCondition`
to be explicitly `false`, ultimately _rendering_ the field for a brief
moment before form state returns with evaluated conditions. The fix is
to set these fields into local form state with a new `isLoading: true`
prop, then display a loader within the row until form state returns with
its proper conditions.
2024-12-13 16:42:52 +00:00
Elliot DeNolf
9c8cdea4b3 test: mock nodemailer network calls (#9957)
In tests, nodemailer was making network calls to create test accounts
using ethereal.email. This mocks them appropriately.
2024-12-13 11:31:24 -05:00
Elliot DeNolf
4334940755 chore(payload-cloud): improve error log of staticHandler (#9934)
Improve error logging of staticHandler.

Before:
`Error getting file from cloud storage`

After:
`Error getting file from cloud storage: some-file.png` and additional
params.
2024-12-13 11:10:41 -05:00
Sasha
b101feca7a feat: expose pagination: false to REST / GraphQL (#9952)
Exposes `pagination: false` to REST / GraphQL to improve performance on
large collections by avoiding count query.
This will also be nice for our SDK
https://github.com/payloadcms/payload/pull/9463 to have the same
properties.
2024-12-13 16:23:43 +02:00
Alessio Gravili
0d07ce22e8 feat: add waitUntil property to payload.jobs.queue (#9950) 2024-12-13 00:39:05 +00:00
Patrik
a582431a36 fix(ui): properly passes readOnly prop to email & username auth fields (#9938)
### What?

The `readOnly` prop was not being passed down to the `email` &
`username` auth fields.

Resulting in these fields not being disabled properly if `update` access
was restricted.

### How?

Passes the `readOnly` prop through to the fields and now properly
disables these fields if `update` access is restricted.
2024-12-12 17:40:23 -05:00
Patrik
7a8b46484b fix: updates username validation to allow for all special character usage (#9946)
### What?

Previously, only `.` & `-` special chars were allowed in usernames

### How?

Now - all special chars are accepted during username creating like `@`
2024-12-12 22:21:31 +00:00
Alessio Gravili
d57cad632d chore: use non-permanent / => /admin redirect so that the browser doesn't cache it for projects that don't redirect (#9948)
When opening payload in our monorepo and then working on a different
project that comes with a frontend, it will automatically redirect me
from localhost:3000 => localhost:3000/admin, not letting me view the
landing page until I clear my browser cache.

I'm hoping this will fix it
2024-12-12 21:58:21 +00:00
Alessio Gravili
7e3fd5d76c docs: fix mdx error (#9945) 2024-12-12 21:26:48 +00:00
Germán Jabloñski
abee24e1d0 docs: refactor and improve rich text docs (#9929)
- Refactoring that simplifies finding things:
    ```md
    ## BEFORE
    
    - Rich Text
      - Overview
      - Slate
      - Lexical
    - Lexical
      - Overview
      - Converters
      - Migration
      - Custom Features
    
    ## AFTER
    
    - Rich Text
      - Overview
      - Converters
      - Custom Features
      - Migration
      - Slate (legacy) 
    ```

- It takes some of the spotlight away from Slate. Lexical is assumed as
the default editor and a banner at the beginning refers to the Slate
documentation.
- Various writing improvements.

PENDING:
- [ ] some 301 redirects needed
  - `/docs/rich-text/lexical` to `/docs/rich-text/overview`
  - `/docs/lexical/overview` to `/docs/rich-text/overview`
  - `/docs/lexical/converters` to `/docs/rich-text/converters`
  - `/docs/lexical/migration` to `/docs/rich-text/migration`
2024-12-12 14:20:01 -07:00
Elliot DeNolf
0d8643a9a3 ci: update lint skip rules 2024-12-12 16:04:41 -05:00
Alessio Gravili
d78550c561 templates: fix build by reducing strictness of eslint rules (#9943) 2024-12-12 20:54:56 +00:00
Alessio Gravili
c7272bb2bf templates: set up prettier, bump next to 15.1.0, bump eslint to v9, set up .vscode configs (#9936) 2024-12-12 15:29:51 -05:00
Nate
9eb1b508f6 chore: update what-is-payload.mdx (#9942)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

update YouTube "What is Payload?" video

### Why?

Reflect 3.0 changes.

### How?

Fixes #

-->
2024-12-12 15:19:42 -05:00
Germán Jabloñski
6fffbdb27a chore(translations): improve password reset message (#9931)
It is still not indicated whether the email exists or not for security
reasons, but:
1. It is indicated that it is a possibility (if the email exists, the
email will be sent).
2. The user is advised to check the spam and junk mail folders.
2024-12-12 20:19:15 +00:00
Sasha
c298cbc90d chore: deflake postgres and sqlite integration joins test (#9939)
`relationTo` was specified incorrectly which led to 
```
  ● Joins Field › rEST API should not populate individual join by providing schemaPath=false

    error: insert or update on table "collection_restricted" violates foreign key constraint "collection_restricted_category_id_restricted_categories_id_fk"

      18 |       .returning()
      19 |   } else {
    > 20 |     result = await (db as TransactionPg).insert(table).values(values).returning()
```
2024-12-12 21:49:32 +02:00
Dan Ribbens
5af71fb8d0 fix: join collection read access (#9930)
Respect read access control through the join field collections for GraphQL and admin UI

fixes #9922 and #9865
2024-12-12 12:01:03 -05:00
Alessio Gravili
d4d79c1141 feat: allow loading predefined migrations from proper exports (#9872)
Currently, predefined migrations can only be loaded if they are part of
one of our db adapters.

With this PR, plugins will be able to export their own predefined
migrations that can be created like this:

`pnpm payload migrate:create --file
@payloadcms/plugin-someplugin/someMigration`

with the plugin exporting it in their package.json:

```json
  "exports": {
    "./someMigration": {
      "import": "./someMigration.mjs",
      "types": "./someMigration.mjs",
      "default": "./someMigration.mjs"
    }
  },
```
2024-12-12 09:39:20 -07:00
Germán Jabloñski
9d324ff207 docs: update Lexical to JSX documentation for RichText component (#9926) 2024-12-12 13:21:59 +00:00
Alessio Gravili
fffab668c9 chore: export JobLog and importHandlerPath, add missing id type to JobLog (#9921)
Requirement for https://github.com/payloadcms/payload/pull/9645
2024-12-12 06:58:47 +00:00
Alessio Gravili
bae2fe535e feat(ui): allow customizing min height of code editor (#9920)
Requirement for https://github.com/payloadcms/payload/pull/9645.

Dynamic code field resizing currently is broken for line-breaks - need
to address that in a future PR.
2024-12-12 06:31:39 +00:00
Alessio Gravili
c8046cade7 chore(ui): export SearchFilter, EditUpload, FileDetails, PreviewSizes, PreviewButton, ErrorIcon, InfoIcon, SuccessIcon, WarningIcon (#9919)
Fixes https://github.com/payloadcms/payload/issues/9794
2024-12-12 06:23:26 +00:00
Sasha
5e3963482e fix(db-postgres): payload.db.upsert inserts new rows instead of updating existing ones (#9916)
### What?
`payload.db.updateOne` (and so `payload.db.upsert`) with drizzle
adapters used incoming `where` incorrectly and worked properly only
either if you passed `id` or some where query path required table joins
(like `where: { 'array.title'`) which is also the reason why `upsert`
_worked_ with user preferences specifically, because we need to join the
`preferences_rels` table to query by `user.relationTo` and `user.value`

Fixes https://github.com/payloadcms/payload/issues/9915

This was found here - https://github.com/payloadcms/payload/pull/9913,
the database KV adapter uses `upsert` with `where` by unique fields.
2024-12-12 03:53:57 +02:00
Patrik
d9efd192e7 fix(ui): truncates richtext fields when displaying within a joins field (#9911)
### What?

Previously, `richtext` fields were not properly truncated when displayed
in a `joins` field.

`Before`:

![Screenshot 2024-12-11 at 3 42
13 PM](https://github.com/user-attachments/assets/a6f44248-8cb9-4a38-a74c-eaa88b739601)

### How?

Now - we've added proper css styling to truncate extended `richtext`
elements inside a `joins` table.

`After`:

![Screenshot 2024-12-11 at 3 41
53 PM](https://github.com/user-attachments/assets/ae846c50-2e29-411f-b056-20e4caf3ef20)
2024-12-11 17:03:27 -05:00
Paul
23e2f7bc9e templates: change names of data variables to improve clarity between data and elements (#9912)
Renames `header` to `headerData` and other props to `data` so that it's
clearer for people learning Nextjs or React.
2024-12-11 15:42:10 -06:00
Jacob Fletcher
4c57df69ca refactor(ui): migrates away from React.forwardRef (#9907)
As of React 19, `ref` can now be accessed directly from props and no
longer require the use of `React.forwardRef`.
2024-12-11 21:09:05 +00:00
Paul
6a09fe1bf9 templates: fix preview path protocol not being https in production environments (#9910) 2024-12-11 20:52:07 +00:00
Marwin Hormiz
821bd35578 fix(translations): capitalized swedish 'collapseAll' translation (#9908)
Co-authored-by: Marwin Hormiz <marwinhormiz@duobit.se>
2024-12-11 20:42:40 +00:00
Paul
afa08d0ebf templates: fix live preview relative URLs on website template (#9906) 2024-12-11 20:11:24 +00:00
Paul
d97d7eda37 templates: bump nextjs version to 15.1 (#9903) 2024-12-11 19:22:10 +00:00
726 changed files with 24995 additions and 15596 deletions

View File

@@ -62,8 +62,12 @@ jobs:
echo "templates: ${{ steps.filter.outputs.templates }}"
lint:
# Follows same github's ci skip: [skip lint], [lint skip], [no lint]
if: >
github.event_name == 'pull_request' && !contains(github.event.pull_request.title, 'no-lint') && !contains(github.event.pull_request.title, 'skip-lint')
github.event_name == 'pull_request' &&
!contains(github.event.pull_request.title, '[skip lint]') &&
!contains(github.event.pull_request.title, '[lint skip]') &&
!contains(github.event.pull_request.title, '[no lint]')
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -176,6 +180,7 @@ jobs:
- postgres-uuid
- supabase
- sqlite
- sqlite-uuid
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@@ -279,20 +284,31 @@ jobs:
- admin-root
- auth
- auth-basic
- joins
- field-error-states
- fields-relationship
- fields
- fields__collections__Blocks
- fields__collections__Array
- fields__collections__Relationship
- fields__collections__RichText
- fields__collections__Blocks
- fields__collections__Collapsible
- fields__collections__ConditionalLogic
- fields__collections__CustomID
- fields__collections__Date
- fields__collections__Email
- fields__collections__Indexed
- fields__collections__JSON
- fields__collections__Lexical__e2e__main
- fields__collections__Lexical__e2e__blocks
- fields__collections__Date
- fields__collections__Number
- fields__collections__Point
- fields__collections__Radio
- fields__collections__Relationship
- fields__collections__RichText
- fields__collections__Row
- fields__collections__Select
- fields__collections__Tabs
- fields__collections__Tabs2
- fields__collections__Text
- fields__collections__UI
- fields__collections__Upload
- live-preview
- localization

View File

@@ -13,7 +13,39 @@ env:
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
jobs:
wait_for_release:
runs-on: ubuntu-24.04
outputs:
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
sparse-checkout: .github/workflows
- name: Determine Release Tag
id: determine_tag
run: |
if [ "${{ github.event_name }}" == "release" ]; then
echo "Using tag from release event: ${{ github.event.release.tag_name }}"
echo "release_tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
else
# pull latest tag from github, must match any version except v2. Should match v3, v4, v99, etc.
echo "Fetching latest tag from github..."
LATEST_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude 'v2*')
echo "Latest tag: $LATEST_TAG"
echo "release_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
fi
- name: Wait until latest versions resolve on npm registry
run: |
./.github/workflows/wait-until-package-version.sh payload ${{ steps.determine_tag.outputs.release_tag }}
./.github/workflows/wait-until-package-version.sh @payloadcms/translations ${{ steps.determine_tag.outputs.release_tag }}
./.github/workflows/wait-until-package-version.sh @payloadcms/next ${{ steps.determine_tag.outputs.release_tag }}
update_templates:
needs: wait_for_release
runs-on: ubuntu-24.04
permissions:
contents: write
@@ -25,8 +57,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Needed for tags
- name: Setup
uses: ./.github/actions/setup
@@ -60,20 +90,6 @@ jobs:
- name: Update template lockfiles and migrations
run: pnpm script:gen-templates
- name: Determine Release Tag
id: determine_tag
run: |
if [ "${{ github.event_name }}" == "release" ]; then
echo "Using tag from release event: ${{ github.event.release.tag_name }}"
echo "release_tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
else
# pull latest tag from github, must match any version except v2. Should match v3, v4, v99, etc.
echo "Fetching latest tag from github..."
LATEST_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude 'v2*')
echo "Latest tag: $LATEST_TAG"
echo "release_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
fi
- name: Commit and push changes
id: commit
env:
@@ -85,7 +101,7 @@ jobs:
git diff --name-only
export BRANCH_NAME=templates/bump-${{ steps.determine_tag.outputs.release_tag }}-$(date +%s)
export BRANCH_NAME=templates/bump-${{ needs.wait_for_release.outputs.release_tag }}-$(date +%s)
echo "branch=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
- name: Create pull request
@@ -94,12 +110,12 @@ jobs:
token: ${{ secrets.GH_TOKEN_POST_RELEASE_TEMPLATES }}
labels: 'area: templates'
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
commit-message: 'templates: bump templates for ${{ steps.determine_tag.outputs.release_tag }}'
commit-message: 'templates: bump templates for ${{ needs.wait_for_release.outputs.release_tag }}'
branch: ${{ steps.commit.outputs.branch }}
base: main
assignees: ${{ github.actor }}
title: 'templates: bump for ${{ steps.determine_tag.outputs.release_tag }}'
title: 'templates: bump for ${{ needs.wait_for_release.outputs.release_tag }}'
body: |
🤖 Automated bump of templates for ${{ steps.determine_tag.outputs.release_tag }}
🤖 Automated bump of templates for ${{ needs.wait_for_release.outputs.release_tag }}
Triggered by user: @${{ github.actor }}

View File

@@ -41,7 +41,9 @@ jobs:
db-vercel-postgres
db-sqlite
drizzle
email-\*
email-nodemailer
email-resend
eslint
graphql
live-preview

View File

@@ -0,0 +1,31 @@
#!/bin/bash
if [[ "$#" -ne 2 ]]; then
echo "Usage: $0 <package-name> <version>"
exit 1
fi
PACKAGE_NAME="$1"
TARGET_VERSION=${2#v} # Git tag has leading 'v', npm version does not
TIMEOUT=300 # 5 minutes in seconds
INTERVAL=10 # 10 seconds
ELAPSED=0
echo "Waiting for version ${TARGET_VERSION} of '${PACKAGE_NAME}' to resolve... (timeout: ${TIMEOUT} seconds)"
while [[ ${ELAPSED} -lt ${TIMEOUT} ]]; do
latest_version=$(npm show "${PACKAGE_NAME}" version 2>/dev/null)
if [[ ${latest_version} == "${TARGET_VERSION}" ]]; then
echo "SUCCCESS: Version ${TARGET_VERSION} of ${PACKAGE_NAME} is available."
exit 0
else
echo "Version ${TARGET_VERSION} of ${PACKAGE_NAME} is not available yet. Retrying in ${INTERVAL} seconds... (elapsed: ${ELAPSED}s)"
fi
sleep "${INTERVAL}"
ELAPSED=$((ELAPSED + INTERVAL))
done
echo "Timed out after ${TIMEOUT} seconds waiting for version ${TARGET_VERSION} of '${PACKAGE_NAME}' to resolve."
exit 1

View File

@@ -98,7 +98,7 @@ The following options are available:
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| **`meta`** | Base metadata to use for the Admin Panel. [More details](./metadata). |
| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
| **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root <html> tag. Defaults to `false`. |
| **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `<html>` tag. Defaults to `false`. |
| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. |
| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |

View File

@@ -45,7 +45,7 @@ import type { CollectionConfig } from 'payload'
export const UsersWithoutJWTs: CollectionConfig = {
slug: 'users-without-jwts',
auth: {
removeTokenFromRepsonse: true, // highlight-line
removeTokenFromResponse: true, // highlight-line
},
}
```

View File

@@ -30,14 +30,15 @@ export default buildConfig({
## Options
| Option | Description |
| -------------------- | ----------- |
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
| `migrationDir` | Customize the directory that migrations are stored. |
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
| Option | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
| `collectionsSchemaOptions` | Customize Mongoose schema options for collections. |
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
| `migrationDir` | Customize the directory that migrations are stored. |
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
## Access to Mongoose models

View File

@@ -50,6 +50,11 @@ export default buildConfig({
})
```
<Banner type="info">
<strong>Note:</strong>
If when using `vercelPostgresAdapter` your `process.env.POSTGRES_URL` or `pool.connectionString` points to a local database (e.g hostname has `localhost` or `127.0.0.1`) we use the `pg` module for pooling instead of `@vercel/postgres`. This is because `@vercel/postgres` doesn't work with local databases, if you want to disable that behavior, you can pass `forceUseVercelPostgres: true` to adapter's 'args and follow [Vercel guide](https://vercel.com/docs/storage/vercel-postgres/local-development#option-2:-local-postgres-instance-with-docker) for a Docker Neon DB setup.
</Banner>
## Options
| Option | Description |
@@ -60,21 +65,33 @@ export default buildConfig({
| `schemaName` (experimental) | A string for the postgres schema to use, defaults to 'public'. |
| `idType` | A string of 'serial', or 'uuid' that is used for the data type given to id columns. |
| `transactionOptions` | A PgTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) |
| `disableCreateDatabase` | Pass `true` to disable auto database creation if it doesn't exist. Defaults to `false`. |
| `disableCreateDatabase` | Pass `true` to disable auto database creation if it doesn't exist. Defaults to `false`. |
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
## Access to Drizzle
After Payload is initialized, this adapter will expose the full power of Drizzle to you for use if you need it.
You can access Drizzle as follows:
To ensure type-safety, you need to generate Drizzle schema first with:
```sh
npx payload generate:db-schema
```
```text
payload.db.drizzle
Then, you can access Drizzle as follows:
```ts
import { posts } from './payload-generated-schema'
// To avoid installing Drizzle, you can import everything that drizzle has from our re-export path.
import { eq, sql, and } from '@payloadcms/db-postgres/drizzle'
// Drizzle's Querying API: https://orm.drizzle.team/docs/rqb
const posts = await payload.db.drizzle.query.posts.findMany()
// Drizzle's Select API https://orm.drizzle.team/docs/select
const result = await payload.db.drizzle.select().from(posts).where(and(eq(posts.id, 50), sql`lower(${posts.title}) = 'example post title'`))
```
## Tables, relations, and enums
@@ -109,7 +126,7 @@ Runs before the schema is built. You can use this hook to extend your database s
```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
import { integer, pgTable, serial } from 'drizzle-orm/pg-core'
import { integer, pgTable, serial } from '@payloadcms/db-postgres/drizzle/pg-core'
postgresAdapter({
beforeSchemaInit: [
@@ -178,7 +195,7 @@ postgresAdapter({
})
```
Make sure Payload doesn't overlap table names with its collections. For example, if you already have a collection with slug "users", you should either change the slug or `dbName` to change the table name for this collection.
Make sure Payload doesn't overlap table names with its collections. For example, if you already have a collection with slug "users", you should either change the slug or `dbName` to change the table name for this collection.
### afterSchemaInit
@@ -189,7 +206,7 @@ The following example adds the `extra_integer_column` column and a composite ind
```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
import { index, integer } from 'drizzle-orm/pg-core'
import { index, integer } from '@payloadcms/db-postgres/drizzle/pg-core'
import { buildConfig } from 'payload'
export default buildConfig({
@@ -231,3 +248,43 @@ export default buildConfig({
})
```
### Note for generated schema:
Columns and tables, added in schema hooks won't be added to the generated via `payload generate:db-schema` Drizzle schema.
If you want them to be there, you either have to edit this file manually or mutate the internal Payload "raw" SQL schema in the `beforeSchemaInit`:
```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
postgresAdapter({
beforeSchemaInit: [
({ schema, adapter }) => {
// Add a new table
schema.rawTables.myTable = {
name: 'my_table',
columns: [{
name: 'my_id',
type: 'serial',
primaryKey: true
}],
}
// Add a new column to generated by Payload table:
schema.rawTables.posts.columns.customColumn = {
name: 'custom_column',
// Note that Payload SQL doesn't support everything that Drizzle does.
type: 'integer',
notNull: true
}
// Add a new index to generated by Payload table:
schema.rawTables.posts.indexes.customColumnIdx = {
name: 'custom_column_idx',
unique: true,
on: ['custom_column']
}
return schema
},
],
})
```

View File

@@ -34,27 +34,41 @@ export default buildConfig({
## Options
| Option | Description |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client` \* | [Client connection options](https://orm.drizzle.team/docs/get-started-sqlite#turso) that will be passed to `createClient` from `@libsql/client`. |
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
| `migrationDir` | Customize the directory that migrations are stored. |
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
| `transactionOptions` | A SQLiteTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) |
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
| Option | Description |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client` \* | [Client connection options](https://orm.drizzle.team/docs/get-started-sqlite#turso) that will be passed to `createClient` from `@libsql/client`. |
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
| `migrationDir` | Customize the directory that migrations are stored. |
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
| `idType` | A string of 'number', or 'uuid' that is used for the data type given to id columns. |
| `transactionOptions` | A SQLiteTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) |
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
## Access to Drizzle
After Payload is initialized, this adapter will expose the full power of Drizzle to you for use if you need it.
You can access Drizzle as follows:
To ensure type-safety, you need to generate Drizzle schema first with:
```sh
npx payload generate:db-schema
```
```text
payload.db.drizzle
Then, you can access Drizzle as follows:
```ts
// Import table from the generated file
import { posts } from './payload-generated-schema'
// To avoid installing Drizzle, you can import everything that drizzle has from our re-export path.
import { eq, sql, and } from '@payloadcms/db-sqlite/drizzle'
// Drizzle's Querying API: https://orm.drizzle.team/docs/rqb
const posts = await payload.db.drizzle.query.posts.findMany()
// Drizzle's Select API https://orm.drizzle.team/docs/select
const result = await payload.db.drizzle.select().from(posts).where(and(eq(posts.id, 50), sql`lower(${posts.title}) = 'example post title'`))
```
## Tables and relations
@@ -88,7 +102,7 @@ Runs before the schema is built. You can use this hook to extend your database s
```ts
import { sqliteAdapter } from '@payloadcms/db-sqlite'
import { integer, sqliteTable } from 'drizzle-orm/sqlite-core'
import { integer, sqliteTable } from '@payloadcms/db-sqlite/drizzle/sqlite-core'
sqliteAdapter({
beforeSchemaInit: [
@@ -168,7 +182,7 @@ The following example adds the `extra_integer_column` column and a composite ind
```ts
import { sqliteAdapter } from '@payloadcms/db-sqlite'
import { index, integer } from 'drizzle-orm/sqlite-core'
import { index, integer } from '@payloadcms/db-sqlite/drizzle/sqlite-core'
import { buildConfig } from 'payload'
export default buildConfig({
@@ -210,3 +224,43 @@ export default buildConfig({
})
```
### Note for generated schema:
Columns and tables, added in schema hooks won't be added to the generated via `payload generate:db-schema` Drizzle schema.
If you want them to be there, you either have to edit this file manually or mutate the internal Payload "raw" SQL schema in the `beforeSchemaInit`:
```ts
import { sqliteAdapter } from '@payloadcms/db-sqlite'
sqliteAdapter({
beforeSchemaInit: [
({ schema, adapter }) => {
// Add a new table
schema.rawTables.myTable = {
name: 'my_table',
columns: [{
name: 'my_id',
type: 'integer',
primaryKey: true
}],
}
// Add a new column to generated by Payload table:
schema.rawTables.posts.columns.customColumn = {
name: 'custom_column',
// Note that Payload SQL doesn't support everything that Drizzle does.
type: 'integer',
notNull: true
}
// Add a new index to generated by Payload table:
schema.rawTables.posts.indexes.customColumnIdx = {
name: 'custom_column_idx',
unique: true,
on: ['custom_column']
}
return schema
},
],
})
```

View File

@@ -125,8 +125,8 @@ powerful Admin UI.
|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** \* | To be used as the property name when retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`collection`** \* | The `slug`s having the relationship field. |
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
| **`where`** \* | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
| **`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](/docs/getting-started/concepts#field-level-max-depth). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
@@ -136,6 +136,7 @@ powerful Admin UI.
| **`admin`** | Admin-specific configuration. [More details](#admin-config-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema. |
| **`graphQL`** | Custom graphQL configuration for the field. [More details](/docs/graphql/overview#field-complexity) |
_\* An asterisk denotes that a property is required._
@@ -144,10 +145,11 @@ _\* An asterisk denotes that a property is required._
You can control the user experience of the join field using the `admin` config properties. The following options are supported:
| Option | Description |
|------------------------|----------------------------------------------------------------------------------------|
| **`allowCreate`** | Set to `false` to remove the controls for making new related documents from this field. |
| **`components.Label`** | Override the default Label of the Field Component. [More details](../admin/fields#label) |
| Option | Description |
|------------------------|---------------------------------------------------------------------------------------------------------------------------|
| **`defaultColumns`** | Array of field names that correspond to which columns to show in the relationship table. Default is the collection config. |
| **`allowCreate`** | Set to `false` to remove the controls for making new related documents from this field. |
| **`components.Label`** | Override the default Label of the Field Component. [More details](../admin/fields#label) |
## Join Field Data

View File

@@ -212,6 +212,7 @@ Functions can be written to make use of the following argument properties:
- `user` - the authenticated user object
- `locale` - the currently selected locale string
- `req` - the `PayloadRequest` object
Here is an example of a `defaultValue` function:
@@ -227,7 +228,7 @@ export const myField: Field = {
name: 'attribution',
type: 'text',
// highlight-start
defaultValue: ({ user, locale }) =>
defaultValue: ({ user, locale, req }) =>
`${translation[locale]} ${user.name}`,
// highlight-end
}
@@ -235,7 +236,7 @@ export const myField: Field = {
<Banner type="success">
<strong>Tip:</strong>
You can use async `defaultValue` functions to fill fields with data from API requests.
You can use async `defaultValue` functions to fill fields with data from API requests or Local API using `req.payload`.
</Banner>
### Validation

View File

@@ -61,6 +61,7 @@ export const MyRelationshipField: Field = {
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
| **`graphQL`** | Custom graphQL configuration for the field. [More details](/docs/graphql/overview#field-complexity) |
_\* An asterisk denotes that a property is required._

View File

@@ -6,7 +6,13 @@ desc: The Rich Text field allows dynamic content to be written through the Admin
keywords: rich text, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
The Rich Text Field is a powerful way to allow editors to write dynamic content. The content is saved as JSON in the database and can be converted into any format, including HTML, that you need.
The Rich Text Field lets editors write and format dynamic content in a familiar interface.
The content is saved as JSON in the database and can be converted to HTML or any other format needed.
Consistent with Payload's goal of making you learn as little of Payload as possible, customizing
and using the Rich Text Editor does not involve learning how to develop for a Payload rich text editor.
Instead, you can invest your time and effort into learning the underlying open-source tools that will allow
you to apply your learnings elsewhere as well.
<LightDarkImage
srcLight="https://payloadcms.com/images/docs/fields/richtext.png"
@@ -15,23 +21,6 @@ The Rich Text Field is a powerful way to allow editors to write dynamic content.
caption="Admin Panel screenshot of a Rich Text field"
/>
Payload's rich text field is built on an "adapter pattern" which lets you specify which rich text editor you'd like to use.
Right now, Payload is officially supporting two rich text editors:
1. [SlateJS](/docs/rich-text/slate) - legacy, backwards-compatible with 1.0
2. [Lexical](/docs/lexical/overview) - recommended
<Banner type="success">
<strong>
Consistent with Payload's goal of making you learn as little of Payload as possible, customizing
and using the Rich Text Editor does not involve learning how to develop for a{' '}<em>Payload</em>{' '}rich text editor.
</strong>
Instead, you can invest your time and effort into learning the underlying open-source tools that
will allow you to apply your learnings elsewhere as well.
</Banner>
## Config Options
| Option | Description |
@@ -47,7 +36,7 @@ Right now, Payload is officially supporting two rich text editors:
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`editor`** | Override the rich text editor specified in your base configuration for this field. |
| **`editor`** | Customize or override the rich text editor. [More details](/docs/rich-text/overview). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
@@ -79,4 +68,5 @@ The Rich Text Field inherits all of the default options from the base [Field Adm
## Editor-specific Options
For a ton more editor-specific options, including how to build custom rich text elements directly into your editor, take a look at either the [Slate docs](/docs/rich-text/slate) or the [Lexical docs](/docs/lexical/overview) depending on which editor you're using.
For a ton more editor-specific options, including how to build custom rich text elements directly into your editor,
take a look at the [rich text editor documentation](/docs/rich-text/overview).

View File

@@ -68,11 +68,12 @@ export const MyTextareaField: Field = {
The Textarea Field inherits all of the default options from the base [Field Admin Config](../admin/fields#admin-options), plus the following additional options:
| Option | Description |
| -------------- | ---------------------------------------------------------------------------------------------------------------- |
| **`placeholder`** | Set this property to define a placeholder string in the textarea. |
| **`autoComplete`** | Set this property to a string that will be used for browser autocomplete. |
| **`rtl`** | Override the default text direction of the Admin Panel for this field. Set to `true` to force right-to-left text direction. |
| Option | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| **`placeholder`** | Set this property to define a placeholder string in the textarea. |
| **`autoComplete`** | Set this property to a string that will be used for browser autocomplete. |
| **`rows`** | Set the number of visible text rows in the textarea. Defaults to `2` if not specified. |
| **`rtl`** | Override the default text direction of the Admin Panel for this field. Set to `true` to force right-to-left text direction. |
## Example

View File

@@ -68,6 +68,7 @@ export const MyUploadField: Field = {
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
| **`graphQL`** | Custom graphQL configuration for the field. [More details](/docs/graphql/overview#field-complexity) |
_\* An asterisk denotes that a property is required._

View File

@@ -7,8 +7,8 @@ keywords: documentation, getting started, guide, Content Management System, cms,
---
<YouTube
id="In_lFhzmbME"
title="Payload Introduction - Closing the Gap Between Headless CMS and Application Frameworks"
id="ftohATkHBi0"
title="Introduction to Payload — The open-source Next.js backend"
/>
**Payload is the Next.js fullstack framework.** Write a Payload Config and instantly get:

View File

@@ -23,6 +23,7 @@ At the top of your Payload Config you can define all the options to manage Graph
| `maxComplexity` | A number used to set the maximum allowed complexity allowed by requests [More](/docs/graphql/overview#query-complexity-limits) |
| `disablePlaygroundInProduction` | A boolean that if false will enable the GraphQL playground, defaults to true. [More](/docs/graphql/overview#graphql-playground) |
| `disable` | A boolean that if true will disable the GraphQL entirely, defaults to false. |
| `validationRules` | A function that takes the ExecutionArgs and returns an array of ValidationRules. |
## Collections
@@ -124,6 +125,55 @@ You can even log in using the `login[collection-singular-label-here]` mutation t
see a ton of detail about how GraphQL operates within Payload.
</Banner>
## Custom Validation Rules
You can add custom validation rules to your GraphQL API by defining a `validationRules` function in your Payload Config. This function should return an array of [Validation Rules](https://graphql.org/graphql-js/validation/#validation-rules) that will be applied to all incoming queries and mutations.
```ts
import { GraphQL } from '@payloadcms/graphql/types'
import { buildConfig } from 'payload'
export default buildConfig({
// ...
graphQL: {
validationRules: (args) => [
NoProductionIntrospection
]
},
// ...
})
const NoProductionIntrospection: GraphQL.ValidationRule = (context) => ({
Field(node) {
if (process.env.NODE_ENV === 'production') {
if (node.name.value === '__schema' || node.name.value === '__type') {
context.reportError(
new GraphQL.GraphQLError(
'GraphQL introspection is not allowed, but the query contained __schema or __type',
{ nodes: [node] }
)
);
}
}
}
})
```
## Query complexity limits
Payload comes with a built-in query complexity limiter to prevent bad people from trying to slow down your server by running massive queries. To learn more, [click here](/docs/production/preventing-abuse#limiting-graphql-complexity).
## Field complexity
You can define custom complexity for `relationship`, `upload` and `join` type fields. This is useful if you want to assign a higher complexity to a field that is more expensive to resolve. This can help prevent users from running queries that are too complex.
```ts
const fieldWithComplexity = {
name: 'authors',
type: 'relationship',
relationship: 'authors',
graphQL: {
complexity: 100, // highlight-line
}
}
```

View File

@@ -141,3 +141,65 @@ export const createPostHandler: TaskHandler<'createPost'> = async ({ input, job,
}
}
```
### Configuring task restoration
By default, if a task has passed previously and a workflow is re-run, the task will not be re-run. Instead, the output from the previous task run will be returned. This is to prevent unnecessary re-runs of tasks that have already passed.
You can configure this behavior through the `retries.shouldRestore` property. This property accepts a boolean or a function.
If `shouldRestore` is set to true, the task will only be re-run if it previously failed. This is the default behavior.
If `shouldRestore` this is set to false, the task will be re-run even if it previously succeeded, ignoring the maximum number of retries.
If `shouldRestore` is a function, the return value of the function will determine whether the task should be re-run. This can be used for more complex restore logic, e.g you may want to re-run a task up to X amount of times and then restore it for consecutive runs, or only re-run a task if the input has changed.
Example:
```ts
export default buildConfig({
// ...
jobs: {
tasks: [
{
slug: 'myTask',
retries: {
shouldRestore: false,
}
// ...
} as TaskConfig<'myTask'>,
]
}
})
```
Example - determine whether a task should be restored based on the input data:
```ts
export default buildConfig({
// ...
jobs: {
tasks: [
{
slug: 'myTask',
inputSchema: [
{
name: 'someDate',
type: 'date',
required: true,
},
],
retries: {
shouldRestore: ({ input }) => {
if(new Date(input.someDate) > new Date()) {
return false
}
return true
},
}
// ...
} as TaskConfig<'myTask'>,
]
}
})
```

View File

@@ -1,292 +0,0 @@
---
title: Lexical Overview
label: Overview
order: 10
desc: Built by Meta, Lexical is an incredibly powerful rich text editor, and it works beautifully within Payload.
keywords: lexical, rich text, editor, headless cms
---
One of Payload's goals is to build the best rich text editor experience that we possibly can. We want to combine the beauty and polish of the Medium editing experience with the strength and features of the Notion editor - all in one place.
Classically, we've used SlateJS to work toward this goal, but building custom elements into Slate has proven to be more difficult than we'd like, and we've been keeping our options open.
Lexical is extremely impressive and trivializes a lot of the hard parts of building new elements into a rich text editor. It has a few distinct advantages over Slate, including the following:
1. A "/" menu, which allows editors to easily add new elements while never leaving their keyboard
1. A "hover" toolbar that pops up if you select text
1. It supports Payload blocks natively, directly within your rich text editor
1. Custom elements, called "features", are much easier to build in Lexical vs. Slate
To use the Lexical editor, first you need to install it:
```
npm install @payloadcms/richtext-lexical
```
Once you have it installed, you can pass it to your top-level Payload Config as follows:
```ts
import { buildConfig } from 'payload'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
export default buildConfig({
collections: [
// your collections here
],
// Pass the Lexical editor to the root config
editor: lexicalEditor({}),
})
```
You can also override Lexical settings on a field-by-field basis as follows:
```ts
import type { CollectionConfig } from 'payload'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
export const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{
name: 'content',
type: 'richText',
// Pass the Lexical editor here and override base settings as necessary
editor: lexicalEditor({}),
},
],
}
```
## Extending the lexical editor with Features
Lexical has been designed with extensibility in mind. Whether you're aiming to introduce new functionalities or tweak the existing ones, Lexical makes it seamless for you to bring those changes to life.
### Features: The Building Blocks
At the heart of Lexical's customization potential are "features". While Lexical ships with a set of default features we believe are essential for most use cases, the true power lies in your ability to redefine, expand, or prune these as needed.
If you remove all the default features, you're left with a blank editor. You can then add in only the features you need, or you can build your own custom features from scratch.
### Integrating New Features
To weave in your custom features, utilize the `features` prop when initializing the Lexical Editor. Here's a basic example of how this is done:
```ts
import {
BlocksFeature,
LinkFeature,
UploadFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
import { Banner } from '../blocks/Banner'
import { CallToAction } from '../blocks/CallToAction'
{
editor: lexicalEditor({
features: ({ defaultFeatures, rootFeatures }) => [
...defaultFeatures,
LinkFeature({
// Example showing how to customize the built-in fields
// of the Link feature
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
},
},
],
}),
UploadFeature({
collections: {
uploads: {
// Example showing how to customize the built-in fields
// of the Upload feature
fields: [
{
name: 'caption',
type: 'richText',
editor: lexicalEditor(),
},
],
},
},
}),
// This is incredibly powerful. You can re-use your Payload blocks
// directly in the Lexical editor as follows:
BlocksFeature({
blocks: [Banner, CallToAction],
}),
],
})
}
```
`features` can be both an array of features, or a function returning an array of features. The function provides the following props:
| Prop | Description |
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`defaultFeatures`** | This opinionated array contains all "recommended" default features. You can see which features are included in the default features in the table below. |
| **`rootFeatures`** | This array contains all features that are enabled in the root richText editor (the one defined in the payload.config.ts). If this field is the root richText editor, or if the root richText editor is not a lexical editor, this array will be empty. |
## Features overview
Here's an overview of all the included features:
| Feature Name | Included by default | Description |
|---------------------------------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`BoldTextFeature`** | Yes | Handles the bold text format |
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
| **`StrikethroughTextFeature`** | Yes | Handles the strikethrough text format |
| **`SubscriptTextFeature`** | Yes | Handles the subscript text format |
| **`SuperscriptTextFeature`** | Yes | Handles the superscript text format |
| **`InlineCodeTextFeature`** | Yes | Handles the inline-code text format |
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
| **`CheckListFeature`** | Yes | Adds checklists |
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
| **`BlockQuoteFeature`** | Yes | Allows you to create block-level quotes |
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
Notice how even the toolbars are features? That's how extensible our lexical editor is - you could theoretically create your own toolbar if you wanted to!
## Creating your own, custom Feature
You can find more information about creating your own feature in our [building custom feature docs](/docs/lexical/building-custom-features).
## TypeScript
Every single piece of saved data is 100% fully-typed within lexical. It provides a type for every single node, which can be imported from `@payloadcms/richtext-lexical` - each type is prefixed with `Serialized`, e.g. `SerializedUploadNode`.
In order to fully type the entire editor JSON, you can use our `TypedEditorState` helper type, which accepts a union of all possible node types as a generic. The reason we do not provide a type which already contains all possible node types is because the possible node types depend on which features you have enabled in your editor. Here is an example:
```ts
import type {
SerializedAutoLinkNode,
SerializedBlockNode,
SerializedHorizontalRuleNode,
SerializedLinkNode,
SerializedListItemNode,
SerializedListNode,
SerializedParagraphNode,
SerializedQuoteNode,
SerializedRelationshipNode,
SerializedTextNode,
SerializedUploadNode,
TypedEditorState,
SerializedHeadingNode,
} from '@payloadcms/richtext-lexical'
const editorState: TypedEditorState<
| SerializedAutoLinkNode
| SerializedBlockNode
| SerializedHorizontalRuleNode
| SerializedLinkNode
| SerializedListItemNode
| SerializedListNode
| SerializedParagraphNode
| SerializedQuoteNode
| SerializedRelationshipNode
| SerializedTextNode
| SerializedUploadNode
| SerializedHeadingNode
> = {
root: {
type: 'root',
direction: 'ltr',
format: '',
indent: 0,
version: 1,
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Some text. Every property here is fully-typed',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
textFormat: 0,
version: 1,
},
],
},
}
```
Alternatively, you can use the `DefaultTypedEditorState` type, which includes all types for all nodes included in the `defaultFeatures`:
```ts
import type {
DefaultTypedEditorState
} from '@payloadcms/richtext-lexical'
const editorState: DefaultTypedEditorState = {
root: {
type: 'root',
direction: 'ltr',
format: '',
indent: 0,
version: 1,
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Some text. Every property here is fully-typed',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
textFormat: 0,
version: 1,
},
],
},
}
```
Just like `TypedEditorState`, the `DefaultTypedEditorState` also accepts an optional node type union as a generic. Here, this would **add** the specified node types to the default ones. Example: `DefaultTypedEditorState<SerializedBlockNode | YourCustomSerializedNode>`.
This is a type-safe representation of the editor state. Looking at the auto-suggestions of `type` it will show you all the possible node types you can use.
Make sure to only use types exported from `@payloadcms/richtext-lexical`, not from the lexical core packages. We only have control over types we export and can guarantee that those are correct, even though lexical core may export types with identical names.
### Automatic type generation
Lexical does not generate the accurate type definitions for your richText fields for you yet - this will be improved in the future. Currently, it only outputs the rough shape of the editor JSON which you can enhance using type assertions.

View File

@@ -79,7 +79,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/lexical/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/converters#markdown-lexical).
```ts
import { createServerFeature } from '@payloadcms/richtext-lexical';

View File

@@ -10,20 +10,24 @@ Lexical saves data in JSON - this is great for storage and flexibility and allow
## Lexical => JSX
If you have a React-based frontend, converting lexical to JSX is the recommended way to render rich text content in your frontend. To do that, import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the lexical content to it:
If your frontend uses React, converting Lexical to JSX is the recommended way to render rich text content. Import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the Lexical content to it:
```tsx
import React from 'react'
import { RichText } from '@payloadcms/richtext-lexical/react'
import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
export const MyComponent = ({ lexicalData }) => {
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
return (
<RichText data={lexicalData} />
<RichText data={data} />
)
}
```
The `RichText` component will come with the most common serializers built-in, though you can also pass in your own serializers if you need to.
The `RichText` component includes built-in serializers for common Lexical nodes but allows customization through the `converters` prop.
In our website template [you have an example](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) of how to use `converters` to render custom blocks.
<Banner type="default">
The JSX converter expects the input data to be fully populated. When fetching data, ensure the `depth` setting is high enough, to ensure that lexical nodes such as uploads are populated.

View File

@@ -1,9 +0,0 @@
---
title: Lexical Rich Text
label: Lexical
order: 30
desc: Built by Meta, Lexical is an incredibly powerful rich text editor, and it works beautifully within Payload.
keywords: lexical, rich text, editor, headless cms
---
The new lexical docs can be found at [Lexical](/docs/lexical/overview).

View File

@@ -1,7 +1,7 @@
---
title: Lexical Migration
label: Migration
order: 30
order: 90
desc: Migration from slate and payload-plugin-lexical to lexical
keywords: lexical, rich text, editor, headless cms, migrate, migration
---

View File

@@ -1,18 +1,300 @@
---
title: Overview
title: Rich Text Editor
label: Overview
order: 10
desc: Rich Text within Payload is extremely powerful. We've combined the beauty of the Medium editor with the power of the Notion editor all in one place.
keywords: slatejs, lexical, rich text, json, custom editor, javascript, typescript
desc: The Payload editor, based on Lexical, allows for great customization with unparalleled ease.
keywords: lexical, rich text, editor, headless cms
---
Payload currently supports two official rich text editors and you can choose either one depending on your needs.
<Banner type="warning">
1. [SlateJS](/docs/rich-text/slate) - stable, backwards-compatible with 1.0
2. [Lexical](/docs/lexical/overview) - recommended
The Payload editor is based on Lexical, Meta's rich text editor. The previous default editor was
based on Slate and is still supported. You can read [its documentation](/docs/rich-text/slate),
or the optional [migration guide](/docs/rich-text/migration) to migrate from Slate to Lexical (recommended).
These editors are built on an "adapter pattern" which means that you will need to install the editor you'd like to use. Take a look at the docs for the editor you'd like to use for instructions on how to install it.
</Banner>
The big TL;DR here is that Slate is what we have used in the past, and we still support it for existing projects, but if you're building something new and you're feeling adventurous, you should give Lexical a shot. Slate has a lot of good stuff, but Lexical has lots more.
One of Payload's goals is to build the best rich text editor experience that we possibly can. We want to combine the beauty and polish of the Medium editing experience with the strength and features of the Notion editor - all in one place.
No matter which editor you use, you have to install it at the top-level on the `config.editor` property, which will then cascade throughout all of your rich text fields and be used accordingly. Additionally, you also have the option to override the editor on a field-by-field basis if you'd like.
Classically, we've used SlateJS to work toward this goal, but building custom elements into Slate has proven to be more difficult than we'd like, and we've been keeping our options open.
Lexical is extremely impressive and trivializes a lot of the hard parts of building new elements into a rich text editor. It has a few distinct advantages over Slate, including the following:
1. A "/" menu, which allows editors to easily add new elements while never leaving their keyboard
1. A "hover" toolbar that pops up if you select text
1. It supports Payload blocks natively, directly within your rich text editor
1. Custom elements, called "features", are much easier to build in Lexical vs. Slate
To use the Lexical editor, first you need to install it:
```
npm install @payloadcms/richtext-lexical
```
Once you have it installed, you can pass it to your top-level Payload Config as follows:
```ts
import { buildConfig } from 'payload'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
export default buildConfig({
collections: [
// your collections here
],
// Pass the Lexical editor to the root config
editor: lexicalEditor({}),
})
```
You can also override Lexical settings on a field-by-field basis as follows:
```ts
import type { CollectionConfig } from 'payload'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
export const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{
name: 'content',
type: 'richText',
// Pass the Lexical editor here and override base settings as necessary
editor: lexicalEditor({}),
},
],
}
```
## Extending the lexical editor with Features
Lexical has been designed with extensibility in mind. Whether you're aiming to introduce new functionalities or tweak the existing ones, Lexical makes it seamless for you to bring those changes to life.
### Features: The Building Blocks
At the heart of Lexical's customization potential are "features". While Lexical ships with a set of default features we believe are essential for most use cases, the true power lies in your ability to redefine, expand, or prune these as needed.
If you remove all the default features, you're left with a blank editor. You can then add in only the features you need, or you can build your own custom features from scratch.
### Integrating New Features
To weave in your custom features, utilize the `features` prop when initializing the Lexical Editor. Here's a basic example of how this is done:
```ts
import {
BlocksFeature,
LinkFeature,
UploadFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
import { Banner } from '../blocks/Banner'
import { CallToAction } from '../blocks/CallToAction'
{
editor: lexicalEditor({
features: ({ defaultFeatures, rootFeatures }) => [
...defaultFeatures,
LinkFeature({
// Example showing how to customize the built-in fields
// of the Link feature
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
},
},
],
}),
UploadFeature({
collections: {
uploads: {
// Example showing how to customize the built-in fields
// of the Upload feature
fields: [
{
name: 'caption',
type: 'richText',
editor: lexicalEditor(),
},
],
},
},
}),
// This is incredibly powerful. You can re-use your Payload blocks
// directly in the Lexical editor as follows:
BlocksFeature({
blocks: [Banner, CallToAction],
}),
],
})
}
```
`features` can be both an array of features, or a function returning an array of features. The function provides the following props:
| Prop | Description |
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`defaultFeatures`** | This opinionated array contains all "recommended" default features. You can see which features are included in the default features in the table below. |
| **`rootFeatures`** | This array contains all features that are enabled in the root richText editor (the one defined in the payload.config.ts). If this field is the root richText editor, or if the root richText editor is not a lexical editor, this array will be empty. |
## Features overview
Here's an overview of all the included features:
| Feature Name | Included by default | Description |
|---------------------------------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`BoldTextFeature`** | Yes | Handles the bold text format |
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
| **`StrikethroughTextFeature`** | Yes | Handles the strikethrough text format |
| **`SubscriptTextFeature`** | Yes | Handles the subscript text format |
| **`SuperscriptTextFeature`** | Yes | Handles the superscript text format |
| **`InlineCodeTextFeature`** | Yes | Handles the inline-code text format |
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
| **`CheckListFeature`** | Yes | Adds checklists |
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
| **`BlockQuoteFeature`** | Yes | Allows you to create block-level quotes |
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
Notice how even the toolbars are features? That's how extensible our lexical editor is - you could theoretically create your own toolbar if you wanted to!
## Creating your own, custom Feature
You can find more information about creating your own feature in our [building custom feature docs](/docs/rich-text/building-custom-features).
## TypeScript
Every single piece of saved data is 100% fully-typed within lexical. It provides a type for every single node, which can be imported from `@payloadcms/richtext-lexical` - each type is prefixed with `Serialized`, e.g. `SerializedUploadNode`.
In order to fully type the entire editor JSON, you can use our `TypedEditorState` helper type, which accepts a union of all possible node types as a generic. The reason we do not provide a type which already contains all possible node types is because the possible node types depend on which features you have enabled in your editor. Here is an example:
```ts
import type {
SerializedAutoLinkNode,
SerializedBlockNode,
SerializedHorizontalRuleNode,
SerializedLinkNode,
SerializedListItemNode,
SerializedListNode,
SerializedParagraphNode,
SerializedQuoteNode,
SerializedRelationshipNode,
SerializedTextNode,
SerializedUploadNode,
TypedEditorState,
SerializedHeadingNode,
} from '@payloadcms/richtext-lexical'
const editorState: TypedEditorState<
| SerializedAutoLinkNode
| SerializedBlockNode
| SerializedHorizontalRuleNode
| SerializedLinkNode
| SerializedListItemNode
| SerializedListNode
| SerializedParagraphNode
| SerializedQuoteNode
| SerializedRelationshipNode
| SerializedTextNode
| SerializedUploadNode
| SerializedHeadingNode
> = {
root: {
type: 'root',
direction: 'ltr',
format: '',
indent: 0,
version: 1,
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Some text. Every property here is fully-typed',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
textFormat: 0,
version: 1,
},
],
},
}
```
Alternatively, you can use the `DefaultTypedEditorState` type, which includes all types for all nodes included in the `defaultFeatures`:
```ts
import type {
DefaultTypedEditorState
} from '@payloadcms/richtext-lexical'
const editorState: DefaultTypedEditorState = {
root: {
type: 'root',
direction: 'ltr',
format: '',
indent: 0,
version: 1,
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Some text. Every property here is fully-typed',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
textFormat: 0,
version: 1,
},
],
},
}
```
Just like `TypedEditorState`, the `DefaultTypedEditorState` also accepts an optional node type union as a generic. Here, this would **add** the specified node types to the default ones. Example: `DefaultTypedEditorState<SerializedBlockNode | YourCustomSerializedNode>`.
This is a type-safe representation of the editor state. Looking at the auto-suggestions of `type` it will show you all the possible node types you can use.
Make sure to only use types exported from `@payloadcms/richtext-lexical`, not from the lexical core packages. We only have control over types we export and can guarantee that those are correct, even though lexical core may export types with identical names.
### Automatic type generation
Lexical does not generate the accurate type definitions for your richText fields for you yet - this will be improved in the future. Currently, it only outputs the rough shape of the editor JSON which you can enhance using type assertions.

View File

@@ -1,7 +1,7 @@
---
title: Slate Rich Text
label: Slate
order: 20
title: Slate Editor
label: Slate (legacy)
order: 100
desc: The Slate editor has been supported by Payload since beta. It's very powerful and stores content as JSON, which unlocks a ton of power.
keywords: slatejs, slate, rich text, editor, headless cms
---

View File

@@ -1,7 +1,11 @@
# NOTE: Change port of `PAYLOAD_PUBLIC_SITE_URL` if front-end is running on another server
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3000
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
# Database connection string
DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
# Used to encrypt JWT tokens
PAYLOAD_SECRET=YOUR_SECRET_HERE
# Used to configure CORS, format links and more. No trailing slash
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
DATABASE_URI=mongodb://127.0.0.1/payload-example-auth
PAYLOAD_SECRET=PAYLOAD_AUTH_EXAMPLE_SECRET_KEY
# Used to share cookies across subdomains
COOKIE_DOMAIN=localhost

View File

@@ -1,12 +0,0 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp
playwright.config.ts
jest.config.js

View File

@@ -1,15 +1,4 @@
module.exports = {
extends: ['plugin:@next/next/core-web-vitals', '@payloadcms'],
ignorePatterns: ['**/payload-types.ts'],
overrides: [
{
extends: ['plugin:@typescript-eslint/disable-type-checked'],
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
},
],
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
root: true,
extends: ['@payloadcms'],
}

View File

@@ -1,4 +1,5 @@
build
dist
node_modules
package - lock.json.env
package-lock.json
.env

24
examples/auth/.swcrc Normal file
View File

@@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true,
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
},
"transform": {
"react": {
"runtime": "automatic",
"pragmaFrag": "React.Fragment",
"throwIfNamespace": true,
"development": false,
"useBuiltins": true
}
}
},
"module": {
"type": "es6"
}
}

View File

@@ -2,28 +2,27 @@
This [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) demonstrates how to implement [Payload Authentication](https://payloadcms.com/docs/authentication/overview) into all types of applications. Follow the [Quick Start](#quick-start) to get up and running quickly.
**IMPORTANT—This example includes a fully integrated Next.js App Router front-end that runs on the same server as Payload.** If you are working on an application running on an entirely separate server, the principals are generally the same. To learn more about this, [check out how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
## Quick Start
To spin up this example locally, follow these steps:
To spin up this example locally, follow the steps below:
1. Clone this repo
1. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
1. Navigate into the project directory and install dependencies using your preferred package manager:
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
- `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
1. `cp .env.example .env` to copy the example environment variables
> \*NOTE: The --ignore-workspace flag is needed if you are running this example within the Payload monorepo to avoid workspace conflicts.
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
1. Start the server:
- Depending on your package manager, run `pnpm dev`, `yarn dev` or `npm run dev`
- When prompted, type `y` then `enter` to seed the database with sample data
1. Access the application:
- Open your browser and navigate to `http://localhost:3000` to access the homepage.
- Open `http://localhost:3000/admin` to access the admin panel.
1. Login:
1. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
- Press `y` when prompted to seed the database
1. `open http://localhost:3000` to access the home page
1. `open http://localhost:3000/admin` to access the admin panel
- Login with email `demo@payloadcms.com` and password `demo`
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
- Use the following credentials to log into the admin panel:
> `Email: demo@payloadcms.com` > `Password: demo`
## How it works
@@ -45,7 +44,7 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
import config from '../../payload.config'
export default async function AccountPage({ searchParams }) {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config: configPromise })
const { permissions, user } = await payload.auth({ headers })

View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -3,41 +3,39 @@
"version": "1.0.0",
"description": "Payload authentication example.",
"license": "MIT",
"type": "module",
"main": "dist/server.js",
"scripts": {
"build": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts NODE_OPTIONS=--no-deprecation next build",
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts && pnpm seed && cross-env NODE_OPTIONS=--no-deprecation next dev",
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
"lint:fix": "eslint --fix --ext .ts,.tsx src",
"payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload",
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
"dev": "cross-env NODE_OPTIONS=--no-deprecation && pnpm seed && next dev",
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"generate:schema": "payload-graphql generate:schema",
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"seed": "npm run payload migrate:fresh",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
},
"dependencies": {
"@payloadcms/db-mongodb": "3.0.0-beta.24",
"@payloadcms/next": "3.0.0-beta.24",
"@payloadcms/richtext-slate": "3.0.0-beta.24",
"@payloadcms/ui": "3.0.0-beta.24",
"@payloadcms/db-mongodb": "latest",
"@payloadcms/next": "latest",
"@payloadcms/richtext-slate": "latest",
"@payloadcms/ui": "latest",
"cross-env": "^7.0.3",
"next": "14.3.0-canary.68",
"payload": "3.0.0-beta.24",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"next": "^15.0.0",
"payload": "latest",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-hook-form": "^7.51.3"
},
"devDependencies": {
"@next/eslint-plugin-next": "^13.1.6",
"@payloadcms/eslint-config": "^1.1.1",
"@swc/core": "^1.4.14",
"@swc/types": "^0.1.6",
"@types/node": "^20.11.25",
"@types/react": "^18.2.64",
"@types/react-dom": "^18.2.21",
"dotenv": "^16.4.5",
"@payloadcms/graphql": "latest",
"@swc/core": "^1.6.13",
"@types/ejs": "^3.1.5",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.1",
"eslint": "^8.57.0",
"tsx": "^4.7.1",
"typescript": "5.4.4"
"eslint-config-next": "^15.0.0",
"tsx": "^4.16.2",
"typescript": "5.5.2"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
import Image from 'next/image'
import Link from 'next/link'
import React from 'react'
import { Gutter } from '../Gutter'
import { HeaderNav } from './Nav'
import classes from './index.module.scss'
export const Header = () => {
return (
<header className={classes.header}>
<Gutter className={classes.wrap}>
<Link className={classes.logo} href="/">
<picture>
<source
media="(prefers-color-scheme: dark)"
srcSet="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-light.svg"
/>
<Image
alt="Payload Logo"
height={30}
src="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-dark.svg"
width={150}
/>
</picture>
</Link>
<HeaderNav />
</Gutter>
</header>
)
}

View File

@@ -13,7 +13,7 @@ import { AccountForm } from './AccountForm'
import classes from './index.module.scss'
export default async function Account() {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config })
const { permissions, user } = await payload.auth({ headers })

View File

@@ -10,7 +10,7 @@ import { CreateAccountForm } from './CreateAccountForm'
import classes from './index.module.scss'
export default async function CreateAccount() {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config })
const { user } = await payload.auth({ headers })

View File

@@ -10,7 +10,7 @@ import classes from './index.module.scss'
import { LoginForm } from './LoginForm'
export default async function Login() {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config })
const { user } = await payload.auth({ headers })

View File

@@ -9,7 +9,7 @@ import classes from './index.module.scss'
import { LogoutPage } from './LogoutPage'
export default async function Logout() {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config })
const { user } = await payload.auth({ headers })

View File

@@ -8,7 +8,7 @@ import { Gutter } from './_components/Gutter'
import { HydrateClientUser } from './_components/HydrateClientUser'
export default async function HomePage() {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config })
const { permissions, user } = await payload.auth({ headers })

View File

@@ -9,7 +9,7 @@ import classes from './index.module.scss'
import { RecoverPasswordForm } from './RecoverPasswordForm'
export default async function RecoverPassword() {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config })
const { user } = await payload.auth({ headers })

View File

@@ -9,7 +9,7 @@ import classes from './index.module.scss'
import { ResetPasswordForm } from './ResetPasswordForm'
export default async function ResetPassword() {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config })
const { user } = await payload.auth({ headers })

View File

@@ -5,18 +5,21 @@ import type { Metadata } from 'next'
import config from '@payload-config'
import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
type Args = {
params: {
params: Promise<{
segments: string[]
}
searchParams: {
}>
searchParams: Promise<{
[key: string]: string | string[]
}
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, importMap, params, searchParams })
export default NotFound

View File

@@ -5,18 +5,21 @@ import type { Metadata } from 'next'
import config from '@payload-config'
import { generatePageMetadata, RootPage } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
type Args = {
params: {
params: Promise<{
segments: string[]
}
searchParams: {
}>
searchParams: Promise<{
[key: string]: string | string[]
}
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, importMap, params, searchParams })
export default Page

View File

@@ -0,0 +1,5 @@
import { BeforeLogin as BeforeLogin_8a7ab0eb7ab5c511aba12e68480bfe5e } from '@/components/BeforeLogin'
export const importMap = {
'@/components/BeforeLogin#BeforeLogin': BeforeLogin_8a7ab0eb7ab5c511aba12e68480bfe5e,
}

View File

@@ -1,16 +1,32 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import configPromise from '@payload-config'
import type { ServerFunctionClient } from 'payload'
import '@payloadcms/next/css'
import { RootLayout } from '@payloadcms/next/layouts'
import config from '@payload-config'
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
import React from 'react'
import { importMap } from './admin/importMap.js'
import './custom.scss'
type Args = {
children: React.ReactNode
}
const Layout = ({ children }: Args) => <RootLayout config={configPromise}>{children}</RootLayout>
const serverFunction: ServerFunctionClient = async function (args) {
'use server'
return handleServerFunctions({
...args,
config,
importMap,
})
}
const Layout = ({ children }: Args) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
)
export default Layout

View File

@@ -1,13 +1,62 @@
import type { CollectionConfig } from 'payload/types'
import { admins } from './access/admins'
import adminsAndUser from './access/adminsAndUser'
import { anyone } from './access/anyone'
import { checkRole } from './access/checkRole'
import { loginAfterCreate } from './hooks/loginAfterCreate'
import { protectRoles } from './hooks/protectRoles'
export const Users: CollectionConfig = {
slug: 'users',
auth: {
tokenExpiration: 28800, // 8 hours
cookies: {
sameSite: 'none',
secure: true,
domain: process.env.COOKIE_DOMAIN,
},
},
admin: {
useAsTitle: 'email',
},
auth: true,
access: {
read: adminsAndUser,
create: anyone,
update: adminsAndUser,
delete: admins,
admin: ({ req: { user } }) => checkRole(['admin'], user),
},
hooks: {
afterChange: [loginAfterCreate],
},
fields: [
// Email added by default
// Add more fields as needed
{
name: 'firstName',
type: 'text',
},
{
name: 'lastName',
type: 'text',
},
{
name: 'roles',
type: 'select',
hasMany: true,
saveToJWT: true,
hooks: {
beforeChange: [protectRoles],
},
options: [
{
label: 'Admin',
value: 'admin',
},
{
label: 'User',
value: 'user',
},
],
},
],
}

View File

@@ -1,6 +1,6 @@
import React from 'react'
const BeforeLogin: React.FC = () => {
export const BeforeLogin: React.FC = () => {
if (process.env.PAYLOAD_PUBLIC_SEED === 'true') {
return (
<p>
@@ -13,5 +13,3 @@ const BeforeLogin: React.FC = () => {
}
return null
}
export default BeforeLogin

View File

@@ -7,16 +7,53 @@
*/
export interface Config {
auth: {
users: UserAuthOperations;
};
collections: {
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
};
globals: {};
globalsSelect: {};
locale: null;
user: User & {
collection: 'users';
};
jobs: {
tasks: unknown;
workflows: unknown;
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -38,6 +75,24 @@ export interface User {
lockUntil?: string | null;
password?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
document?: {
relationTo: 'users';
value: string | User;
} | null;
globalSlug?: string | null;
user: {
relationTo: 'users';
value: string | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
@@ -72,6 +127,63 @@ export interface PayloadMigration {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
firstName?: T;
lastName?: T;
roles?: T;
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}
declare module 'payload' {

View File

@@ -2,28 +2,21 @@ import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { slateEditor } from '@payloadcms/richtext-slate'
import { fileURLToPath } from 'node:url'
import path from 'path'
import { buildConfig } from 'payload/config'
import { buildConfig } from 'payload'
import { Users } from './collections/Users'
import BeforeLogin from './components/BeforeLogin'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfig({
admin: {
components: {
beforeLogin: [BeforeLogin],
beforeLogin: ['@/components/BeforeLogin#BeforeLogin'],
},
},
collections: [Users],
cors: [
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
].filter(Boolean),
csrf: [
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
].filter(Boolean),
cors: [process.env.NEXT_PUBLIC_SERVER_URL || ''].filter(Boolean),
csrf: [process.env.NEXT_PUBLIC_SERVER_URL || ''].filter(Boolean),
db: mongooseAdapter({
url: process.env.DATABASE_URI || '',
}),

View File

@@ -23,10 +23,25 @@
}
],
"paths": {
"@/*": ["./src/*"],
"@payload-config": ["src/payload.config.ts"]
}
"@/*": [
"./src/*"
],
"@payload-config": [
"src/payload.config.ts"
],
"@payload-types": [
"src/payload-types.ts"
]
},
"target": "ES2022",
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View File

@@ -1,8 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { ServerFunctionClient } from 'payload'
import '@payloadcms/next/css'
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
import React from 'react'

View File

@@ -22,7 +22,6 @@
"dotenv": "^8.2.0",
"escape-html": "^1.0.3",
"graphql": "^16.9.0",
"jsonwebtoken": "9.0.2",
"next": "^15.0.0",
"payload": "latest",
"payload-admin-bar": "^1.0.6",

View File

@@ -1,6 +1,5 @@
import type { CollectionSlug } from 'payload'
import type { CollectionSlug, PayloadRequest } from 'payload'
import jwt from 'jsonwebtoken'
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
import { getPayload } from 'payload'
@@ -42,23 +41,20 @@ export async function GET(
return new Response('No path provided', { status: 404 })
}
if (!token) {
new Response('You are not allowed to preview this page', { status: 403 })
}
if (!path.startsWith('/')) {
new Response('This endpoint can only be used for internal previews', { status: 500 })
return new Response('This endpoint can only be used for internal previews', { status: 500 })
}
let user
try {
user = jwt.verify(token, payload.secret)
} catch (error) {
payload.logger.error({
err: error,
msg: 'Error verifying token for live preview',
user = await payload.auth({
req: req as unknown as PayloadRequest,
headers: req.headers,
})
} catch (error) {
payload.logger.error({ err: error }, 'Error verifying token for live preview')
return new Response('You are not allowed to preview this page', { status: 403 })
}
const draft = await draftMode()

View File

@@ -30,7 +30,7 @@
"react": "19.0.0",
"react-dom": "19.0.0",
"react-hook-form": "^7.41.0",
"react-select": "^5.8.0"
"react-select": "^5.9.0"
},
"devDependencies": {
"@payloadcms/graphql": "latest",

View File

@@ -32,7 +32,7 @@ const config = withBundleAnalyzer(
return [
{
destination: '/admin',
permanent: true,
permanent: false,
source: '/',
},
]

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.6.0",
"version": "3.9.0",
"private": true,
"type": "module",
"scripts": {
@@ -54,6 +54,7 @@
"clean:build": "node ./scripts/delete-recursively.js 'media/' '**/dist/' '**/.cache/' '**/.next/' '**/.turbo/' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
"clean:cache": "node ./scripts/delete-recursively.js node_modules/.cache! packages/payload/node_modules/.cache! .next/*",
"dev": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts",
"dev:generate-db-schema": "pnpm runts ./test/generateDatabaseSchema.ts",
"dev:generate-graphql-schema": "pnpm runts ./test/generateGraphQLSchema.ts",
"dev:generate-importmap": "pnpm runts ./test/generateImportMap.ts",
"dev:generate-types": "pnpm runts ./test/generateTypes.ts",
@@ -168,7 +169,7 @@
"tempy": "1.0.1",
"tstyche": "^3.1.1",
"tsx": "4.19.2",
"turbo": "^2.1.3",
"turbo": "^2.3.3",
"typescript": "5.7.2"
},
"peerDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.6.0",
"version": "3.9.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -4,42 +4,66 @@ import path from 'path'
import type { CliArgs, DbType, ProjectTemplate } from '../types.js'
import { debug, error } from '../utils/log.js'
import { dbChoiceRecord } from './select-db.js'
const updateEnvVariables = (
contents: string,
const updateEnvExampleVariables = (contents: string, databaseType: DbType | undefined): string => {
return contents
.split('\n')
.map((line) => {
if (line.startsWith('#') || !line.includes('=')) {
return line // Preserve comments and unrelated lines
}
const [key] = line.split('=')
if (key === 'DATABASE_URI' || key === 'POSTGRES_URL' || key === 'MONGODB_URI') {
const dbChoice = databaseType ? dbChoiceRecord[databaseType] : null
if (dbChoice) {
const placeholderUri = `${dbChoice.dbConnectionPrefix}your-database-name${
dbChoice.dbConnectionSuffix || ''
}`
return databaseType === 'vercel-postgres'
? `POSTGRES_URL=${placeholderUri}`
: `DATABASE_URI=${placeholderUri}`
}
return `DATABASE_URI=your-database-connection-here` // Fallback
}
if (key === 'PAYLOAD_SECRET' || key === 'PAYLOAD_SECRET_KEY') {
return `PAYLOAD_SECRET=YOUR_SECRET_HERE`
}
return line
})
.join('\n')
}
const generateEnvContent = (
existingEnv: string,
databaseType: DbType | undefined,
databaseUri: string,
payloadSecret: string,
): string => {
return contents
const dbKey = databaseType === 'vercel-postgres' ? 'POSTGRES_URL' : 'DATABASE_URI'
const envVars: Record<string, string> = {}
existingEnv
.split('\n')
.filter((e) => e)
.map((line) => {
if (line.startsWith('#') || !line.includes('=')) {
return line
}
const [key, ...valueParts] = line.split('=')
let value = valueParts.join('=')
if (
key === 'MONGODB_URI' ||
key === 'MONGO_URL' ||
key === 'DATABASE_URI' ||
key === 'POSTGRES_URL'
) {
value = databaseUri
if (databaseType === 'vercel-postgres') {
value = databaseUri
}
}
if (key === 'PAYLOAD_SECRET' || key === 'PAYLOAD_SECRET_KEY') {
value = payloadSecret
}
return `${key}=${value}`
.filter((line) => line.includes('=') && !line.startsWith('#'))
.forEach((line) => {
const [key, value] = line.split('=')
envVars[key] = value
})
// Override specific keys
envVars[dbKey] = databaseUri
envVars['PAYLOAD_SECRET'] = payloadSecret
// Rebuild content
return Object.entries(envVars)
.map(([key, value]) => `${key}=${value}`)
.join('\n')
}
@@ -65,6 +89,7 @@ export async function manageEnvFiles(args: {
try {
let updatedExampleContents: string
// Update .env.example
if (template?.type === 'starter') {
if (!fs.existsSync(envExamplePath)) {
error(`.env.example file not found at ${envExamplePath}`)
@@ -72,25 +97,25 @@ export async function manageEnvFiles(args: {
}
const envExampleContents = await fs.readFile(envExamplePath, 'utf8')
updatedExampleContents = updateEnvVariables(
envExampleContents,
databaseType,
databaseUri,
payloadSecret,
)
updatedExampleContents = updateEnvExampleVariables(envExampleContents, databaseType)
await fs.writeFile(envExamplePath, updatedExampleContents)
await fs.writeFile(envExamplePath, updatedExampleContents.trimEnd() + '\n')
debug(`.env.example file successfully updated`)
} else {
updatedExampleContents = `# Added by Payload\nDATABASE_URI=${databaseUri}\nPAYLOAD_SECRET=${payloadSecret}\n`
updatedExampleContents = `# Added by Payload\nDATABASE_URI=your-connection-string-here\nPAYLOAD_SECRET=YOUR_SECRET_HERE\n`
await fs.writeFile(envExamplePath, updatedExampleContents.trimEnd() + '\n')
}
const existingEnvContents = fs.existsSync(envPath) ? await fs.readFile(envPath, 'utf8') : ''
const updatedEnvContents = existingEnvContents
? `${existingEnvContents}\n# Added by Payload\n${updatedExampleContents}`
: `# Added by Payload\n${updatedExampleContents}`
// Merge existing variables and create or update .env
const envExampleContents = await fs.readFile(envExamplePath, 'utf8')
const envContent = generateEnvContent(
envExampleContents,
databaseType,
databaseUri,
payloadSecret,
)
await fs.writeFile(envPath, `# Added by Payload\n${envContent.trimEnd()}\n`)
await fs.writeFile(envPath, updatedEnvContents)
debug(`.env file successfully created or updated`)
} catch (err: unknown) {
error('Unable to manage environment files')

View File

@@ -10,7 +10,7 @@ type DbChoice = {
value: DbType
}
const dbChoiceRecord: Record<DbType, DbChoice> = {
export const dbChoiceRecord: Record<DbType, DbChoice> = {
mongodb: {
dbConnectionPrefix: 'mongodb://127.0.0.1/',
title: 'MongoDB',

View File

@@ -13,10 +13,16 @@ export function copyRecursiveSync(src: string, dest: string, ignoreRegex?: strin
if (isDirectory) {
fs.mkdirSync(dest, { recursive: true })
fs.readdirSync(src).forEach((childItemName) => {
if (ignoreRegex && ignoreRegex.some((regex) => new RegExp(regex).test(childItemName))) {
if (
ignoreRegex &&
ignoreRegex.some((regex) => {
return new RegExp(regex).test(childItemName)
})
) {
console.log(`Ignoring ${childItemName} due to regex: ${ignoreRegex}`)
return
}
copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName))
copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName), ignoreRegex)
})
} else {
fs.copyFileSync(src, dest)

View File

@@ -1,13 +1,6 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true, // Make sure typescript knows that this module depends on their references
"noEmit": false /* Do not emit outputs. */,
"emitDeclarationOnly": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
"strict": true
},
"exclude": ["dist", "build", "tests", "test", "node_modules", "eslint.config.js"],
"include": ["src/**/*.ts", "src/**/*.spec.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.6.0",
"version": "3.9.0",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,50 +1,47 @@
import type { CountOptions } from 'mongodb'
import type { Count, PayloadRequest } from 'payload'
import { flattenWhereToOperators } from 'payload'
import type { Count } from 'payload'
import type { MongooseAdapter } from './index.js'
import { withSession } from './withSession.js'
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
import { getSession } from './utilities/getSession.js'
export const count: Count = async function count(
this: MongooseAdapter,
{ collection, locale, req = {} as PayloadRequest, where },
{ collection, locale, req, where },
) {
const Model = this.collections[collection]
const options: CountOptions = await withSession(this, req)
const session = await getSession(this, req)
let hasNearConstraint = false
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
const hasNearConstraint = getHasNearConstraint(where)
const query = await Model.buildQuery({
locale,
payload: this.payload,
session,
where,
})
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
options.hint = {
_id: 1,
}
}
let result: number
if (useEstimatedCount) {
result = await Model.estimatedDocumentCount({ session: options.session })
result = await Model.collection.estimatedDocumentCount()
} else {
result = await Model.countDocuments(query, options)
const options: CountOptions = { session }
if (this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
options.hint = {
_id: 1,
}
}
result = await Model.collection.countDocuments(query, options)
}
return {

View File

@@ -1,50 +1,47 @@
import type { CountOptions } from 'mongodb'
import type { CountGlobalVersions, PayloadRequest } from 'payload'
import { flattenWhereToOperators } from 'payload'
import type { CountGlobalVersions } from 'payload'
import type { MongooseAdapter } from './index.js'
import { withSession } from './withSession.js'
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
import { getSession } from './utilities/getSession.js'
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
this: MongooseAdapter,
{ global, locale, req = {} as PayloadRequest, where },
{ global, locale, req, where },
) {
const Model = this.versions[global]
const options: CountOptions = await withSession(this, req)
const session = await getSession(this, req)
let hasNearConstraint = false
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
const hasNearConstraint = getHasNearConstraint(where)
const query = await Model.buildQuery({
locale,
payload: this.payload,
session,
where,
})
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
options.hint = {
_id: 1,
}
}
let result: number
if (useEstimatedCount) {
result = await Model.estimatedDocumentCount({ session: options.session })
result = await Model.collection.estimatedDocumentCount()
} else {
result = await Model.countDocuments(query, options)
const options: CountOptions = { session }
if (Object.keys(query).length === 0 && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
options.hint = {
_id: 1,
}
}
result = await Model.collection.countDocuments(query, options)
}
return {

View File

@@ -1,50 +1,47 @@
import type { CountOptions } from 'mongodb'
import type { CountVersions, PayloadRequest } from 'payload'
import { flattenWhereToOperators } from 'payload'
import type { CountVersions } from 'payload'
import type { MongooseAdapter } from './index.js'
import { withSession } from './withSession.js'
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
import { getSession } from './utilities/getSession.js'
export const countVersions: CountVersions = async function countVersions(
this: MongooseAdapter,
{ collection, locale, req = {} as PayloadRequest, where },
{ collection, locale, req, where },
) {
const Model = this.versions[collection]
const options: CountOptions = await withSession(this, req)
const session = await getSession(this, req)
let hasNearConstraint = false
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
const hasNearConstraint = getHasNearConstraint(where)
const query = await Model.buildQuery({
locale,
payload: this.payload,
session,
where,
})
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
options.hint = {
_id: 1,
}
}
let result: number
if (useEstimatedCount) {
result = await Model.estimatedDocumentCount({ session: options.session })
result = await Model.collection.estimatedDocumentCount()
} else {
result = await Model.countDocuments(query, options)
const options: CountOptions = { session }
if (this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
options.hint = {
_id: 1,
}
}
result = await Model.collection.countDocuments(query, options)
}
return {

View File

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

View File

@@ -1,35 +1,39 @@
import type { CreateGlobal, PayloadRequest } from 'payload'
import type { CreateGlobal } from 'payload'
import type { MongooseAdapter } from './index.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js'
export const createGlobal: CreateGlobal = async function createGlobal(
this: MongooseAdapter,
{ slug, data, req = {} as PayloadRequest },
{ slug, data, req },
) {
const Model = this.globals
const global = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
globalType: slug,
...data,
},
fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
const fields = this.payload.config.globals.find(
(globalConfig) => globalConfig.slug === slug,
).flattenedFields
transform({
adapter: this,
data,
fields,
globalSlug: slug,
operation: 'create',
})
const options = await withSession(this, req)
const session = await getSession(this, req)
let [result] = (await Model.create([global], options)) as any
const { insertedId: insertedID } = await Model.collection.insertOne(data, { session })
;(data as any)._id = insertedID
result = JSON.parse(JSON.stringify(result))
transform({
adapter: this,
data,
fields,
operation: 'read',
})
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
return result
return data
}

View File

@@ -1,14 +1,9 @@
import {
buildVersionGlobalFields,
type CreateGlobalVersion,
type Document,
type PayloadRequest,
} from 'payload'
import { buildVersionGlobalFields, type CreateGlobalVersion } from 'payload'
import type { MongooseAdapter } from './index.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js'
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
this: MongooseAdapter,
@@ -18,41 +13,48 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
globalSlug,
parent,
publishedLocale,
req = {} as PayloadRequest,
req,
snapshot,
updatedAt,
versionData,
},
) {
const VersionModel = this.versions[globalSlug]
const options = await withSession(this, req)
const session = await getSession(this, req)
const data = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
fields: buildVersionGlobalFields(
this.payload.config,
this.payload.config.globals.find((global) => global.slug === globalSlug),
),
const data = {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
}
const fields = buildVersionGlobalFields(
this.payload.config,
this.payload.config.globals.find((global) => global.slug === globalSlug),
true,
)
transform({
adapter: this,
data,
fields,
operation: 'create',
})
const [doc] = await VersionModel.create([data], options, req)
const { insertedId: insertedID } = await VersionModel.collection.insertOne(data, { session })
;(data as any)._id = insertedID
await VersionModel.updateMany(
await VersionModel.collection.updateMany(
{
$and: [
{
_id: {
$ne: doc._id,
$ne: insertedID,
},
},
{
@@ -68,16 +70,15 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
],
},
{ $unset: { latest: 1 } },
options,
{ session },
)
const result: Document = JSON.parse(JSON.stringify(doc))
const verificationToken = doc._verificationToken
transform({
adapter: this,
data,
fields,
operation: 'read',
})
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
return data as any
}

View File

@@ -1,15 +1,10 @@
import { Types } from 'mongoose'
import {
buildVersionCollectionFields,
type CreateVersion,
type Document,
type PayloadRequest,
} from 'payload'
import { buildVersionCollectionFields, type CreateVersion } from 'payload'
import type { MongooseAdapter } from './index.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js'
export const createVersion: CreateVersion = async function createVersion(
this: MongooseAdapter,
@@ -19,34 +14,41 @@ export const createVersion: CreateVersion = async function createVersion(
createdAt,
parent,
publishedLocale,
req = {} as PayloadRequest,
req,
snapshot,
updatedAt,
versionData,
},
) {
const VersionModel = this.versions[collectionSlug]
const options = await withSession(this, req)
const session = await getSession(this, req)
const data = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collectionSlug].config,
),
const data: any = {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
}
const fields = buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collectionSlug].config,
true,
)
transform({
adapter: this,
data,
fields,
operation: 'create',
})
const [doc] = await VersionModel.create([data], options, req)
const { insertedId: insertedID } = await VersionModel.collection.insertOne(data, { session })
data._id = insertedID
const parentQuery = {
$or: [
@@ -57,7 +59,7 @@ export const createVersion: CreateVersion = async function createVersion(
},
],
}
if (data.parent instanceof Types.ObjectId) {
if ((data.parent as unknown) instanceof Types.ObjectId) {
parentQuery.$or.push({
parent: {
$eq: data.parent.toString(),
@@ -65,12 +67,12 @@ export const createVersion: CreateVersion = async function createVersion(
})
}
await VersionModel.updateMany(
await VersionModel.collection.updateMany(
{
$and: [
{
_id: {
$ne: doc._id,
$ne: insertedID,
},
},
parentQuery,
@@ -81,22 +83,21 @@ export const createVersion: CreateVersion = async function createVersion(
},
{
updatedAt: {
$lt: new Date(doc.updatedAt),
$lt: new Date(data.updatedAt),
},
},
],
},
{ $unset: { latest: 1 } },
options,
{ session },
)
const result: Document = JSON.parse(JSON.stringify(doc))
const verificationToken = doc._verificationToken
transform({
adapter: this,
data,
fields,
operation: 'read',
})
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
return data
}

View File

@@ -1,23 +1,24 @@
import type { DeleteMany, PayloadRequest } from 'payload'
import type { DeleteMany } from 'payload'
import type { MongooseAdapter } from './index.js'
import { withSession } from './withSession.js'
import { getSession } from './utilities/getSession.js'
export const deleteMany: DeleteMany = async function deleteMany(
this: MongooseAdapter,
{ collection, req = {} as PayloadRequest, where },
{ collection, req, where },
) {
const Model = this.collections[collection]
const options = {
...(await withSession(this, req)),
lean: true,
}
const session = await getSession(this, req)
const query = await Model.buildQuery({
payload: this.payload,
session,
where,
})
await Model.deleteMany(query, options)
await Model.collection.deleteMany(query, {
session,
})
}

View File

@@ -1,37 +1,41 @@
import type { DeleteOne, Document, PayloadRequest } from 'payload'
import type { DeleteOne } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { withSession } from './withSession.js'
import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js'
export const deleteOne: DeleteOne = async function deleteOne(
this: MongooseAdapter,
{ collection, req = {} as PayloadRequest, select, where },
{ collection, req, select, where },
) {
const Model = this.collections[collection]
const options = await withSession(this, req)
const session = await getSession(this, req)
const query = await Model.buildQuery({
payload: this.payload,
session,
where,
})
const doc = await Model.findOneAndDelete(query, {
...options,
const fields = this.payload.collections[collection].config.flattenedFields
const doc = await Model.collection.findOneAndDelete(query, {
projection: buildProjectionFromSelect({
adapter: this,
fields: this.payload.collections[collection].config.flattenedFields,
fields,
select,
}),
}).lean()
session,
})
let result: Document = JSON.parse(JSON.stringify(doc))
transform({
adapter: this,
data: doc,
fields,
operation: 'read',
})
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
return result
return doc
}

View File

@@ -1,24 +1,25 @@
import type { DeleteVersions, PayloadRequest } from 'payload'
import type { DeleteVersions } from 'payload'
import type { MongooseAdapter } from './index.js'
import { withSession } from './withSession.js'
import { getSession } from './utilities/getSession.js'
export const deleteVersions: DeleteVersions = async function deleteVersions(
this: MongooseAdapter,
{ collection, locale, req = {} as PayloadRequest, where },
{ collection, locale, req, where },
) {
const VersionsModel = this.versions[collection]
const options = {
...(await withSession(this, req)),
lean: true,
}
const session = await getSession(this, req)
const query = await VersionsModel.buildQuery({
locale,
payload: this.payload,
session,
where,
})
await VersionsModel.deleteMany(query, options)
await VersionsModel.collection.deleteMany(query, {
session,
})
}

View File

@@ -1,15 +1,15 @@
import type { PaginateOptions } from 'mongoose'
import type { Find, PayloadRequest } from 'payload'
import { flattenWhereToOperators } from 'payload'
import type { CollationOptions } from 'mongodb'
import type { Find } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { withSession } from './withSession.js'
import { findMany } from './utilities/findMany.js'
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js'
export const find: Find = async function find(
this: MongooseAdapter,
@@ -20,8 +20,7 @@ export const find: Find = async function find(
locale,
page,
pagination,
projection,
req = {} as PayloadRequest,
req,
select,
sort: sortArg,
where,
@@ -29,20 +28,17 @@ export const find: Find = async function find(
) {
const Model = this.collections[collection]
const collectionConfig = this.payload.collections[collection].config
const options = await withSession(this, req)
const session = await getSession(this, req)
let hasNearConstraint = false
const hasNearConstraint = getHasNearConstraint(where)
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
const fields = collectionConfig.flattenedFields
let sort
if (!hasNearConstraint) {
sort = buildSortParam({
config: this.payload.config,
fields: collectionConfig.flattenedFields,
fields,
locale,
sort: sortArg || collectionConfig.defaultSort,
timestamps: true,
@@ -52,88 +48,51 @@ export const find: Find = async function find(
const query = await Model.buildQuery({
locale,
payload: this.payload,
session,
where,
})
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
const paginationOptions: PaginateOptions = {
lean: true,
leanWithId: true,
options,
page,
pagination,
projection,
sort,
useEstimatedCount,
}
if (select) {
paginationOptions.projection = buildProjectionFromSelect({
adapter: this,
fields: collectionConfig.flattenedFields,
select,
})
}
const projection = buildProjectionFromSelect({
adapter: this,
fields,
select,
})
if (this.collation) {
const defaultLocale = 'en'
paginationOptions.collation = {
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
...this.collation,
}
}
const collation: CollationOptions | undefined = this.collation
? {
locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en',
...this.collation,
}
: undefined
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(
Model.countDocuments(query, {
...options,
hint: { _id: 1 },
}),
)
}
}
if (limit >= 0) {
paginationOptions.limit = limit
// limit must also be set here, it's ignored when pagination is false
paginationOptions.options.limit = limit
// Disable pagination if limit is 0
if (limit === 0) {
paginationOptions.pagination = false
}
}
let result
const aggregate = await buildJoinAggregation({
const joinAgreggation = await buildJoinAggregation({
adapter: this,
collection,
collectionConfig,
joins,
locale,
query,
session,
})
// build join aggregation
if (aggregate) {
result = await Model.aggregatePaginate(Model.aggregate(aggregate), paginationOptions)
} else {
result = await Model.paginate(query, paginationOptions)
}
const docs = JSON.parse(JSON.stringify(result.docs))
const result = await findMany({
adapter: this,
collation,
collection: Model.collection,
joinAgreggation,
limit,
page,
pagination,
projection,
query,
session,
sort,
useEstimatedCount,
})
return {
...result,
docs: docs.map((doc) => {
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}
transform({ adapter: this, data: result.docs, fields, operation: 'read' })
return result
}

View File

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

View File

@@ -1,29 +1,20 @@
import type { PaginateOptions } from 'mongoose'
import type { FindGlobalVersions, PayloadRequest } from 'payload'
import type { CollationOptions } from 'mongodb'
import type { FindGlobalVersions } from 'payload'
import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
import { buildVersionGlobalFields } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { withSession } from './withSession.js'
import { findMany } from './utilities/findMany.js'
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js'
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
this: MongooseAdapter,
{
global,
limit,
locale,
page,
pagination,
req = {} as PayloadRequest,
select,
skip,
sort: sortArg,
where,
},
{ global, limit, locale, page, pagination, req, select, skip, sort: sortArg, where },
) {
const Model = this.versions[global]
const versionFields = buildVersionGlobalFields(
@@ -31,18 +22,8 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
this.payload.globals.config.find(({ slug }) => slug === global),
true,
)
const options = {
...(await withSession(this, req)),
limit,
skip,
}
let hasNearConstraint = false
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
const hasNearConstraint = getHasNearConstraint(where)
let sort
if (!hasNearConstraint) {
@@ -55,69 +36,49 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
})
}
const session = await getSession(this, req)
const query = await Model.buildQuery({
globalSlug: global,
locale,
payload: this.payload,
session,
where,
})
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
const paginationOptions: PaginateOptions = {
lean: true,
leanWithId: true,
const projection = buildProjectionFromSelect({ adapter: this, fields: versionFields, select })
const collation: CollationOptions | undefined = this.collation
? {
locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en',
...this.collation,
}
: undefined
const result = await findMany({
adapter: this,
collation,
collection: Model.collection,
limit,
options,
page,
pagination,
projection: buildProjectionFromSelect({ adapter: this, fields: versionFields, select }),
projection,
query,
session,
skip,
sort,
useEstimatedCount,
}
})
if (this.collation) {
const defaultLocale = 'en'
paginationOptions.collation = {
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
...this.collation,
}
}
transform({
adapter: this,
data: result.docs,
fields: versionFields,
operation: 'read',
})
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(
Model.countDocuments(query, {
...options,
hint: { _id: 1 },
}),
)
}
}
if (limit >= 0) {
paginationOptions.limit = limit
// limit must also be set here, it's ignored when pagination is false
paginationOptions.options.limit = limit
// Disable pagination if limit is 0
if (limit === 0) {
paginationOptions.pagination = false
}
}
const result = await Model.paginate(query, paginationOptions)
const docs = JSON.parse(JSON.stringify(result.docs))
return {
...result,
docs: docs.map((doc) => {
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}
return result
}

View File

@@ -1,64 +1,76 @@
import type { MongooseQueryOptions, QueryOptions } from 'mongoose'
import type { Document, FindOne, PayloadRequest } from 'payload'
import type { FindOne } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { withSession } from './withSession.js'
import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js'
export const findOne: FindOne = async function findOne(
this: MongooseAdapter,
{ collection, joins, locale, req = {} as PayloadRequest, select, where },
{ collection, joins, locale, req, select, where },
) {
const Model = this.collections[collection]
const collectionConfig = this.payload.collections[collection].config
const options: MongooseQueryOptions = {
...(await withSession(this, req)),
lean: true,
}
const session = await getSession(this, req)
const query = await Model.buildQuery({
locale,
payload: this.payload,
session,
where,
})
const fields = collectionConfig.flattenedFields
const projection = buildProjectionFromSelect({
adapter: this,
fields: collectionConfig.flattenedFields,
fields,
select,
})
const aggregate = await buildJoinAggregation({
const joinAggregation = await buildJoinAggregation({
adapter: this,
collection,
collectionConfig,
joins,
limit: 1,
locale,
projection,
query,
session,
})
let doc
if (aggregate) {
;[doc] = await Model.aggregate(aggregate, options)
if (joinAggregation) {
const aggregation = Model.collection.aggregate(
[
{
$match: query,
},
],
{ session },
)
aggregation.limit(1)
for (const stage of joinAggregation) {
aggregation.addStage(stage)
}
;[doc] = await aggregation.toArray()
} else {
;(options as Record<string, unknown>).projection = projection
doc = await Model.findOne(query, {}, options)
doc = await Model.collection.findOne(query, { projection, session })
}
if (!doc) {
return null
}
let result: Document = JSON.parse(JSON.stringify(doc))
transform({
adapter: this,
data: doc,
fields,
operation: 'read',
})
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
return result
return doc
}

View File

@@ -1,44 +1,27 @@
import type { PaginateOptions } from 'mongoose'
import type { FindVersions, PayloadRequest } from 'payload'
import type { CollationOptions } from 'mongodb'
import type { FindVersions } from 'payload'
import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload'
import { buildVersionCollectionFields } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { withSession } from './withSession.js'
import { findMany } from './utilities/findMany.js'
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js'
export const findVersions: FindVersions = async function findVersions(
this: MongooseAdapter,
{
collection,
limit,
locale,
page,
pagination,
req = {} as PayloadRequest,
select,
skip,
sort: sortArg,
where,
},
{ collection, limit, locale, page, pagination, req = {}, select, skip, sort: sortArg, where },
) {
const Model = this.versions[collection]
const collectionConfig = this.payload.collections[collection].config
const options = {
...(await withSession(this, req)),
limit,
skip,
}
let hasNearConstraint = false
const session = await getSession(this, req)
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
const hasNearConstraint = getHasNearConstraint(where)
let sort
if (!hasNearConstraint) {
@@ -54,69 +37,48 @@ export const findVersions: FindVersions = async function findVersions(
const query = await Model.buildQuery({
locale,
payload: this.payload,
session,
where,
})
const versionFields = buildVersionCollectionFields(this.payload.config, collectionConfig, true)
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
const paginationOptions: PaginateOptions = {
lean: true,
leanWithId: true,
const projection = buildProjectionFromSelect({
adapter: this,
fields: versionFields,
select,
})
const collation: CollationOptions | undefined = this.collation
? {
locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en',
...this.collation,
}
: undefined
const result = await findMany({
adapter: this,
collation,
collection: Model.collection,
limit,
options,
page,
pagination,
projection: buildProjectionFromSelect({
adapter: this,
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
select,
}),
projection,
query,
session,
skip,
sort,
useEstimatedCount,
}
})
if (this.collation) {
const defaultLocale = 'en'
paginationOptions.collation = {
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
...this.collation,
}
}
transform({
adapter: this,
data: result.docs,
fields: versionFields,
operation: 'read',
})
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(
Model.countDocuments(query, {
...options,
hint: { _id: 1 },
}),
)
}
}
if (limit >= 0) {
paginationOptions.limit = limit
// limit must also be set here, it's ignored when pagination is false
paginationOptions.options.limit = limit
// Disable pagination if limit is 0
if (limit === 0) {
paginationOptions.pagination = false
}
}
const result = await Model.paginate(query, paginationOptions)
const docs = JSON.parse(JSON.stringify(result.docs))
return {
...result,
docs: docs.map((doc) => {
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}
return result
}

View File

@@ -1,8 +1,15 @@
import type { CollationOptions, TransactionOptions } from 'mongodb'
import type { MongoMemoryReplSet } from 'mongodb-memory-server'
import type { ClientSession, Connection, ConnectOptions, QueryOptions } from 'mongoose'
import type {
ClientSession,
Connection,
ConnectOptions,
QueryOptions,
SchemaOptions,
} from 'mongoose'
import type {
BaseDatabaseAdapter,
CollectionSlug,
DatabaseAdapterObj,
Payload,
TypeWithID,
@@ -52,6 +59,8 @@ import { upsert } from './upsert.js'
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
export { transform } from './utilities/transform.js'
export interface Args {
/** Set to false to disable auto-pluralization of collection names, Defaults to true */
autoPluralization?: boolean
@@ -79,12 +88,13 @@ export interface Args {
* Defaults to disabled.
*/
collation?: Omit<CollationOptions, 'locale'>
collectionsSchemaOptions?: Partial<Record<CollectionSlug, SchemaOptions>>
/** Extra configuration options */
connectOptions?: {
/** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */
useFacet?: boolean
} & ConnectOptions
/** Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false */
disableIndexHints?: boolean
/**
@@ -103,6 +113,7 @@ export interface Args {
up: (args: MigrateUpArgs) => Promise<void>
}[]
transactionOptions?: false | TransactionOptions
/** The URL to connect to MongoDB or false to start payload and prevent connecting */
url: false | string
}
@@ -163,6 +174,7 @@ declare module 'payload' {
export function mongooseAdapter({
autoPluralization = true,
collectionsSchemaOptions = {},
connectOptions,
disableIndexHints = false,
ensureIndexes,
@@ -194,6 +206,7 @@ export function mongooseAdapter({
versions: {},
// DatabaseAdapter
beginTransaction: transactionOptions === false ? defaultBeginTransaction() : beginTransaction,
collectionsSchemaOptions,
commitTransaction,
connect,
count,

View File

@@ -17,7 +17,9 @@ import { getDBName } from './utilities/getDBName.js'
export const init: Init = function init(this: MongooseAdapter) {
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const schema = buildCollectionSchema(collection, this.payload)
const schemaOptions = this.collectionsSchemaOptions[collection.slug]
const schema = buildCollectionSchema(collection, this.payload, schemaOptions)
if (collection.versions) {
const versionModelName = getDBName({ config: collection, versions: true })
@@ -32,6 +34,7 @@ export const init: Init = function init(this: MongooseAdapter) {
minimize: false,
timestamps: false,
},
...schemaOptions,
})
versionSchema.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true }).plugin(

View File

@@ -1,5 +1,3 @@
import type { PayloadRequest } from 'payload'
import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload'
import prompts from 'prompts'
@@ -45,7 +43,7 @@ export async function migrateFresh(
msg: `Found ${migrationFiles.length} migration files.`,
})
const req = { payload } as PayloadRequest
const req = { payload }
// Run all migrate up
for (const migration of migrationFiles) {

View File

@@ -1,23 +1,23 @@
import type { ClientSession, Model } from 'mongoose'
import type { Field, PayloadRequest, SanitizedConfig } from 'payload'
import type { Field, FlattenedField, PayloadRequest } from 'payload'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
import type { MongooseAdapter } from '../index.js'
import { sanitizeRelationshipIDs } from '../utilities/sanitizeRelationshipIDs.js'
import { withSession } from '../withSession.js'
import { getSession } from '../utilities/getSession.js'
import { transform } from '../utilities/transform.js'
const migrateModelWithBatching = async ({
adapter,
batchSize,
config,
fields,
Model,
session,
}: {
adapter: MongooseAdapter
batchSize: number
config: SanitizedConfig
fields: Field[]
fields: FlattenedField[]
Model: Model<any>
session: ClientSession
}): Promise<void> => {
@@ -47,7 +47,7 @@ const migrateModelWithBatching = async ({
}
for (const doc of docs) {
sanitizeRelationshipIDs({ config, data: doc, fields })
transform({ adapter, data: doc, fields, operation: 'update', validateRelationships: false })
}
await Model.collection.bulkWrite(
@@ -109,15 +109,15 @@ export async function migrateRelationshipsV2_V3({
const db = payload.db as MongooseAdapter
const config = payload.config
const { session } = await withSession(db, req)
const session = await getSession(db, req)
for (const collection of payload.config.collections.filter(hasRelationshipOrUploadField)) {
payload.logger.info(`Migrating collection "${collection.slug}"`)
await migrateModelWithBatching({
adapter: db,
batchSize,
config,
fields: collection.fields,
fields: collection.flattenedFields,
Model: db.collections[collection.slug],
session,
})
@@ -128,9 +128,9 @@ export async function migrateRelationshipsV2_V3({
payload.logger.info(`Migrating collection versions "${collection.slug}"`)
await migrateModelWithBatching({
adapter: db,
batchSize,
config,
fields: buildVersionCollectionFields(config, collection),
fields: buildVersionCollectionFields(config, collection, true),
Model: db.versions[collection.slug],
session,
})
@@ -156,7 +156,13 @@ export async function migrateRelationshipsV2_V3({
// in case if the global doesn't exist in the database yet (not saved)
if (doc) {
sanitizeRelationshipIDs({ config, data: doc, fields: global.fields })
transform({
adapter: db,
data: doc,
fields: global.flattenedFields,
operation: 'update',
validateRelationships: false,
})
await GlobalsModel.collection.updateOne(
{
@@ -173,9 +179,9 @@ export async function migrateRelationshipsV2_V3({
payload.logger.info(`Migrating global versions "${global.slug}"`)
await migrateModelWithBatching({
adapter: db,
batchSize,
config,
fields: buildVersionGlobalFields(config, global),
fields: buildVersionGlobalFields(config, global, true),
Model: db.versions[global.slug],
session,
})

View File

@@ -3,12 +3,12 @@ import type { Payload, PayloadRequest } from 'payload'
import type { MongooseAdapter } from '../index.js'
import { withSession } from '../withSession.js'
import { getSession } from '../utilities/getSession.js'
export async function migrateVersionsV1_V2({ req }: { req: PayloadRequest }) {
const { payload } = req
const { session } = await withSession(payload.db as MongooseAdapter, req)
const session = await getSession(payload.db as MongooseAdapter, req)
// For each collection
@@ -106,12 +106,12 @@ async function migrateCollectionDocs({
return
}
const remainingDocIds = remainingDocs.map((doc) => doc._versionID)
const remainingDocIDs = remainingDocs.map((doc) => doc._versionID)
await VersionsModel.updateMany(
{
_id: {
$in: remainingDocIds,
$in: remainingDocIDs,
},
},
{

View File

@@ -1,3 +1,4 @@
import type { ClientSession } from 'mongodb'
import type { FlattenedField, Payload, Where } from 'payload'
import { parseParams } from './parseParams.js'
@@ -8,6 +9,7 @@ export async function buildAndOrConditions({
globalSlug,
locale,
payload,
session,
where,
}: {
collectionSlug?: string
@@ -15,6 +17,7 @@ export async function buildAndOrConditions({
globalSlug?: string
locale?: string
payload: Payload
session?: ClientSession
where: Where[]
}): Promise<Record<string, unknown>[]> {
const completedConditions = []
@@ -30,6 +33,7 @@ export async function buildAndOrConditions({
globalSlug,
locale,
payload,
session,
where: condition,
})
if (Object.keys(result).length > 0) {

View File

@@ -1,7 +1,6 @@
import type { ClientSession } from 'mongodb'
import type { FlattenedField, Payload, Where } from 'payload'
import { QueryError } from 'payload'
import { parseParams } from './parseParams.js'
type GetBuildQueryPluginArgs = {
@@ -13,6 +12,7 @@ export type BuildQueryArgs = {
globalSlug?: string
locale?: string
payload: Payload
session?: ClientSession
where: Where
}
@@ -28,6 +28,7 @@ export const getBuildQueryPlugin = ({
globalSlug,
locale,
payload,
session,
where,
}: BuildQueryArgs): Promise<Record<string, unknown>> {
let fields = versionsFields
@@ -41,20 +42,17 @@ export const getBuildQueryPlugin = ({
fields = collectionConfig.flattenedFields
}
}
const errors = []
const result = await parseParams({
collectionSlug,
fields,
globalSlug,
locale,
payload,
session,
where,
})
if (errors.length > 0) {
throw new QueryError(errors)
}
return result
}
modifiedSchema.statics.buildQuery = buildQuery

View File

@@ -1,3 +1,4 @@
import type { ClientSession, FindOptions } from 'mongodb'
import type { FlattenedField, Operator, PathToQuery, Payload } from 'payload'
import { Types } from 'mongoose'
@@ -15,9 +16,11 @@ type SearchParam = {
value?: unknown
}
const subQueryOptions = {
lean: true,
const subQueryOptions: FindOptions = {
limit: 50,
projection: {
_id: true,
},
}
/**
@@ -31,6 +34,7 @@ export async function buildSearchParam({
locale,
operator,
payload,
session,
val,
}: {
collectionSlug?: string
@@ -40,6 +44,7 @@ export async function buildSearchParam({
locale?: string
operator: string
payload: Payload
session?: ClientSession
val: unknown
}): Promise<SearchParam> {
// Replace GraphQL nested field double underscore formatting
@@ -87,6 +92,7 @@ export async function buildSearchParam({
const sanitizedQueryValue = sanitizeQueryValue({
field,
hasCustomID,
locale,
operator,
path,
payload,
@@ -133,17 +139,14 @@ export async function buildSearchParam({
},
})
const result = await SubModel.find(subQuery, subQueryOptions)
const result = await SubModel.collection
.find(subQuery, { session, ...subQueryOptions })
.toArray()
const $in: unknown[] = []
result.forEach((doc) => {
const stringID = doc._id.toString()
$in.push(stringID)
if (Types.ObjectId.isValid(stringID)) {
$in.push(doc._id)
}
$in.push(doc._id)
})
if (pathsToQuery.length === 1) {
@@ -161,7 +164,9 @@ export async function buildSearchParam({
}
const subQuery = priorQueryResult.value
const result = await SubModel.find(subQuery, subQueryOptions)
const result = await SubModel.collection
.find(subQuery, { session, ...subQueryOptions })
.toArray()
const $in = result.map((doc) => doc._id)

View File

@@ -11,20 +11,13 @@ type Args = {
timestamps: boolean
}
export type SortArgs = {
direction: SortDirection
property: string
}[]
export type SortDirection = 'asc' | 'desc'
export const buildSortParam = ({
config,
fields,
locale,
sort,
timestamps,
}: Args): PaginateOptions['sort'] => {
}: Args): Record<string, -1 | 1> => {
if (!sort) {
if (timestamps) {
sort = '-createdAt'
@@ -37,15 +30,15 @@ export const buildSortParam = ({
sort = [sort]
}
const sorting = sort.reduce<PaginateOptions['sort']>((acc, item) => {
const sorting = sort.reduce<Record<string, -1 | 1>>((acc, item) => {
let sortProperty: string
let sortDirection: SortDirection
let sortDirection: -1 | 1
if (item.indexOf('-') === 0) {
sortProperty = item.substring(1)
sortDirection = 'desc'
sortDirection = -1
} else {
sortProperty = item
sortDirection = 'asc'
sortDirection = 1
}
if (sortProperty === 'id') {
acc['_id'] = sortDirection

View File

@@ -1,6 +1,6 @@
import type { FlattenedField, SanitizedConfig } from 'payload'
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared'
import { fieldAffectsData } from 'payload/shared'
type Args = {
config: SanitizedConfig
@@ -33,7 +33,7 @@ export const getLocalizedSortProperty = ({
(field) => fieldAffectsData(field) && field.name === firstSegment,
)
if (matchedField && !fieldIsPresentationalOnly(matchedField)) {
if (matchedField) {
let nextFields: FlattenedField[]
const remainingSegments = [...segments]
let localizedSegment = matchedField.name

View File

@@ -1,3 +1,4 @@
import type { ClientSession } from 'mongodb'
import type { FilterQuery } from 'mongoose'
import type { FlattenedField, Operator, Payload, Where } from 'payload'
@@ -13,6 +14,7 @@ export async function parseParams({
globalSlug,
locale,
payload,
session,
where,
}: {
collectionSlug?: string
@@ -20,6 +22,7 @@ export async function parseParams({
globalSlug?: string
locale: string
payload: Payload
session?: ClientSession
where: Where
}): Promise<Record<string, unknown>> {
let result = {} as FilterQuery<any>
@@ -62,6 +65,7 @@ export async function parseParams({
locale,
operator,
payload,
session,
val: pathOperators[operator],
})

View File

@@ -6,6 +6,7 @@ import { createArrayFromCommaDelineated } from 'payload'
type SanitizeQueryValueArgs = {
field: FlattenedField
hasCustomID: boolean
locale?: string
operator: string
path: string
payload: Payload
@@ -36,6 +37,22 @@ const buildExistsQuery = (formattedValue, path, treatEmptyString = true) => {
}
}
const sanitizeCoordinates = (coordinates: unknown[]): unknown[] => {
const result: unknown[] = []
for (const value of coordinates) {
if (typeof value === 'string') {
result.push(Number(value))
} else if (Array.isArray(value)) {
result.push(sanitizeCoordinates(value))
} else {
result.push(value)
}
}
return result
}
// returns nestedField Field object from blocks.nestedField path because getLocalizedPaths splits them only for relationships
const getFieldFromSegments = ({
field,
@@ -74,6 +91,7 @@ const getFieldFromSegments = ({
export const sanitizeQueryValue = ({
field,
hasCustomID,
locale,
operator,
path,
payload,
@@ -205,11 +223,34 @@ export const sanitizeQueryValue = ({
formattedValue.value = new Types.ObjectId(value)
}
let localizedPath = path
if (field.localized && payload.config.localization && locale) {
localizedPath = `${path}.${locale}`
}
return {
rawQuery: {
$and: [
{ [`${path}.value`]: { $eq: formattedValue.value } },
{ [`${path}.relationTo`]: { $eq: formattedValue.relationTo } },
$or: [
{
[localizedPath]: {
$eq: {
// disable auto sort
/* eslint-disable */
value: formattedValue.value,
relationTo: formattedValue.relationTo,
/* eslint-enable */
},
},
},
{
[localizedPath]: {
$eq: {
relationTo: formattedValue.relationTo,
value: formattedValue.value,
},
},
},
],
},
}
@@ -334,6 +375,14 @@ export const sanitizeQueryValue = ({
}
if (operator === 'within' || operator === 'intersects') {
if (
formattedValue &&
typeof formattedValue === 'object' &&
Array.isArray(formattedValue.coordinates)
) {
formattedValue.coordinates = sanitizeCoordinates(formattedValue.coordinates)
}
formattedValue = {
$geometry: formattedValue,
}

View File

@@ -1,43 +1,29 @@
import type { PaginateOptions } from 'mongoose'
import type { PayloadRequest, QueryDrafts } from 'payload'
import type { CollationOptions } from 'mongodb'
import type { QueryDrafts } from 'payload'
import { buildVersionCollectionFields, combineQueries, flattenWhereToOperators } from 'payload'
import { buildVersionCollectionFields, combineQueries } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { withSession } from './withSession.js'
import { findMany } from './utilities/findMany.js'
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js'
export const queryDrafts: QueryDrafts = async function queryDrafts(
this: MongooseAdapter,
{
collection,
joins,
limit,
locale,
page,
pagination,
req = {} as PayloadRequest,
select,
sort: sortArg,
where,
},
{ collection, joins, limit, locale, page, pagination, req, select, sort: sortArg, where },
) {
const VersionModel = this.versions[collection]
const collectionConfig = this.payload.collections[collection].config
const options = await withSession(this, req)
const session = await getSession(this, req)
let hasNearConstraint
const hasNearConstraint = getHasNearConstraint(where)
let sort
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
if (!hasNearConstraint) {
sort = buildSortParam({
config: this.payload.config,
@@ -53,95 +39,65 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
const versionQuery = await VersionModel.buildQuery({
locale,
payload: this.payload,
session,
where: combinedWhere,
})
const versionFields = buildVersionCollectionFields(this.payload.config, collectionConfig, true)
const projection = buildProjectionFromSelect({
adapter: this,
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
fields: versionFields,
select,
})
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
const useEstimatedCount =
hasNearConstraint || !versionQuery || Object.keys(versionQuery).length === 0
const paginationOptions: PaginateOptions = {
lean: true,
leanWithId: true,
options,
page,
pagination,
projection,
sort,
useEstimatedCount,
}
if (this.collation) {
const defaultLocale = 'en'
paginationOptions.collation = {
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
...this.collation,
}
}
const collation: CollationOptions | undefined = this.collation
? {
locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en',
...this.collation,
}
: undefined
if (
!useEstimatedCount &&
Object.keys(versionQuery).length === 0 &&
this.disableIndexHints !== true
) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(
VersionModel.countDocuments(versionQuery, {
hint: { _id: 1 },
}),
)
}
}
if (limit > 0) {
paginationOptions.limit = limit
// limit must also be set here, it's ignored when pagination is false
paginationOptions.options.limit = limit
}
let result
const aggregate = await buildJoinAggregation({
const joinAgreggation = await buildJoinAggregation({
adapter: this,
collection,
collectionConfig,
joins,
locale,
projection,
query: versionQuery,
session,
versions: true,
})
// build join aggregation
if (aggregate) {
result = await VersionModel.aggregatePaginate(
VersionModel.aggregate(aggregate),
paginationOptions,
)
} else {
result = await VersionModel.paginate(versionQuery, paginationOptions)
const result = await findMany({
adapter: this,
collation,
collection: VersionModel.collection,
joinAgreggation,
limit,
page,
pagination,
projection,
query: versionQuery,
session,
sort,
useEstimatedCount,
})
transform({
adapter: this,
data: result.docs,
fields: versionFields,
operation: 'read',
})
for (let i = 0; i < result.docs.length; i++) {
const id = result.docs[i].parent
result.docs[i] = result.docs[i].version
result.docs[i].id = id
}
const docs = JSON.parse(JSON.stringify(result.docs))
return {
...result,
docs: docs.map((doc) => {
doc = {
_id: doc.parent,
id: doc.parent,
...doc.version,
}
return sanitizeInternalFields(doc)
}),
}
return result
}

View File

@@ -1,47 +1,45 @@
import type { QueryOptions } from 'mongoose'
import type { PayloadRequest, UpdateGlobal } from 'payload'
import type { UpdateGlobal } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js'
export const updateGlobal: UpdateGlobal = async function updateGlobal(
this: MongooseAdapter,
{ slug, data, options: optionsArgs = {}, req = {} as PayloadRequest, select },
{ slug, data, options: optionsArgs = {}, req, select },
) {
const Model = this.globals
const fields = this.payload.config.globals.find((global) => global.slug === slug).fields
const fields = this.payload.config.globals.find((global) => global.slug === slug).flattenedFields
const options: QueryOptions = {
...optionsArgs,
...(await withSession(this, req)),
lean: true,
new: true,
projection: buildProjectionFromSelect({
adapter: this,
fields: this.payload.config.globals.find((global) => global.slug === slug).flattenedFields,
select,
}),
}
const session = await getSession(this, req)
let result
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
transform({
adapter: this,
data,
fields,
operation: 'update',
timestamps: optionsArgs.timestamps !== false,
})
result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options)
const result: any = await Model.collection.findOneAndUpdate(
{ globalType: slug },
{ $set: data },
{
...optionsArgs,
projection: buildProjectionFromSelect({ adapter: this, fields, select }),
returnDocument: 'after',
session,
},
)
result = JSON.parse(JSON.stringify(result))
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
transform({
adapter: this,
data: result,
fields,
operation: 'read',
})
return result
}

View File

@@ -1,17 +1,10 @@
import type { QueryOptions } from 'mongoose'
import {
buildVersionGlobalFields,
type PayloadRequest,
type TypeWithID,
type UpdateGlobalVersionArgs,
} from 'payload'
import { buildVersionGlobalFields, type TypeWithID, type UpdateGlobalVersionArgs } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js'
export async function updateGlobalVersion<T extends TypeWithID>(
this: MongooseAdapter,
@@ -20,7 +13,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
global: globalSlug,
locale,
options: optionsArgs = {},
req = {} as PayloadRequest,
req,
select,
versionData,
where,
@@ -28,44 +21,50 @@ export async function updateGlobalVersion<T extends TypeWithID>(
) {
const VersionModel = this.versions[globalSlug]
const whereToUse = where || { id: { equals: id } }
const fields = buildVersionGlobalFields(
this.payload.config,
this.payload.config.globals.find((global) => global.slug === globalSlug),
true,
)
const currentGlobal = this.payload.config.globals.find((global) => global.slug === globalSlug)
const fields = buildVersionGlobalFields(this.payload.config, currentGlobal)
const options: QueryOptions = {
...optionsArgs,
...(await withSession(this, req)),
lean: true,
new: true,
projection: buildProjectionFromSelect({
adapter: this,
fields: buildVersionGlobalFields(this.payload.config, currentGlobal, true),
select,
}),
}
const session = await getSession(this, req)
const query = await VersionModel.buildQuery({
locale,
payload: this.payload,
session,
where: whereToUse,
})
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
transform({
adapter: this,
data: versionData,
fields,
operation: 'update',
timestamps: optionsArgs.timestamps !== false,
})
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
const doc: any = await VersionModel.collection.findOneAndUpdate(
query,
{ $set: versionData },
{
...optionsArgs,
projection: buildProjectionFromSelect({
adapter: this,
fields,
select,
}),
returnDocument: 'after',
session,
},
)
const result = JSON.parse(JSON.stringify(doc))
transform({
adapter: this,
data: doc,
fields,
operation: 'read',
})
const verificationToken = doc._verificationToken
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
return doc
}

View File

@@ -1,65 +1,57 @@
import type { QueryOptions } from 'mongoose'
import type { PayloadRequest, UpdateOne } from 'payload'
import type { UpdateOne } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { handleError } from './utilities/handleError.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
import { transform } from './utilities/transform.js'
export const updateOne: UpdateOne = async function updateOne(
this: MongooseAdapter,
{
id,
collection,
data,
locale,
options: optionsArgs = {},
req = {} as PayloadRequest,
select,
where: whereArg,
},
{ id, collection, data, locale, options: optionsArgs = {}, req, select, where: whereArg },
) {
const where = id ? { id: { equals: id } } : whereArg
const Model = this.collections[collection]
const fields = this.payload.collections[collection].config.fields
const options: QueryOptions = {
...optionsArgs,
...(await withSession(this, req)),
lean: true,
new: true,
projection: buildProjectionFromSelect({
adapter: this,
fields: this.payload.collections[collection].config.flattenedFields,
select,
}),
}
const fields = this.payload.collections[collection].config.flattenedFields
const session = await getSession(this, req)
const query = await Model.buildQuery({
locale,
payload: this.payload,
session,
where,
})
let result
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
transform({
adapter: this,
data,
fields,
operation: 'update',
timestamps: optionsArgs.timestamps !== false,
})
try {
result = await Model.findOneAndUpdate(query, sanitizedData, options)
const result = await Model.collection.findOneAndUpdate(
query,
{ $set: data },
{
...optionsArgs,
projection: buildProjectionFromSelect({ adapter: this, fields, select }),
returnDocument: 'after',
session,
},
)
transform({
adapter: this,
data: result,
fields,
operation: 'read',
})
return result
} catch (error) {
handleError({ collection, error, req })
}
result = JSON.parse(JSON.stringify(result))
result.id = result._id
result = sanitizeInternalFields(result)
return result
}

View File

@@ -1,71 +1,65 @@
import type { QueryOptions } from 'mongoose'
import { buildVersionCollectionFields, type PayloadRequest, type UpdateVersion } from 'payload'
import { buildVersionCollectionFields, type UpdateVersion } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js'
export const updateVersion: UpdateVersion = async function updateVersion(
this: MongooseAdapter,
{
id,
collection,
locale,
options: optionsArgs = {},
req = {} as PayloadRequest,
select,
versionData,
where,
},
{ id, collection, locale, options: optionsArgs = {}, req, select, versionData, where },
) {
const VersionModel = this.versions[collection]
const whereToUse = where || { id: { equals: id } }
const fields = buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collection].config,
true,
)
const options: QueryOptions = {
...optionsArgs,
...(await withSession(this, req)),
lean: true,
new: true,
projection: buildProjectionFromSelect({
adapter: this,
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collection].config,
true,
),
select,
}),
}
const session = await getSession(this, req)
const query = await VersionModel.buildQuery({
locale,
payload: this.payload,
session,
where: whereToUse,
})
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
transform({
adapter: this,
data: versionData,
fields,
operation: 'update',
timestamps: optionsArgs.timestamps !== false,
})
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
const doc = await VersionModel.collection.findOneAndUpdate(
query,
{ $set: versionData },
{
...optionsArgs,
projection: buildProjectionFromSelect({
adapter: this,
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collection].config,
true,
),
select,
}),
returnDocument: 'after',
session,
},
)
const result = JSON.parse(JSON.stringify(doc))
transform({
adapter: this,
data: doc,
fields,
operation: 'read',
})
const verificationToken = doc._verificationToken
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
return doc as any
}

View File

@@ -1,10 +1,10 @@
import type { PayloadRequest, Upsert } from 'payload'
import type { Upsert } from 'payload'
import type { MongooseAdapter } from './index.js'
export const upsert: Upsert = async function upsert(
this: MongooseAdapter,
{ collection, data, locale, req = {} as PayloadRequest, select, where },
{ collection, data, locale, req, select, where },
) {
return this.updateOne({ collection, data, locale, options: { upsert: true }, req, select, where })
}

View File

@@ -1,3 +1,4 @@
import type { ClientSession } from 'mongodb'
import type { PipelineStage } from 'mongoose'
import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload'
@@ -10,12 +11,9 @@ type BuildJoinAggregationArgs = {
collection: CollectionSlug
collectionConfig: SanitizedCollectionConfig
joins: JoinQuery
// the number of docs to get at the top collection level
limit?: number
locale: string
projection?: Record<string, true>
// the where clause for the top collection
query?: Where
session?: ClientSession
/** whether the query is from drafts */
versions?: boolean
}
@@ -25,10 +23,9 @@ export const buildJoinAggregation = async ({
collection,
collectionConfig,
joins,
limit,
locale,
projection,
query,
session,
versions,
}: BuildJoinAggregationArgs): Promise<PipelineStage[] | undefined> => {
if (Object.keys(collectionConfig.joins).length === 0 || joins === false) {
@@ -36,23 +33,7 @@ export const buildJoinAggregation = async ({
}
const joinConfig = adapter.payload.collections[collection].config.joins
const aggregate: PipelineStage[] = [
{
$sort: { createdAt: -1 },
},
]
if (query) {
aggregate.push({
$match: query,
})
}
if (limit) {
aggregate.push({
$limit: limit,
})
}
const aggregate: PipelineStage[] = []
for (const slug of Object.keys(joinConfig)) {
for (const join of joinConfig[slug]) {
@@ -72,26 +53,25 @@ export const buildJoinAggregation = async ({
where: whereJoin,
} = joins?.[join.joinPath] || {}
const sort = buildSortParam({
const $sort = buildSortParam({
config: adapter.payload.config,
fields: adapter.payload.collections[slug].config.flattenedFields,
locale,
sort: sortJoin,
timestamps: true,
})
const sortProperty = Object.keys(sort)[0]
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
const $match = await joinModel.buildQuery({
locale,
payload: adapter.payload,
session,
where: whereJoin,
})
const pipeline: Exclude<PipelineStage, PipelineStage.Merge | PipelineStage.Out>[] = [
{ $match },
{
$sort: { [sortProperty]: sortDirection },
$sort,
},
]
@@ -101,6 +81,11 @@ export const buildJoinAggregation = async ({
})
}
let polymorphicSuffix = ''
if (Array.isArray(join.targetField.relationTo)) {
polymorphicSuffix = '.value'
}
if (adapter.payload.config.localization && locale === 'all') {
adapter.payload.config.localization.localeCodes.forEach((code) => {
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${code}`
@@ -109,7 +94,7 @@ export const buildJoinAggregation = async ({
{
$lookup: {
as: `${as}.docs`,
foreignField: `${join.field.on}${code}`,
foreignField: `${join.field.on}${code}${polymorphicSuffix}`,
from: adapter.collections[slug].collection.name,
localField: versions ? 'parent' : '_id',
pipeline,
@@ -150,7 +135,7 @@ export const buildJoinAggregation = async ({
{
$lookup: {
as: `${as}.docs`,
foreignField: `${join.field.on}${localeSuffix}`,
foreignField: `${join.field.on}${localeSuffix}${polymorphicSuffix}`,
from: adapter.collections[slug].collection.name,
localField: versions ? 'parent' : '_id',
pipeline,
@@ -184,8 +169,8 @@ export const buildJoinAggregation = async ({
}
}
if (projection) {
aggregate.push({ $project: projection })
if (!aggregate.length) {
return
}
return aggregate

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