Compare commits

..

93 Commits

Author SHA1 Message Date
Elliot DeNolf
dc7c952ace chore(release): db-postgres/0.8.6 [skip ci] 2024-09-04 17:08:44 -04:00
Elliot DeNolf
c8a659cd39 chore(release): payload/2.28.0 [skip ci] 2024-09-04 17:06:53 -04:00
Kendell Joseph
6ba293c0f8 feat: collections can use custom database operations (#7675)
## Description
Adds option to override default database operations for a collection

```ts
import { CollectionConfig } from 'payload/types';

export const Collection: CollectionConfig = {
  slug: 'example-collection',
  // Database operations for this collection  
  db: {
    create: () => {},
    deleteMany: () => {},
    deleteOne: () => {},
    find: () => {},
    findOne: () => {},
    updateOne: () => {}
  },
  fields: [
    {
      name: 'someField',
      type: 'text',
    },
  ],
}
```
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2024-09-04 16:44:40 -04:00
Sasha
96a624ad5c fix(db-postgres): query hasMany text/number in array/blocks (#8033)
## Description

Fixes https://github.com/payloadcms/payload/issues/7671
Copy of https://github.com/payloadcms/payload/pull/8003 to 2.0

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

## Type of change

<!-- Please delete options that are not relevant. -->
- [x] Bug fix (non-breaking change which fixes an issue)
## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-09-04 11:52:40 -04:00
Himanshu
545949dafc docs: fix typo (modifing => modifying) (#7846) 2024-09-01 18:58:41 -04:00
Bruno Crosier
d9f61bbdc8 chore: update incorrect plurality in Italian translations (#7866)
Currently the `{{label}}` in `noResults` translation is a plural.

In the italian translation, the words around `{{label}}` imply that it
would be singular.

This fixes it so that the translation works for a pluralized label

Before (incorrect)

<img width="1140" alt="image"
src="https://github.com/user-attachments/assets/40c62d79-4bc6-4523-9f7c-c07808e7e79f">

ChatGPT confirming it's currently incorrect:
https://chatgpt.com/share/477a3d53-d988-4416-afbf-eab4455779e2
2024-09-01 18:57:52 -04:00
DragnovDC
be06579b3e chore(templates): change import in ecommerce template to be type-only (#8019)
Linter Error: Import 'Header' conflicts with local value, so must be
declared with a type-only import when 'isolatedModules' is
enabled.ts(2865)
2024-09-01 18:51:01 -04:00
Jayce Pulsipher
25e9bc62db fix(db-postgres): migration exit codes (#7873)
## Description

Fixes #7031 for v2

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

## Type of change

- [X] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] Existing test suite passes locally with my changes
- [ ] I have made corresponding changes to the documentation

Co-authored-by: Jayce Pulsipher <jpulsipher@nav.com>
2024-08-30 12:15:17 -04:00
Elliot DeNolf
aca567634b chore(release): plugin-cloud/3.0.2 [skip ci] 2024-08-28 12:34:20 -04:00
Elliot DeNolf
1f0934877c fix(plugin-cloud): better logging on static handler (#7924)
Better logging on static handler.
2024-08-28 12:28:47 -04:00
Elliot DeNolf
61da010991 chore: update v3 issue template (#7901)
Mention `payload info` command when providing versions, which is now a
textarea.

Note: that individual version fields are now gone and are no longer
required/enforced. We may need to come up with a better solution if
issues are being submitted without this info.
2024-08-27 22:48:11 -04:00
Elliot DeNolf
ab9074220a chore(release): payload/2.27.0 [skip ci] 2024-08-26 14:01:47 -04:00
Paul
afa90a4362 chore: update docs for stripe plugin webhook (#7763)
Closes https://github.com/payloadcms/payload/issues/7740
2024-08-19 13:12:24 -06:00
Elliot DeNolf
bc0516da90 chore(dependabot): add .github/actions dir 2024-08-14 22:20:42 -04:00
Elliot DeNolf
46daf473c8 chore(dependabot): add .github/workflows dir 2024-08-14 22:02:01 -04:00
Elliot DeNolf
337b8ccbf3 chore: add packageManager property for dependabot 2024-08-14 21:52:06 -04:00
Elliot DeNolf
ba2e4c278f chore: remove explicit dependabot versioning-strategy 2024-08-14 21:23:02 -04:00
Elliot DeNolf
3196036ae9 chore: dependabot time format 2024-08-14 21:19:43 -04:00
Elliot DeNolf
9bc3ad5159 chore: add dependabot.yml 2024-08-14 21:17:28 -04:00
Alessio Gravili
94d18e8d74 feat: upgrade react-toastify dependency, and upgrade to pnpm v9 in our monorepo (#7667) 2024-08-14 20:05:04 -04:00
Patrik
c624eea0d8 fix: update state of field if either valid status or errorMessage changes (#7632)
## Description

Fixes #6413 

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

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-08-13 11:23:51 -04:00
Paul
f97627092c feat: add support for custom image size file names (#7637)
Add support for custom file names in images sizes

```ts
{
  name: 'thumbnail',
  width: 400,
  height: 300,
  generateImageName: ({ height, sizeName, extension, width }) => {
    return `custom-${sizeName}-${height}-${width}.${extension}`
  },
}
```
2024-08-12 14:36:09 -06:00
Elliot DeNolf
f00183029e chore(release): richtext-lexical/0.11.3 [skip ci] 2024-08-09 09:39:40 -04:00
Elliot DeNolf
b6c5aaa966 chore(release): db-mongodb/1.7.2 [skip ci] 2024-08-09 09:39:18 -04:00
Elliot DeNolf
517aaa0665 chore(release): payload/2.26.0 [skip ci] 2024-08-09 09:37:40 -04:00
Jarrod Flesch
2c2ffe406f chore: allow password to be mutated by hooks (#7537)
Fixes https://github.com/payloadcms/payload/issues/7531

Allows passwords to be updated in hooks.
2024-08-09 09:27:09 -04:00
James Mikrut
7f39afa192 feat: adds classnames to edit, list views (#7595)
## Description

Adds classnames to List and Edit views to be able to more easily target
individual entity views via CSS / similar.

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

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-08-08 19:44:09 -04:00
Patrik
fc4d24aa88 fix: render singular label for ArrayCell when length is 1 (#7585)
## Description

Fixes #6099

![Screenshot 2024-08-08 at 2 40
25 PM](https://github.com/user-attachments/assets/0a7ac732-adfe-456b-80c6-1e4b6ce4c4c8)

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

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-08-08 15:44:35 -04:00
Patrik
efa56cefc1 fix: filtering by non-poly relationships with not_equals operator (#7573)
## Description

Fixes #5212

Fixes #6278 

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

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-08-08 11:22:47 -04:00
Patrik
907d7d1d3a fix: filtering by polymorphic relationships with drafts enabled (#7565)
## Description

Fixes #6880 

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

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-08-07 15:31:47 -04:00
Patrik
eca1517237 fix: deprecated inflight package (#6558)
Fixes #6492
2024-08-07 10:32:17 -04:00
Patrik
9865ae998b fix: enable relationship & upload field population in versions (#7533) 2024-08-06 12:09:53 -04:00
Patrik
1a0ef4824b fix: prevents hasMany text going outside of input boundaries (#7454)
## Description

Fixes #6034

`Before`:
![Screenshot 2024-07-31 at 12 26
25 PM](https://github.com/user-attachments/assets/df2cfcda-d81e-42cf-a97d-9552a420b9e8)

`After`:
![Screenshot 2024-07-31 at 12 26
10 PM](https://github.com/user-attachments/assets/fa7c369f-efc3-4aff-95ad-3e2b2525d3c3)

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

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-08-05 17:09:29 -04:00
Radosław Kłos
39e110e633 feat: adds upload's relationship thumbnail (#5015)
## Description

I've made an implementation of the feature requested here:
https://github.com/payloadcms/payload/discussions/3407

Before:
![CleanShot 2024-02-07 at 00 39
47](https://github.com/payloadcms/payload/assets/34719093/4b182118-41bd-47f7-af03-a0b739f7e407)

After:
![CleanShot 2024-02-07 at 00 40
17](https://github.com/payloadcms/payload/assets/34719093/d813de81-bab5-40b2-b31c-5a7ee107dabd)


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

## Type of change

<!-- Please delete options that are not relevant. -->

- [x] New feature (non-breaking change which adds functionality)

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-08-01 15:09:59 +01:00
Paul
3e780b9815 feat(ui): expose custom errors in delete many (#7439)
Exposes any custom errors out to the delete many toast as well.
Closes https://github.com/payloadcms/payload/issues/7214


![image](https://github.com/user-attachments/assets/e5d1fc92-3f22-4906-b09c-e94caf82eb64)
2024-07-31 17:25:23 -04:00
Dan Ribbens
a308d6384f fix(db-postgres): localized array inside blocks field (#7458)
fixes #5240
Copy of https://github.com/payloadcms/payload/pull/7457
2024-07-31 16:31:19 -04:00
Jarrod Flesch
492ed30cb8 chore: fix generic usage, fixes CI (#7421) 2024-07-29 16:28:15 -04:00
Francisco Lourenço
fca5a404db fix: previousValue missing from ValidateOptions type (#6931) 2024-07-29 11:49:19 -04:00
Jason Toups
b13f7e8843 chore: updates all of the Readme localhost:3000 Code to Links (#7252) 2024-07-29 11:22:50 -04:00
Ante
25dfdb66cd chore: croatian translation improvements (#7377) 2024-07-29 11:20:40 -04:00
Patrik
9c9e6896a5 fix(payload): retained date milliseconds (#7393)
## Description

Fixes #6108 

Defaults `milliseconds` to `0` for date field picker.

`Before`:
![Screenshot 2024-07-26 at 3 56
45 PM](https://github.com/user-attachments/assets/1806801a-b457-476e-ad84-bcfe3248b61e)


`After`:
![Screenshot 2024-07-26 at 3 54
14 PM](https://github.com/user-attachments/assets/ad92a106-df95-4184-9de2-666d08b636ab)

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

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-07-26 16:16:45 -04:00
Elliot DeNolf
a3085435ef chore(release): db-mongodb/1.7.1 [skip ci] 2024-07-26 11:38:00 -04:00
Elliot DeNolf
1466657e8f chore(release): payload/2.25.0 [skip ci] 2024-07-26 11:36:14 -04:00
James Mikrut
1348483648 fix: preserves objectids in deepCopyObject (#7385)
## Description

The `deepCopyObject` function was cannibalizing ObjectIDs, which
conflicted with the ability to surface them from the MongoDB adapter.
Now, the `deepCopyObject` function will simply pass through ObjectIDs
rather than break them.
2024-07-26 11:10:16 -04:00
Patrik
5321098d7e fix(payload): properly handles 0 value number fields in list view (#7364)
## Description

Fixes #5510 

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

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-07-25 15:35:35 -04:00
Kendell Joseph
c57591bc4f fix: supports null values in query strings (#5241)
Fixes issue where null values were not being handled properly from client/server

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2024-07-25 13:25:06 -04:00
Patrik
9750bc217e fix(db-mongodb): adds new optional collation feature flag behind mongodb collation option (#7359)
## Description

Fixes #7349 

Adds new `collation` prop to the mongodb adapter config to allow for
enabling the `mongodb` collation feature.

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

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update

## Checklist:

- [x] Existing test suite passes locally with my changes
- [x] I have made corresponding changes to the documentation
2024-07-25 12:37:41 -04:00
Jarrod Flesch
468e5441f1 fix: relaxes equality check for relationship options in filter (#7344)
Fixes https://github.com/payloadcms/payload/issues/7103

When extracting the value from the querystring, it is always a string.
We were using a strict equality check which would cause the filter
options to never find the correct option. This caused an infinite loop
when using PG as ID's are numbers by default.
2024-07-24 15:53:09 -04:00
Patrik
3c5cce4c6f feat(payload): allows metadata to be appended to the file of the output media (#7295)
## Description

V3 PR [here](https://github.com/payloadcms/payload/pull/7293)

`Feat`: Adds new prop `withMetadata` to `uploads` config that allows the
user to allow media metadata to be appended to the file of the output
media.

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

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
- [x] I have made corresponding changes to the documentation
2024-07-24 15:32:54 -04:00
Elliot DeNolf
9f0f94893d chore(release): db-mongodb/1.7.0 [skip ci] 2024-07-24 14:17:35 -04:00
Elliot DeNolf
03b7892fc9 chore(release): payload/2.24.2 [skip ci] 2024-07-24 14:16:09 -04:00
James Mikrut
f96cf593ce feat: add jsonParse flag to mongooseAdapter that preserves existing, untracked MongoDB data types (#7338)
## Description

Preserves external data structures stored in MongoDB by avoiding the use
of `JSON.parse(JSON.stringify(mongooseDoc))`.

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

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-07-24 14:02:06 -04:00
Jarrod Flesch
8259611ce6 fix: fetches and sets permissions before setting user (#7337)
Fixes https://github.com/payloadcms/payload/issues/7330

Ensures permissions are always present before setting the user.
2024-07-24 12:30:10 -04:00
Jacob Fletcher
a3ed25a253 chore(live-preview): ensures dev points to src (#7340) 2024-07-24 12:17:03 -04:00
Jarrod Flesch
69e7b7a158 fix: allow autosave relationship drawers to function properly (#7325)
Fixes https://github.com/payloadcms/payload/issues/6887

Collections with autosave enabled would open and immediately close when
they were edited inside a relationship field. This PR threads onSave
through to autosave and checks the current drawer depth to determine if
it should call the onSave function or if it should redirect the user to
the doc page when autosave is triggered.
2024-07-23 17:00:27 -04:00
Jacob Fletcher
c6da99b4d1 fix(plugin-stripe): properly types async webhooks (#7316) 2024-07-23 13:56:29 -04:00
dependabot[bot]
ebd23caa56 chore(deps): bump ws from 7.5.9 to 7.5.10 in /examples/auth/payload (#7288) 2024-07-22 22:04:48 -04:00
dependabot[bot]
faa9b21824 chore(deps): bump nodemailer from 6.9.4 to 6.9.14 in /examples/email (#7289) 2024-07-22 22:04:31 -04:00
dependabot[bot]
1690560f11 chore(deps): bump braces from 3.0.2 to 3.0.3 in /examples/auth/next-app (#7286) 2024-07-22 22:04:12 -04:00
Patrik
0058660b3f fix(db-mongodb): removes precedence of regular chars over international chars in sort (#6923)
## Description

Fixes #6719 

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

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-07-22 16:55:35 -04:00
Elliot DeNolf
6d7ef919cb chore(release): payload/2.24.1 [skip ci] 2024-07-22 15:59:08 -04:00
Yosuf Ali
abffa37d85 chore(templates): fixes bug in e-commerce (#7258)
## Description

When following the documentation to run the E Commerce template locally,
you are asked to run `yarn stripe:webhooks` to work with webhooks.

However, when checking out your cart and a webhook is triggered, your
terminal receives the following error:

```
[ERROR] Failed to POST: Post "http://localhost:8000/stripe/webhooks": dial tcp 127.0.0.1:8000: connect: connection refused
```

I believe this is because the port is wrong, and it should be port
`3000`. There is no reference to a port `8000` anywhere in the code base
for this template, including in the docker-compose.yml file.

Making this changes allows webhook requests to be forwarded correctly:

```
--> customer.created [evt_...]
<-- [200] POST http://localhost:3000/stripe/webhooks [evt_...]
```

This PR makes this small change.

<!-- Please include a summary of the pull request and any related issues
it fixes. Please also include relevant motivation and context. -->

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

## Type of change

<!-- Please delete options that are not relevant. -->

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] Change to the
[templates](https://github.com/payloadcms/payload/tree/main/templates)
directory (does not affect core functionality)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-07-22 13:43:29 -04:00
Patrik
1b208c7add fix: resizes images first before applying focal point (#7278)
## Description

V3 (original) PR [here](https://github.com/payloadcms/payload/pull/7277)

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

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-07-22 12:28:55 -04:00
Rafał Nawojczyk
2840632161 docs: add warning about forbidden chars in custom ID field (#7059)
## Description

Add a warning text for users in DOCs, that will notify them about
forbidden characters while using `text` as a custom ID.
It resolves #7021 

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

## Type of change

<!-- Please delete options that are not relevant. -->

- [x] Chore (non-breaking change which does not add functionality)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Change to the
[templates](https://github.com/payloadcms/payload/tree/main/templates)
directory (does not affect core functionality)
- [ ] Change to the
[examples](https://github.com/payloadcms/payload/tree/main/examples)
directory (does not affect core functionality)
- [ ] This change requires a documentation update

## Checklist:

- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] Existing test suite passes locally with my changes
- [x] I have made corresponding changes to the documentation
2024-07-21 22:23:03 -04:00
Patrik
0841d5a35e fix: uploads from drawer and focal point positioning (#7244)
## Description

V3 PR [here](https://github.com/payloadcms/payload/pull/7117)

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

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-07-19 15:01:06 -04:00
Jessica Chowdhury
bd19fcf259 chore(docs): expand on reserved field names (#7242)
## Description

Closes #6640

Note: Only updated for v2 as the v3 docs cover this topic already.

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

## Type of change

- [X] Chore (non-breaking change which does not add functionality)
2024-07-19 12:32:34 -04:00
Jacob Fletcher
18645771c8 fix: exports fallback hook types to ensure backwards compatibility (#7217) 2024-07-18 12:34:59 -04:00
Isak ✏ ⇝
20377bb22c fix: aliases AfterMe, AfterLogout, and AfterRefresh hook types (#7146)
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2024-07-18 10:56:56 -04:00
Elliot DeNolf
7daaf3d780 chore(release): db-mongodb/1.6.0 [skip ci] 2024-07-16 14:33:42 -04:00
Elliot DeNolf
667d3dc885 chore(release): payload/2.24.0 [skip ci] 2024-07-16 14:31:53 -04:00
James Mikrut
51474fa661 feat: allows mongoose schemaOptions to be configured (#7099)
## Description

This PR adds the ability to configure Mongoose's `schemaOptions`, which exposes more control about how Mongoose operates internally. For example, you can now disable `strict` mode in Mongoose to be able to preserve / surface data in MongoDB that is not reflected in Payload schemas.

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

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
- [ ] I have made corresponding changes to the documentation
2024-07-16 12:58:55 -04:00
Jessica Chowdhury
d475b16790 chore(docs): minor change to uploads overview (#7147)
## Description

Updates upload overview to remove point about the upload gallery.

- [X] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.
2024-07-15 10:58:09 -04:00
Konsequanzheng
4d0befb67a Correct stripe plugin documentation local use webhook url (#7144)
## Description
The stripe plugin documentation has an error in the forwarding URL for
local development (`/stripe/webhooks` instead of `/api/stripe/webhooks`)

Spent a day debugging my application because the URL in the docs is
wrong. Hoping to save others some time with this correction.

<!-- Please include a summary of the pull request and any related issues
it fixes. Please also include relevant motivation and context. -->

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

## Type of change

<!-- Please delete options that are not relevant. -->

- [x] Documentation correction
2024-07-15 10:01:03 -04:00
Patrik
84d214f992 feat: adds ability to upload files from a remote url (#7087)
Adds new button to uploads labeled `Paste URL` 

![Screenshot 2024-07-08 at 10 46
14 AM](https://github.com/payloadcms/payload/assets/35232443/5024fc20-c860-48e5-bdc8-b69ac3c9cc53)

Upon clicking it, a modal with an input field will appear to where one
can input a remote url of an image.

![Screenshot 2024-07-08 at 10 46
22 AM](https://github.com/payloadcms/payload/assets/35232443/5ea67977-f118-4d34-9dfb-d270b3578262)
2024-07-12 11:41:50 -04:00
Jarrod Flesch
51cd5942fa chore: revert changed localization test suite (#7089) 2024-07-10 08:19:34 -04:00
Mark Aloo
74105d8ee5 feat(templates): add import alias to tsconfig.json (#7051) 2024-07-08 11:39:16 -04:00
Federico Di Luca
1cc61ddab6 docs: update unlock examples to include email parameter (#6920) 2024-06-30 14:02:36 -04:00
Adarsh-Raj-Jaiswal
99397a0bdb fix(docs): small typo in custom components documentation (#6973) 2024-06-30 13:58:45 -04:00
Jarrod Flesch
a5492afad6 fix: ensures access query runs with locale when present (#6981)
Fixes https://github.com/payloadcms/payload/issues/6915
2024-06-28 16:19:34 -04:00
Elliot DeNolf
6c1156e2e4 chore(release): payload/2.23.1 [skip ci] 2024-06-28 12:43:13 -04:00
James Mikrut
77e8ce980e Chore/remove unused refresh arg (#6976)
## Description

The `refresh` operation was accepting a `token` argument, but it was not
being used at all. This PR cleans up that unused logic.
2024-06-28 10:52:41 -04:00
Elliot DeNolf
39e34ce94e chore(release): richtext-lexical/0.11.2 [skip ci] 2024-06-28 09:25:51 -04:00
Elliot DeNolf
2aa2971fb9 chore(release): payload/2.23.0 [skip ci] 2024-06-28 09:24:22 -04:00
James Mikrut
c82d2caa29 feat: adds me and refresh hooks (#6968)
## Description

Duplicate of https://github.com/payloadcms/payload/pull/6965 for 2.x
2024-06-28 09:06:27 -04:00
Alessio Gravili
cf52d64d98 fix(richtext-lexical): html converters unnecessarily growing over time (#6963)
Fixes https://github.com/payloadcms/payload/issues/6962
2024-06-27 11:57:23 -04:00
Elliot DeNolf
320dcc0a08 chore(release): payload/2.22.2 [skip ci] 2024-06-26 14:31:58 -04:00
Elliot DeNolf
ea18735d3b fix: return exp and strategy from auth (#6943)
Expiration and strategy were not being properly sent using the useAuth
hook.
2024-06-26 14:26:17 -04:00
Jacob Fletcher
4baa0e3221 docs: fixes syntax error in access control overview (#6924) 2024-06-25 15:51:21 -04:00
Elliot DeNolf
2d35d695ea chore(release): payload/2.22.1 [skip ci] 2024-06-25 15:21:11 -04:00
James Mikrut
bb911cc7ec fix(payload): #6800, #5108 - graphql query concurrency issues (#6857)
Fixes #6800 and #5108 by improving the `isolateObjectProperty` utility
function and flattening `req.transactionID` and
`req.transactionIDPromise` to a single `req.transactionID` property.
2024-06-25 14:57:50 -04:00
Patrik
874774375f fix: sends cropped image pixel values to server instead of percent values (#6852)
## Description

Fixes #6824 

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

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-06-24 11:28:37 -04:00
Elliot DeNolf
d337c5b523 chore(release): db-mongodb/1.5.2 [skip ci] 2024-06-20 10:22:42 -04:00
Elliot DeNolf
dab632388e chore(release): plugin-cloud-storage/1.1.3 [skip ci] 2024-06-20 10:21:05 -04:00
280 changed files with 17477 additions and 10766 deletions

View File

@@ -9,43 +9,38 @@ body:
description: Want us to look into your issue faster? Follow the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md) for more information.
validations:
required: false
- type: input
id: version
- type: textarea
attributes:
label: Payload Version
description: What version of Payload are you running?
validations:
required: true
- type: input
id: node-version
attributes:
label: Node Version
description: What version of Node are you running?
validations:
required: true
- type: input
id: nextjs-version
attributes:
label: Next.js Version
description: What version of Next.js are you running?
label: Environment Info
description: Paste output from `pnpm payload info` (>= beta.92) _or_ Payload, Node.js, and Next.js versions.
render: text
placeholder: |
Payload:
Node.js:
Next.js:
validations:
required: true
- type: textarea
attributes:
label: Describe the Bug
validations:
required: true
- type: textarea
attributes:
label: Reproduction Steps
description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken.
validations:
required: true
- type: input
id: adapters-plugins
attributes:
label: Adapters and Plugins
description: What adapters and plugins are you using if relevant? ie. db-mongodb, db-postgres, storage-vercel-blob, etc.
- type: markdown
attributes:
value: Before submitting the issue, go through the steps you've written down to make sure the steps provided are detailed and clear.

47
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
# docs: https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: github-actions
directories:
- /
- /.github/workflows
- /.github/actions/* # Not working until resolved: https://github.com/dependabot/dependabot-core/issues/6345
- /.github/actions/setup
target-branch: beta
schedule:
interval: monthly
timezone: America/Detroit
time: '06:00'
groups:
github_actions:
patterns:
- '*'
- package-ecosystem: npm
directory: /
target-branch: beta
schedule:
interval: weekly
day: sunday
timezone: America/Detroit
time: '06:00'
commit-message:
prefix: 'chore(deps)'
labels:
- dependencies
groups:
production:
dependency-type: production
update-types:
- minor
- patch
patterns:
- '*'
dev:
dependency-type: development
update-types:
- minor
- patch
patterns:
- '*'

View File

@@ -61,7 +61,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9.7.0
run_install: false
- name: Get pnpm store directory
@@ -116,7 +116,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9.7.0
run_install: false
- name: Restore build
@@ -201,7 +201,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9.7.0
run_install: false
- name: Restore build
@@ -242,7 +242,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9.7.0
run_install: false
- name: Restore build
@@ -286,7 +286,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9.7.0
run_install: false
- name: Restore build
@@ -327,7 +327,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9.7.0
run_install: false
- name: Restore build

View File

@@ -35,5 +35,9 @@
"eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }],
"typescript.tsdk": "node_modules/typescript/lib",
// Load .git-blame-ignore-revs file
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"]
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
"jestrunner.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" node 'node_modules/jest/bin/jest.js'",
"jestrunner.debugOptions": {
"runtimeArgs": ["--experimental-vm-modules", "--no-deprecation"]
}
}

View File

@@ -1,3 +1,139 @@
## [2.28.0](https://github.com/payloadcms/payload/compare/v2.27.0...v2.28.0) (2024-09-04)
### Features
* collections can use custom database operations ([#7675](https://github.com/payloadcms/payload/issues/7675)) ([6ba293c](https://github.com/payloadcms/payload/commit/6ba293c0f84f91bf89cf089a20e47de130013ebb))
### Bug Fixes
* **db-postgres:** migration exit codes ([#7873](https://github.com/payloadcms/payload/issues/7873)) ([25e9bc6](https://github.com/payloadcms/payload/commit/25e9bc62dbcbabcb3619cf83e3dc0110e0a4cabf)), closes [#7031](https://github.com/payloadcms/payload/issues/7031)
* **db-postgres:** query hasMany text/number in array/blocks ([#8033](https://github.com/payloadcms/payload/issues/8033)) ([96a624a](https://github.com/payloadcms/payload/commit/96a624ad5c5259b197b4ca793d8419d1e827de9c))
* **plugin-cloud:** better logging on static handler ([#7924](https://github.com/payloadcms/payload/issues/7924)) ([1f09348](https://github.com/payloadcms/payload/commit/1f0934877ce5aabb771c936c3677a26d2ef006ec))
## [2.27.0](https://github.com/payloadcms/payload/compare/v2.26.0...v2.27.0) (2024-08-26)
### Features
* add support for custom image size file names ([#7637](https://github.com/payloadcms/payload/issues/7637)) ([f976270](https://github.com/payloadcms/payload/commit/f97627092cabe4eabbebefa75afc53579188386b))
* upgrade react-toastify dependency, and upgrade to pnpm v9 in our monorepo ([#7667](https://github.com/payloadcms/payload/issues/7667)) ([94d18e8](https://github.com/payloadcms/payload/commit/94d18e8d747588efce225cde0b621db9b513e7c1))
### Bug Fixes
* update state of field if either `valid` status or `errorMessage` changes ([#7632](https://github.com/payloadcms/payload/issues/7632)) ([c624eea](https://github.com/payloadcms/payload/commit/c624eea0d868938f4603860fa25be3df580ba7fe)), closes [#6413](https://github.com/payloadcms/payload/issues/6413)
## [2.26.0](https://github.com/payloadcms/payload/compare/v2.25.0...v2.26.0) (2024-08-09)
### Features
* adds classnames to edit, list views ([#7595](https://github.com/payloadcms/payload/issues/7595)) ([7f39afa](https://github.com/payloadcms/payload/commit/7f39afa1928b118451138e811ea71a04fce021d5))
* adds upload's relationship thumbnail ([#5015](https://github.com/payloadcms/payload/issues/5015)) ([39e110e](https://github.com/payloadcms/payload/commit/39e110e6331efff0ca8ca7174780076243a016de))
* **ui:** expose custom errors in delete many ([#7439](https://github.com/payloadcms/payload/issues/7439)) ([3e780b9](https://github.com/payloadcms/payload/commit/3e780b98155550f877021996dd094ba435dff81b))
### Bug Fixes
* **db-postgres:** localized array inside blocks field ([#7458](https://github.com/payloadcms/payload/issues/7458)) ([a308d63](https://github.com/payloadcms/payload/commit/a308d6384f9724c5ff330382070a5803fbcf167c)), closes [#5240](https://github.com/payloadcms/payload/issues/5240)
* deprecated `inflight` package ([#6558](https://github.com/payloadcms/payload/issues/6558)) ([eca1517](https://github.com/payloadcms/payload/commit/eca1517237c78983c192f4bafa92a86d94a0de9e)), closes [#6492](https://github.com/payloadcms/payload/issues/6492)
* enable `relationship` & `upload` field population in `versions` ([#7533](https://github.com/payloadcms/payload/issues/7533)) ([9865ae9](https://github.com/payloadcms/payload/commit/9865ae998b9aeb5d72724023976bb203133e19ff))
* filtering by non-poly `relationships` with `not_equals` operator ([#7573](https://github.com/payloadcms/payload/issues/7573)) ([efa56ce](https://github.com/payloadcms/payload/commit/efa56cefc15a48cd45b3aaba2eddacca79e1be30)), closes [#5212](https://github.com/payloadcms/payload/issues/5212) [#6278](https://github.com/payloadcms/payload/issues/6278)
* filtering by polymorphic `relationships` with `drafts` enabled ([#7565](https://github.com/payloadcms/payload/issues/7565)) ([907d7d1](https://github.com/payloadcms/payload/commit/907d7d1d3a89ed22bb991a1f238bb77d54e3e173)), closes [#6880](https://github.com/payloadcms/payload/issues/6880)
* retained date milliseconds ([#7393](https://github.com/payloadcms/payload/issues/7393)) ([9c9e689](https://github.com/payloadcms/payload/commit/9c9e6896a502de209c6cccf63cc5cfc0f0143bf3)), closes [#6108](https://github.com/payloadcms/payload/issues/6108)
* prevents `hasMany` text going outside of input boundaries ([#7454](https://github.com/payloadcms/payload/issues/7454)) ([1a0ef48](https://github.com/payloadcms/payload/commit/1a0ef4824b3d6548d36e7f28a2030640361c0655)), closes [#6034](https://github.com/payloadcms/payload/issues/6034)
* previousValue missing from ValidateOptions type ([#6931](https://github.com/payloadcms/payload/issues/6931)) ([fca5a40](https://github.com/payloadcms/payload/commit/fca5a404dbf3b440b428e55cf5e03db647f9a453))
* render singular label for `ArrayCell` when length is 1 ([#7585](https://github.com/payloadcms/payload/issues/7585)) ([fc4d24a](https://github.com/payloadcms/payload/commit/fc4d24aa8889ac9be76059a92478d5532b142b5c)), closes [#6099](https://github.com/payloadcms/payload/issues/6099)
## [2.25.0](https://github.com/payloadcms/payload/compare/v2.24.2...v2.25.0) (2024-07-26)
### Features
* allows metadata to be appended to the file of the output media ([#7295](https://github.com/payloadcms/payload/issues/7295)) ([3c5cce4](https://github.com/payloadcms/payload/commit/3c5cce4c6f108f87e87b091bbfec976423de73a2))
* **db-mongodb:** adds new optional `collation` feature flag behind mongodb collation option ([#7359](https://github.com/payloadcms/payload/issues/7359)) ([9750bc2](https://github.com/payloadcms/payload/commit/9750bc217ee7d63732a34908c84eb88b88dac0a8)), closes [#7349](https://github.com/payloadcms/payload/issues/7349)
### Bug Fixes
* properly handles `0` value number fields in list view ([#7364](https://github.com/payloadcms/payload/issues/7364)) ([5321098](https://github.com/payloadcms/payload/commit/5321098d7eada43838f6d5c69f3233c150fe0afa)), closes [#5510](https://github.com/payloadcms/payload/issues/5510)
* preserves objectids in deepCopyObject ([#7385](https://github.com/payloadcms/payload/issues/7385)) ([1348483](https://github.com/payloadcms/payload/commit/134848364801c72cc773ef7b48854306d1b9bac3))
* relaxes equality check for relationship options in filter ([#7344](https://github.com/payloadcms/payload/issues/7344)) ([468e544](https://github.com/payloadcms/payload/commit/468e5441f16775134d915ec7caddb17b817d3408))
* supports null values in query strings ([#5241](https://github.com/payloadcms/payload/issues/5241)) ([c57591b](https://github.com/payloadcms/payload/commit/c57591bc4fb8d28b7de16a111faffea7d3e11f8d))
## [2.24.2](https://github.com/payloadcms/payload/compare/v2.24.1...v2.24.2) (2024-07-24)
### Features
* **db-mongodb:** add jsonParse flag to mongooseAdapter that preserves existing, untracked MongoDB data types ([#7338](https://github.com/payloadcms/payload/issues/7338)) ([f96cf59](https://github.com/payloadcms/payload/commit/f96cf593cedcae0d8ed55f9a70e8e4e77917a876))
### Bug Fixes
* allow autosave relationship drawers to function properly ([#7325](https://github.com/payloadcms/payload/issues/7325)) ([69e7b7a](https://github.com/payloadcms/payload/commit/69e7b7a158c38058ece54a97bfa79e65192774a6))
* **db-mongodb:** removes precedence of regular chars over international chars in sort ([#6923](https://github.com/payloadcms/payload/issues/6923)) ([0058660](https://github.com/payloadcms/payload/commit/0058660b3f8bd820abb4494ff53fa67f49f0f6b4)), closes [#6719](https://github.com/payloadcms/payload/issues/6719)
* fetches and sets permissions before setting user ([#7337](https://github.com/payloadcms/payload/issues/7337)) ([8259611](https://github.com/payloadcms/payload/commit/8259611ce60e23f6298a07564d5f6dd2966d61ff))
* **plugin-stripe:** properly types async webhooks ([#7316](https://github.com/payloadcms/payload/issues/7316)) ([c6da99b](https://github.com/payloadcms/payload/commit/c6da99b4d1b986089bb697486a7825db66323078))
## [2.24.1](https://github.com/payloadcms/payload/compare/v2.24.0...v2.24.1) (2024-07-22)
### Bug Fixes
* aliases AfterMe, AfterLogout, and AfterRefresh hook types ([#7146](https://github.com/payloadcms/payload/issues/7146)) ([20377bb](https://github.com/payloadcms/payload/commit/20377bb22c867552e412c1cafd16869399aadd68))
* exports fallback hook types to ensure backwards compatibility ([#7217](https://github.com/payloadcms/payload/issues/7217)) ([1864577](https://github.com/payloadcms/payload/commit/18645771c86664f1246f0fb599c8265a4cd1d6c0))
* resizes images first before applying focal point ([#7278](https://github.com/payloadcms/payload/issues/7278)) ([1b208c7](https://github.com/payloadcms/payload/commit/1b208c7addf56ae8a1af5e408b001b3e5f080a38))
* uploads from drawer and focal point positioning ([#7244](https://github.com/payloadcms/payload/issues/7244)) ([0841d5a](https://github.com/payloadcms/payload/commit/0841d5a35ee00650c703231a08fc9a361861ba67))
## [2.24.0](https://github.com/payloadcms/payload/compare/v2.23.1...v2.24.0) (2024-07-16)
### Features
* adds ability to upload files from a remote url ([#7087](https://github.com/payloadcms/payload/issues/7087)) ([84d214f](https://github.com/payloadcms/payload/commit/84d214f99207ad78a466b8c16eb36e29f57cd0e3))
* **db-mongodb:** allows mongoose schemaOptions to be configured ([#7099](https://github.com/payloadcms/payload/issues/7099)) ([51474fa](https://github.com/payloadcms/payload/commit/51474fa661ae24ab8fc0d13001fafc0f35216c1e))
### Bug Fixes
* ensures access query runs with locale when present ([#6981](https://github.com/payloadcms/payload/issues/6981)) ([a5492af](https://github.com/payloadcms/payload/commit/a5492afad672e19dd35b1f5370b51f22656f334c))
## [2.23.1](https://github.com/payloadcms/payload/compare/v2.23.0...v2.23.1) (2024-06-28)
### Bug Fixes
* remove unused refresh arg, this affected me/refresh hooks ([#6976](https://github.com/payloadcms/payload/issues/6976)) ([c82d2ca](https://github.com/payloadcms/payload/commit/77e8ce980ef0bcb0380b499dd1ccdfd36199b707))
## [2.23.0](https://github.com/payloadcms/payload/compare/v2.22.2...v2.23.0) (2024-06-28)
### Features
* adds me and refresh hooks ([#6968](https://github.com/payloadcms/payload/issues/6968)) ([c82d2ca](https://github.com/payloadcms/payload/commit/c82d2caa29422083e97affc99a033296d78892d6))
### Bug Fixes
* **richtext-lexical:** html converters unnecessarily growing over time ([#6963](https://github.com/payloadcms/payload/issues/6963)) ([cf52d64](https://github.com/payloadcms/payload/commit/cf52d64d984d98ab782ca33f20b43c935ce60683))
## [2.22.2](https://github.com/payloadcms/payload/compare/v2.22.1...v2.22.2) (2024-06-26)
### Bug Fixes
* return exp and strategy from auth ([#6943](https://github.com/payloadcms/payload/issues/6943)) ([ea18735](https://github.com/payloadcms/payload/commit/ea18735d3b2d2a96989009130e3724aab487e520))
## [2.22.1](https://github.com/payloadcms/payload/compare/v2.22.0...v2.22.1) (2024-06-25)
### Bug Fixes
* graphql query concurrency issues ([#6857](https://github.com/payloadcms/payload/issues/6857)) ([bb911cc](https://github.com/payloadcms/payload/commit/bb911cc7eca1eeef15ade8eb043c0056c281e311))
* sends cropped image pixel values to server instead of percent values ([#6852](https://github.com/payloadcms/payload/issues/6852)) ([8747743](https://github.com/payloadcms/payload/commit/874774375f8beada9bac0a8ef3e77f63adc30834))
## [2.22.0](https://github.com/payloadcms/payload/compare/v2.21.0...v2.22.0) (2024-06-20)

View File

@@ -37,9 +37,12 @@ const defaultPayloadAccess = ({ req: { user } }) => {
<strong>Note:</strong>
<br />
In the Local API, all Access Control functions are skipped by default, allowing your server to do
whatever it needs. But, you can opt back in by setting the option <strong>
whatever it needs. But, you can opt back in by setting the option
{' '}
<strong>
overrideAccess
</strong>{' '}
</strong>
{' '}
to <strong>false</strong>.
</Banner>

View File

@@ -491,7 +491,7 @@ As an alternative to replacing the entire Field component, you may want to keep
| Component | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
| **`Label`** | Override the default Label in the Field Component. [More](#label-component) |
| **`Error`** | Override the default Label in the Field Component. [More](#error-component) |
| **`Error`** | Override the default Error in the Field Component. [More](#error-component) |
| **`beforeInput`** | An array of elements that will be added before `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
| **`afterInput`** | An array of elements that will be added after `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |

View File

@@ -191,7 +191,7 @@ mutation {
### Refresh
Allows for "refreshing" JWTs. If your user has a token that is about to expire, but the user is still active and using the app, you might want to use the `refresh` operation to receive a new token by sending the operation the token that is about to expire.
Allows for "refreshing" JWTs. If your user has a token that is about to expire, but the user is still active and using the app, you might want to use the `refresh` operation to receive a new token by executing this operation via the authenticated user.
This operation requires a non-expired token to send back a new one. If the user's token has already expired, you will need to allow them to log in again to retrieve a new token.
@@ -237,13 +237,6 @@ mutation {
}
```
<Banner type="success">
The Refresh operation will automatically find the user's token in either a JWT header or the
HTTP-only cookie. But, you can specify the token you're looking to refresh by providing the REST
API with a `token` within the JSON body of the request, or by providing the GraphQL resolver a
`token` arg.
</Banner>
### Verify by Email
If your collection supports email verification, the Verify operation will be exposed which accepts a verification token and sets the user's `_verified` property to `true`, thereby allowing the user to authenticate with the Payload API.
@@ -290,6 +283,9 @@ const res = await fetch(`http://localhost:3000/api/[collection-slug]/unlock`, {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'dev@payloadcms.com',
}),
})
```
@@ -297,7 +293,7 @@ const res = await fetch(`http://localhost:3000/api/[collection-slug]/unlock`, {
```
mutation {
unlock[collection-singular-label]
unlock[collection-singular-label](email: "dev@payloadcms.com")
}
```
@@ -306,6 +302,9 @@ mutation {
```ts
const result = await payload.unlock({
collection: '[collection-slug]',
data: {
email: 'dev@payloadcms.com',
},
})
```

View File

@@ -30,13 +30,18 @@ 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. | |
| Option | Description |
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
| `schemaOptions` | Customize schema options for all Mongoose schemas created internally. |
| `jsonParse` | Set to false to disable the automatic JSON stringify/parse of data queried by MongoDB. For example, if you have data not tracked by Payload such as `Date` fields and similar, you can use this option to ensure that existing `Date` properties remain as `Date` and not strings. |
| `collections` | Options on a collection-by-collection basis. [More](#collections-options) |
| `globals` | Options for the Globals collection created by Payload. [More](#globals-options) |
| `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/). |
### Access to Mongoose models
@@ -48,3 +53,51 @@ You can access Mongoose models as follows:
- Collection models - `payload.db.collections[myCollectionSlug]`
- Globals model - `payload.db.globals`
- Versions model (both collections and globals) - `payload.db.versions[myEntitySlug]`
### Collections Options
You can configure the way the MongoDB adapter works on a collection-by-collection basis, including customizing Mongoose `schemaOptions` for each collection schema created.
Example:
```ts
const db = mongooseAdapter({
url: 'your-url-here',
collections: {
users: {
//
schemaOptions: {
strict: false,
}
}
}
})
```
### Global Options
Payload automatically creates a single `globals` collection that correspond with any Payload globals that you define. When you initialize the `mongooseAdapter`, you can specify settings here for your globals in a similar manner to how you can for collections above. Right now, the only property available is `schemaOptions` but more may be added in the future.
### Preserving externally managed data
You can use Payload in conjunction with an existing MongoDB database, where you might have some fields "tracked" in Payload via corresponding field configs, and other fields completely unknown to Payload.
If you have external field data in existing MongoDB collections which you'd like to use in combination with Payload, and you don't want to lose those external fields, you can configure Payload to "preserve" that data while it makes updates to your existing documents.
To do this, the first step is to configure Mongoose's `strict` property, which tells Mongoose to write all data that it receives (and not disregard any data that it does not know about).
The second step is to disable Payload's automatic JSON parsing of documents it receives from MongoDB.
Here's an example for how to configure your Mongoose adapter to preserve external collection fields that are not tracked by Payload:
```ts
mongooseAdapter({
url: process.env.DATABASE_URI,
// Disable the JSON parsing that Payload performs
jsonParse: false,
// Disable strict mode for Mongoose
schemaOptions: {
strict: false,
},
})
```

View File

@@ -70,7 +70,16 @@ In addition to being able to define access control on a document-level, you can
### Field names
Some fields use their `name` property as a unique identifier to store and retrieve from the database. `__v`, `salt`, and `hash` are all reserved field names which are sanitized from Payload's config and cannot be used.
All fields require a `name` property. This is the key that will be used to store and retrieve the field's value in the database. This property must be unique within the Collection, Global, or nested group that it is defined in.
Payload reserves various field names for internal use. Using reserved field names will result in your field being sanitized from the config.
The following field names are forbidden and cannot be used:
- `__v`
- `salt`
- `hash`
- `file`
### Validation
@@ -145,6 +154,7 @@ const field: Field = {
Collections ID fields are generated automatically by default. An explicit `id` field can be declared in the `fields` array to override this behavior.
Users are then required to provide a custom ID value when creating a record through the Admin UI or API.
Valid ID types are `number` and `text`.
When using the text value, remember that it shouldn't contain the / (slash) sign, as the API will read it separately and this can result in unexpected behavior.
Example:

View File

@@ -49,6 +49,7 @@ caption="Admin panel screenshot of an Upload field"
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More](/docs/upload/overview#collection-upload-options). |
| **`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. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |

View File

@@ -26,6 +26,8 @@ Additionally, `auth`-enabled collections feature the following hooks:
- [afterRefresh](#afterrefresh)
- [afterMe](#afterme)
- [afterForgotPassword](#afterforgotpassword)
- [refresh](#refresh)
- [me](#me)
## Config
@@ -59,6 +61,8 @@ export const ExampleHooks: CollectionConfig = {
afterRefresh: [(args) => {...}],
afterMe: [(args) => {...}],
afterForgotPassword: [(args) => {...}],
refresh: [(args) => {...}],
me: [(args) => {...}],
},
}
```
@@ -299,6 +303,32 @@ const afterForgotPasswordHook: CollectionAfterForgotPasswordHook = async ({
}) => {...}
```
### refresh
For auth-enabled Collections, this hook allows you to optionally replace the default behavior of the `refresh` operation with your own. If you optionally return a value from your hook, the operation will not perform its own logic and continue.
```ts
import type { CollectionRefreshHook } from 'payload/types'
const myRefreshHook: CollectionRefreshHook = async ({
args, // arguments passed into the `refresh` operation
user, // the user as queried from the database
}) => {...}
```
### me
For auth-enabled Collections, this hook allows you to optionally replace the default behavior of the `me` operation with your own. If you optionally return a value from your hook, the operation will not perform its own logic and continue.
```ts
import type { CollectionMeHook } from 'payload/types'
const meHook: CollectionMeHook = async ({
args, // arguments passed into the `me` operation
user, // the user as queried from the database
}) => {...}
```
## TypeScript
Payload exports a type for each Collection hook which can be accessed as follows:
@@ -319,5 +349,7 @@ import type {
CollectionAfterRefreshHook,
CollectionAfterMeHook,
CollectionAfterForgotPasswordHook,
CollectionRefreshHook,
CollectionMeHook,
} from 'payload/types'
```

View File

@@ -2,13 +2,13 @@
title: Hooks Overview
label: Overview
order: 10
desc: Hooks allow you to add your own logic to Payload, including integrating with third-party APIs, adding auto-generated data, or modifing Payload's base functionality.
desc: Hooks allow you to add your own logic to Payload, including integrating with third-party APIs, adding auto-generated data, or modifying Payload's base functionality.
keywords: hooks, overview, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
---
<Banner type="info">
Hooks are powerful ways to tie into existing Payload actions in order to add your own logic like
integrating with third-party APIs, adding auto-generated data, or modifing Payload's base
integrating with third-party APIs, adding auto-generated data, or modifying Payload's base
functionality.
</Banner>

View File

@@ -76,7 +76,7 @@ The following custom endpoints are automatically opened for you:
| Endpoint | Method | Description |
| --- | --- | --- |
| `/api/stripe/rest` | `POST` | Proxies the [Stripe REST API](https://stripe.com/docs/api) behind [Payload access control](https://payloadcms.com/docs/access-control/overview) and returns the result. See the [REST Proxy](#stripe-rest-proxy) section for more details. |
| `/api/stripe/webhooks` | `POST` | Handles all Stripe webhook events |
| `/stripe/webhooks` | `POST` | Handles all Stripe webhook events |
##### Stripe REST Proxy
@@ -120,7 +120,7 @@ Development:
Production:
1. Login and [create a new webhook](https://dashboard.stripe.com/test/webhooks/create) from the Stripe dashboard
1. Paste `YOUR_DOMAIN_NAME/api/stripe/webhooks` as the "Webhook Endpoint URL"
1. Paste `YOUR_DOMAIN_NAME/stripe/webhooks` as the "Webhook Endpoint URL"
1. Select which events to broadcast
1. Paste the given secret into your `.env` file as `STRIPE_WEBHOOKS_ENDPOINT_SECRET`
1. Then, handle these events using the `webhooks` portion of this plugin's config:

View File

@@ -23,7 +23,7 @@ _Admin panel screenshot depicting a Media Collection with Upload enabled_
**By simply enabling Upload functionality on a Collection, Payload will automatically transform your Collection into a robust file management / storage solution. The following modifications will be made:**
1. `filename`, `mimeType`, and `filesize` fields will be automatically added to your Collection. Optionally, if you pass `imageSizes` to your Collection's Upload config, a [`sizes`](#image-sizes) array will also be added containing auto-resized image sizes and filenames.
1. The Admin panel will modify its built-in `List` component to show a thumbnail gallery of your Uploads instead of the default Table view
1. The Admin panel will modify its built-in `List` component to show a thumbnail for each upload within the List View
1. The Admin panel will modify its `Edit` view(s) to add a new set of corresponding Upload UI which will allow for file upload
1. The `create`, `update`, and `delete` Collection operations will be modified to support file upload, re-upload, and deletion
@@ -47,6 +47,7 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config). |
| **`externalFileHeaderFilter`** | Accepts existing headers and can filter/modify them. |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
@@ -56,6 +57,7 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
| **`withMetadata`** | If specified, appends metadata to the output image file. Accepts a boolean or a function that receives `metadata` and `req`, returning a boolean. |
_An asterisk denotes that a property above is required._
@@ -166,6 +168,22 @@ When an uploaded image is smaller than the defined image size, we have 3 options
Use the `withoutEnlargement` prop to change this.
</Banner>
#### Custom file name per size
Each image size supports a `generateImageName` function that can be used to generate a custom file name for the resized image.
This function receives the original file name, the resize name, the extension, height and width as arguments.
```ts
{
name: 'thumbnail',
width: 400,
height: 300,
generateImageName: ({ height, sizeName, extension, width }) => {
return `custom-${sizeName}-${height}-${width}.${extension}`
},
}
```
### Crop and Focal Point Selector
This feature is only available for image file types.

View File

@@ -481,11 +481,11 @@ brace-expansion@^1.1.7:
concat-map "0.0.1"
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.0.1"
fill-range "^7.1.1"
busboy@1.6.0:
version "1.6.0"
@@ -1071,10 +1071,10 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"

View File

@@ -15,7 +15,7 @@ To spin up this example locally, follow these steps:
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server and seed the database
5. `open http://localhost:3000/admin` to access the admin panel
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
6. 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.

View File

@@ -8304,9 +8304,9 @@ wrappy@1:
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@^7.3.1:
version "7.5.9"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
version "7.5.10"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
xss@^1.0.6:
version "1.0.14"

View File

@@ -9,7 +9,7 @@ To spin up this example locally, follow these steps:
1. First clone the repo
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
1. Next `yarn && yarn dev`
1. Now `open http://localhost:3000/admin` to access the admin panel
1. Now Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
1. 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.

View File

@@ -13,7 +13,7 @@ Follow the instructions in each respective README to get started. If you are set
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server and seed the database
5. `open http://localhost:3000/admin` to access the admin panel
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
6. 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.

View File

@@ -5141,9 +5141,9 @@ node-releases@^2.0.12:
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
nodemailer@^6.9.0:
version "6.9.4"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.4.tgz#93bd4a60eb0be6fa088a0483340551ebabfd2abf"
integrity sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==
version "6.9.14"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.14.tgz#845fda981f9fd5ac264f4446af908a7c78027f75"
integrity sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==
nodemon@^2.0.6:
version "2.0.22"

View File

@@ -9,7 +9,7 @@ To spin up the project locally, follow these steps:
1. First clone the repo
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
1. Next `yarn && yarn dev` (or `docker-compose up`, see [Docker](#docker))
1. Now `open http://localhost:3000/admin` to access the admin panel
1. Now Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
1. Create your first admin user using the form on the page
That's it! Changes made in `./src` will be reflected in your app.

View File

@@ -15,7 +15,7 @@ Follow the instructions in each respective README to get started. If you are set
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server and seed the database
5. `open http://localhost:3000/admin` to access the admin panel
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
6. 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.

View File

@@ -9,7 +9,7 @@ To spin up this example locally, follow these steps:
1. First clone the repo
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
1. Next `yarn && yarn dev`
1. Now `open http://localhost:3000/admin` to access the admin panel
1. Now Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
1. 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 on how to log in as a tenant.

View File

@@ -17,7 +17,7 @@ To spin up this example locally, follow these steps:
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server and seed the database
5. `open http://localhost:3000/admin` to access the admin panel
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
6. Login with email `demo@payloadcms.com` and password `demo`
## How it works

View File

@@ -16,7 +16,7 @@ To spin up this example locally, follow these steps:
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server and seed the database
5. `open http://localhost:3000/admin` to access the admin panel
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
6. Login with email `demo@payloadcms.com` and password `demo`
## How it works

View File

@@ -91,7 +91,7 @@
"prompts": "2.4.2",
"qs": "6.11.2",
"read-stream": "^2.1.1",
"rimraf": "3.0.2",
"rimraf": "4.4.1",
"semver": "^7.5.4",
"shelljs": "0.8.5",
"simple-git": "^3.20.0",
@@ -120,8 +120,9 @@
},
"engines": {
"node": ">=14",
"pnpm": ">=8"
"pnpm": ">=9.7.0"
},
"packageManager": "pnpm@9.7.0",
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write"

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "1.5.1",
"version": "1.7.2",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": {
"type": "git",
@@ -27,11 +27,11 @@
"bson-objectid": "2.0.4",
"deepmerge": "4.3.1",
"get-port": "5.1.1",
"http-status": "1.6.2",
"mongoose": "6.12.3",
"mongoose-aggregate-paginate-v2": "1.0.6",
"mongoose-paginate-v2": "1.7.22",
"prompts": "2.4.2",
"http-status": "1.6.2",
"uuid": "9.0.0"
},
"devDependencies": {

View File

@@ -13,7 +13,7 @@ export const count: Count = async function count(
{ collection, locale, req = {} as PayloadRequest, where },
) {
const Model = this.collections[collection]
const options: QueryOptions = withSession(this, req.transactionID)
const options: QueryOptions = await withSession(this, req)
let hasNearConstraint = false

View File

@@ -1,9 +1,10 @@
import type { Create } from 'payload/database'
import type { Document, PayloadRequest } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import type { MongooseAdapter } from '.'
import handleError from './utilities/handleError'
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
import { withSession } from './withSession'
export const create: Create = async function create(
@@ -11,7 +12,7 @@ export const create: Create = async function create(
{ collection, data, req = {} as PayloadRequest },
) {
const Model = this.collections[collection]
const options = withSession(this, req.transactionID)
const options = await withSession(this, req)
let doc
try {
;[doc] = await Model.create([data], options)
@@ -19,15 +20,13 @@ export const create: Create = async function create(
handleError(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 result = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc.toObject()
const verificationToken = doc._verificationToken
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
return sanitizeInternalFields(result)
}

View File

@@ -15,14 +15,12 @@ export const createGlobal: CreateGlobal = async function createGlobal(
globalType: slug,
...data,
}
const options = withSession(this, req.transactionID)
const options = await withSession(this, req)
let [result] = (await Model.create([global], options)) as any
result = JSON.parse(JSON.stringify(result))
result = this.jsonParse ? JSON.parse(JSON.stringify(result)) : result.toObject()
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
return result

View File

@@ -1,9 +1,9 @@
import type { CreateGlobalVersion } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { Document } from 'payload/types'
import type { MongooseAdapter } from '.'
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
import { withSession } from './withSession'
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
@@ -11,7 +11,7 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
{ autosave, createdAt, globalSlug, parent, req = {} as PayloadRequest, updatedAt, versionData },
) {
const VersionModel = this.versions[globalSlug]
const options = withSession(this, req.transactionID)
const options = await withSession(this, req)
const [doc] = await VersionModel.create(
[
@@ -52,13 +52,12 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
options,
)
const result: Document = JSON.parse(JSON.stringify(doc))
const result = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc.toObject()
const verificationToken = doc._verificationToken
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
return sanitizeInternalFields(result)
}

View File

@@ -1,9 +1,9 @@
import type { CreateVersion } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { Document } from 'payload/types'
import type { MongooseAdapter } from '.'
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
import { withSession } from './withSession'
export const createVersion: CreateVersion = async function createVersion(
@@ -19,7 +19,7 @@ export const createVersion: CreateVersion = async function createVersion(
},
) {
const VersionModel = this.versions[collectionSlug]
const options = withSession(this, req.transactionID)
const options = await withSession(this, req)
const [doc] = await VersionModel.create(
[
@@ -60,13 +60,13 @@ export const createVersion: CreateVersion = async function createVersion(
options,
)
const result: Document = JSON.parse(JSON.stringify(doc))
const result = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc.toObject()
const verificationToken = doc._verificationToken
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
return sanitizeInternalFields(result)
}

View File

@@ -11,7 +11,7 @@ export const deleteMany: DeleteMany = async function deleteMany(
) {
const Model = this.collections[collection]
const options = {
...withSession(this, req.transactionID),
...(await withSession(this, req)),
lean: true,
}

View File

@@ -1,6 +1,5 @@
import type { DeleteOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { Document } from 'payload/types'
import type { MongooseAdapter } from '.'
@@ -12,20 +11,16 @@ export const deleteOne: DeleteOne = async function deleteOne(
{ collection, req = {} as PayloadRequest, where },
) {
const Model = this.collections[collection]
const options = withSession(this, req.transactionID)
const options = await withSession(this, req)
const query = await Model.buildQuery({
payload: this.payload,
where,
})
const doc = await Model.findOneAndDelete(query, options).lean()
let doc = await Model.findOneAndDelete(query, options).lean()
let result: Document = JSON.parse(JSON.stringify(doc))
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
return result
return sanitizeInternalFields(doc)
}

View File

@@ -11,7 +11,7 @@ export const deleteVersions: DeleteVersions = async function deleteVersions(
) {
const VersionsModel = this.versions[collection]
const options = {
...withSession(this, req.transactionID),
...(await withSession(this, req)),
lean: true,
}

View File

@@ -16,7 +16,7 @@ export const find: Find = async function find(
) {
const Model = this.collections[collection]
const collectionConfig = this.payload.collections[collection].config
const options = withSession(this, req.transactionID)
const options = await withSession(this, req)
let hasNearConstraint = false
@@ -55,6 +55,14 @@ export const find: Find = async function find(
useEstimatedCount,
}
if (this.collation) {
const defaultLocale = 'en'
paginationOptions.collation = {
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
...this.collation,
}
}
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,
@@ -82,13 +90,12 @@ export const find: Find = async function find(
}
const result = await Model.paginate(query, paginationOptions)
const docs = JSON.parse(JSON.stringify(result.docs))
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
return {
...result,
docs: docs.map((doc) => {
// eslint-disable-next-line no-param-reassign
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}

View File

@@ -14,7 +14,7 @@ export const findGlobal: FindGlobal = async function findGlobal(
) {
const Model = this.globals
const options = {
...withSession(this, req.transactionID),
...(await withSession(this, req)),
lean: true,
}
@@ -30,12 +30,16 @@ export const findGlobal: FindGlobal = async function findGlobal(
if (!doc) {
return null
}
if (this.jsonParse) {
doc = JSON.parse(JSON.stringify(doc))
}
if (doc._id) {
doc.id = doc._id
doc.id = JSON.parse(JSON.stringify(doc._id))
delete doc._id
}
doc = JSON.parse(JSON.stringify(doc))
doc = sanitizeInternalFields(doc)
return doc

View File

@@ -30,7 +30,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
this.payload.globals.config.find(({ slug }) => slug === global),
)
const options = {
...withSession(this, req.transactionID),
...(await withSession(this, req)),
limit,
skip,
}
@@ -74,6 +74,14 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
useEstimatedCount,
}
if (this.collation) {
const defaultLocale = 'en'
paginationOptions.collation = {
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
...this.collation,
}
}
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,
@@ -101,13 +109,12 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
}
const result = await Model.paginate(query, paginationOptions)
const docs = JSON.parse(JSON.stringify(result.docs))
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
return {
...result,
docs: docs.map((doc) => {
// eslint-disable-next-line no-param-reassign
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}

View File

@@ -1,7 +1,6 @@
import type { MongooseQueryOptions } from 'mongoose'
import type { FindOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { Document } from 'payload/types'
import type { MongooseAdapter } from '.'
@@ -14,7 +13,7 @@ export const findOne: FindOne = async function findOne(
) {
const Model = this.collections[collection]
const options: MongooseQueryOptions = {
...withSession(this, req.transactionID),
...(await withSession(this, req)),
lean: true,
}
@@ -24,17 +23,15 @@ export const findOne: FindOne = async function findOne(
where,
})
const doc = await Model.findOne(query, {}, options)
let doc = await Model.findOne(query, {}, options)
if (!doc) {
return null
}
let result: Document = JSON.parse(JSON.stringify(doc))
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
doc = sanitizeInternalFields(doc)
return result
return doc
}

View File

@@ -27,7 +27,7 @@ export const findVersions: FindVersions = async function findVersions(
const Model = this.versions[collection]
const collectionConfig = this.payload.collections[collection].config
const options = {
...withSession(this, req.transactionID),
...(await withSession(this, req)),
limit,
skip,
}
@@ -70,6 +70,14 @@ export const findVersions: FindVersions = async function findVersions(
useEstimatedCount,
}
if (this.collation) {
const defaultLocale = 'en'
paginationOptions.collation = {
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
...this.collation,
}
}
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,
@@ -97,13 +105,12 @@ export const findVersions: FindVersions = async function findVersions(
}
const result = await Model.paginate(query, paginationOptions)
const docs = JSON.parse(JSON.stringify(result.docs))
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
return {
...result,
docs: docs.map((doc) => {
// eslint-disable-next-line no-param-reassign
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}

View File

@@ -1,5 +1,5 @@
import type { TransactionOptions } from 'mongodb'
import type { ClientSession, ConnectOptions, Connection } from 'mongoose'
import type { CollationOptions, TransactionOptions } from 'mongodb'
import type { ClientSession, ConnectOptions, Connection, SchemaOptions } from 'mongoose'
import type { Payload } from 'payload'
import type { BaseDatabaseAdapter } from 'payload/database'
@@ -42,14 +42,58 @@ export type { MigrateDownArgs, MigrateUpArgs } from './types'
export interface Args {
/** Set to false to disable auto-pluralization of collection names, Defaults to true */
autoPluralization?: boolean
/**
* If enabled, collation allows for language-specific rules for string comparison.
* This configuration can include the following options:
*
* - `strength` (number): Comparison level (1: Primary, 2: Secondary, 3: Tertiary (default), 4: Quaternary, 5: Identical)
* - `caseLevel` (boolean): Include case comparison at strength level 1 or 2.
* - `caseFirst` (string): Sort order of case differences during tertiary level comparisons ("upper", "lower", "off").
* - `numericOrdering` (boolean): Compare numeric strings as numbers.
* - `alternate` (string): Consider whitespace and punctuation as base characters ("non-ignorable", "shifted").
* - `maxVariable` (string): Characters considered ignorable when `alternate` is "shifted" ("punct", "space").
* - `backwards` (boolean): Sort strings with diacritics from back of the string.
* - `normalization` (boolean): Check if text requires normalization and perform normalization.
*
* Available on MongoDB version 3.4 and up.
* The locale that gets passed is your current project's locale but defaults to "en".
*
* Example:
* {
* strength: 3
* }
*
* Defaults to disabled.
*/
collation?: Omit<CollationOptions, 'locale'>
/** Define Mongoose options on a collection-by-collection basis.
*/
collections?: {
[slug: string]: {
/** Define Mongoose schema options for a given collection.
*/
schemaOptions?: SchemaOptions
}
}
/** Extra configuration options */
connectOptions?: ConnectOptions & {
/** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */
useFacet?: boolean
}
/** 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
/** Define Mongoose options for the globals collection.
*/
globals?: {
schemaOptions?: SchemaOptions
}
/** Set to false to disable the automatic JSON stringify/parse of data queried by MongoDB. For example, if you have data not tracked by Payload such as `Date` fields and similar, you can use this option to ensure that existing `Date` properties remain as `Date` and not strings. */
jsonParse?: boolean
migrationDir?: string
/** Define default Mongoose schema options for all schemas created.
*/
schemaOptions?: SchemaOptions
transactionOptions?: TransactionOptions | false
/** The URL to connect to MongoDB or false to start payload and prevent connecting */
url: false | string
@@ -57,12 +101,22 @@ export interface Args {
export type MongooseAdapter = BaseDatabaseAdapter &
Args & {
collectionOptions: {
[slug: string]: {
schemaOptions?: SchemaOptions
}
}
collections: {
[slug: string]: CollectionModel
}
connection: Connection
globals: GlobalModel
globalsOptions: {
schemaOptions?: SchemaOptions
}
jsonParse: boolean
mongoMemoryServer: any
schemaOptions?: SchemaOptions
sessions: Record<number | string, ClientSession>
versions: {
[slug: string]: CollectionModel
@@ -74,13 +128,24 @@ type MongooseAdapterResult = (args: { payload: Payload }) => MongooseAdapter
declare module 'payload' {
export interface DatabaseAdapter
extends Omit<BaseDatabaseAdapter, 'sessions'>,
Omit<Args, 'migrationDir'> {
Omit<Args, 'collections' | 'globals' | 'migrationDir'> {
collectionOptions: {
[slug: string]: {
schemaOptions?: SchemaOptions
}
}
collections: {
[slug: string]: CollectionModel
}
connection: Connection
globals: GlobalModel
globalsOptions: {
schemaOptions?: SchemaOptions
}
jsonParse: boolean
mongoMemoryServer: any
schemaOptions?: SchemaOptions
sessions: Record<number | string, ClientSession>
transactionOptions: TransactionOptions
versions: {
@@ -91,9 +156,13 @@ declare module 'payload' {
export function mongooseAdapter({
autoPluralization = true,
collections,
connectOptions,
disableIndexHints = false,
globals,
jsonParse = true,
migrationDir: migrationDirArg,
schemaOptions,
transactionOptions = {},
url,
}: Args): MongooseAdapterResult {
@@ -106,17 +175,22 @@ export function mongooseAdapter({
// Mongoose-specific
autoPluralization,
collectionOptions: collections || {},
collections: {},
connectOptions: connectOptions || {},
connection: undefined,
count,
disableIndexHints,
globals: undefined,
globalsOptions: globals || {},
jsonParse,
mongoMemoryServer: undefined,
schemaOptions: schemaOptions || {},
sessions: {},
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
url,
versions: {},
// DatabaseAdapter
beginTransaction: transactionOptions ? beginTransaction : undefined,
commitTransaction,

View File

@@ -19,20 +19,22 @@ import { getDBName } from './utilities/getDBName'
export const init: Init = async function init(this: MongooseAdapter) {
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const schema = buildCollectionSchema(collection, this.payload.config)
const schema = buildCollectionSchema(collection, this)
if (collection.versions) {
const versionModelName = getDBName({ config: collection, versions: true })
const versionCollectionFields = buildVersionCollectionFields(collection)
const versionSchema = buildSchema(this.payload.config, versionCollectionFields, {
const versionSchema = buildSchema(this, versionCollectionFields, {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: false,
...this.schemaOptions,
...(this.collectionOptions[collection.slug]?.schemaOptions || {}),
},
})
@@ -69,7 +71,7 @@ export const init: Init = async function init(this: MongooseAdapter) {
}
})
const model = buildGlobalModel(this.payload.config)
const model = buildGlobalModel(this)
this.globals = model
this.payload.config.globals.forEach((global) => {
@@ -78,13 +80,15 @@ export const init: Init = async function init(this: MongooseAdapter) {
const versionGlobalFields = buildVersionGlobalFields(global)
const versionSchema = buildSchema(this.payload.config, versionGlobalFields, {
const versionSchema = buildSchema(this, versionGlobalFields, {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: false,
...this.schemaOptions,
...(this.globalsOptions.schemaOptions || {}),
},
})

View File

@@ -1,28 +1,29 @@
import type { PaginateOptions, Schema } from 'mongoose'
import type { SanitizedConfig } from 'payload/config'
import type { SanitizedCollectionConfig } from 'payload/types'
import paginate from 'mongoose-paginate-v2'
import type { MongooseAdapter } from '..'
import getBuildQueryPlugin from '../queries/buildQuery'
import buildSchema from './buildSchema'
const buildCollectionSchema = (
collection: SanitizedCollectionConfig,
config: SanitizedConfig,
schemaOptions = {},
adapter: MongooseAdapter,
): Schema => {
const schema = buildSchema(config, collection.fields, {
const schema = buildSchema(adapter, collection.fields, {
draftsEnabled: Boolean(typeof collection?.versions === 'object' && collection.versions.drafts),
indexSortableFields: config.indexSortableFields,
indexSortableFields: adapter.payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: collection.timestamps !== false,
...schemaOptions,
...adapter.schemaOptions,
...(adapter.collectionOptions[collection.slug]?.schemaOptions || {}),
},
})
if (config.indexSortableFields && collection.timestamps !== false) {
if (adapter.payload.config.indexSortableFields && collection.timestamps !== false) {
schema.index({ updatedAt: 1 })
schema.index({ createdAt: 1 })
}

View File

@@ -1,27 +1,34 @@
import type { SanitizedConfig } from 'payload/config'
import mongoose from 'mongoose'
import type { MongooseAdapter } from '..'
import type { GlobalModel } from '../types'
import getBuildQueryPlugin from '../queries/buildQuery'
import buildSchema from './buildSchema'
export const buildGlobalModel = (config: SanitizedConfig): GlobalModel | null => {
if (config.globals && config.globals.length > 0) {
export const buildGlobalModel = (adapter: MongooseAdapter): GlobalModel | null => {
if (adapter.payload.config.globals && adapter.payload.config.globals.length > 0) {
const globalsSchema = new mongoose.Schema(
{},
{ discriminatorKey: 'globalType', minimize: false, timestamps: true },
{
discriminatorKey: 'globalType',
minimize: false,
...adapter.schemaOptions,
...(adapter.globalsOptions.schemaOptions || {}),
timestamps: true,
},
)
globalsSchema.plugin(getBuildQueryPlugin())
const Globals = mongoose.model('globals', globalsSchema, 'globals') as unknown as GlobalModel
Object.values(config.globals).forEach((globalConfig) => {
const globalSchema = buildSchema(config, globalConfig.fields, {
Object.values(adapter.payload.config.globals).forEach((globalConfig) => {
const globalSchema = buildSchema(adapter, globalConfig.fields, {
options: {
minimize: false,
...adapter.schemaOptions,
...(adapter.globalsOptions.schemaOptions || {}),
},
})
Globals.discriminator(globalConfig.slug, globalSchema)

View File

@@ -40,6 +40,8 @@ import {
tabHasName,
} from 'payload/types'
import type { MongooseAdapter } from '..'
export type BuildSchemaOptions = {
allowIDField?: boolean
disableUnique?: boolean
@@ -51,7 +53,7 @@ export type BuildSchemaOptions = {
type FieldSchemaGenerator = (
field: Field,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
) => void
@@ -90,10 +92,10 @@ const localizeSchema = (
if (fieldIsLocalized(entity) && localization && Array.isArray(localization.locales)) {
return {
type: localization.localeCodes.reduce(
(localeSchema, locale) => ({
...localeSchema,
[locale]: schema,
}),
(localeSchema, locale) => {
localeSchema[locale] = schema
return localeSchema
},
{
_id: false,
},
@@ -105,7 +107,7 @@ const localizeSchema = (
}
const buildSchema = (
config: SanitizedConfig,
adapter: MongooseAdapter,
configFields: Field[],
buildSchemaOptions: BuildSchemaOptions = {},
): Schema => {
@@ -133,7 +135,7 @@ const buildSchema = (
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[field.type]
if (addFieldSchema) {
addFieldSchema(field, schema, config, buildSchemaOptions)
addFieldSchema(field, schema, adapter, buildSchemaOptions)
}
}
})
@@ -145,20 +147,22 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
array: (
field: ArrayField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
) => {
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
type: [
buildSchema(config, field.fields, {
buildSchema(adapter, field.fields, {
allowIDField: true,
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
options: {
minimize: false,
...(buildSchemaOptions.options || {}),
_id: false,
id: false,
minimize: false,
timestamps: false,
},
}),
],
@@ -166,36 +170,54 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
})
},
blocks: (
field: BlockField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const fieldSchema = {
type: [new Schema({}, { _id: false, discriminatorKey: 'blockType' })],
type: [
new Schema(
{},
{
_id: false,
discriminatorKey: 'blockType',
...(buildSchemaOptions.options || {}),
timestamps: false,
},
),
],
default: undefined,
}
schema.add({
[field.name]: localizeSchema(field, fieldSchema, config.localization),
[field.name]: localizeSchema(field, fieldSchema, adapter.payload.config.localization),
})
field.blocks.forEach((blockItem: Block) => {
const blockSchema = new Schema({}, { _id: false, id: false })
const blockSchema = new Schema(
{},
{
...(buildSchemaOptions.options || {}),
_id: false,
id: false,
timestamps: false,
},
)
blockItem.fields.forEach((blockField) => {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[blockField.type]
if (addFieldSchema) {
addFieldSchema(blockField, blockSchema, config, buildSchemaOptions)
addFieldSchema(blockField, blockSchema, adapter, buildSchemaOptions)
}
})
if (field.localized && config.localization) {
config.localization.localeCodes.forEach((localeCode) => {
if (field.localized && adapter.payload.config.localization) {
adapter.payload.config.localization.localeCodes.forEach((localeCode) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error Possible incorrect typing in mongoose types, this works
schema.path(`${field.name}.${localeCode}`).discriminator(blockItem.slug, blockSchema)
@@ -210,69 +232,69 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
checkbox: (
field: CheckboxField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Boolean }
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
})
},
code: (
field: CodeField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
})
},
collapsible: (
field: CollapsibleField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
field.fields.forEach((subField: Field) => {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
if (addFieldSchema) {
addFieldSchema(subField, schema, config, buildSchemaOptions)
addFieldSchema(subField, schema, adapter, buildSchemaOptions)
}
})
},
date: (
field: DateField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Date }
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
})
},
email: (
field: EmailField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
})
},
group: (
field: GroupField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const formattedBaseSchema = formatBaseSchema(field, buildSchemaOptions)
@@ -285,38 +307,40 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
const baseSchema = {
...formattedBaseSchema,
type: buildSchema(config, field.fields, {
type: buildSchema(adapter, field.fields, {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
indexSortableFields,
options: {
minimize: false,
...(buildSchemaOptions.options || {}),
_id: false,
id: false,
minimize: false,
timestamps: false,
},
}),
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
})
},
json: (
field: JSONField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Schema.Types.Mixed }
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
})
},
number: (
field: NumberField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
@@ -325,13 +349,13 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
})
},
point: (
field: PointField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema: SchemaTypeOptions<unknown> = {
@@ -350,7 +374,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
})
if (field.index === true || field.index === undefined) {
@@ -359,8 +383,8 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
indexOptions.sparse = true
indexOptions.unique = true
}
if (field.localized && config.localization) {
config.localization.locales.forEach((locale) => {
if (field.localized && adapter.payload.config.localization) {
adapter.payload.config.localization.locales.forEach((locale) => {
schema.index({ [`${field.name}.${locale.code}`]: '2dsphere' }, indexOptions)
})
} else {
@@ -371,7 +395,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
radio: (
field: RadioField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
@@ -384,21 +408,21 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
})
},
relationship: (
field: RelationshipField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
) => {
const hasManyRelations = Array.isArray(field.relationTo)
let schemaToReturn: { [key: string]: any } = {}
if (field.localized && config.localization) {
if (field.localized && adapter.payload.config.localization) {
schemaToReturn = {
type: config.localization.localeCodes.reduce((locales, locale) => {
type: adapter.payload.config.localization.localeCodes.reduce((locales, locale) => {
let localeSchema: { [key: string]: any } = {}
if (hasManyRelations) {
@@ -467,33 +491,33 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
richText: (
field: RichTextField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Schema.Types.Mixed }
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
})
},
row: (
field: RowField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
field.fields.forEach((subField: Field) => {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
if (addFieldSchema) {
addFieldSchema(subField, schema, config, buildSchemaOptions)
addFieldSchema(subField, schema, adapter, buildSchemaOptions)
}
})
},
select: (
field: SelectField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
@@ -513,39 +537,41 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
[field.name]: localizeSchema(
field,
field.hasMany ? [baseSchema] : baseSchema,
config.localization,
adapter.payload.config.localization,
),
})
},
tabs: (
field: TabsField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
field.tabs.forEach((tab) => {
if (tabHasName(tab)) {
const baseSchema = {
type: buildSchema(config, tab.fields, {
type: buildSchema(adapter, tab.fields, {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
options: {
minimize: false,
...(buildSchemaOptions.options || {}),
_id: false,
id: false,
minimize: false,
timestamps: false,
},
}),
}
schema.add({
[tab.name]: localizeSchema(tab, baseSchema, config.localization),
[tab.name]: localizeSchema(tab, baseSchema, adapter.payload.config.localization),
})
} else {
tab.fields.forEach((subField: Field) => {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
if (addFieldSchema) {
addFieldSchema(subField, schema, config, buildSchemaOptions)
addFieldSchema(subField, schema, adapter, buildSchemaOptions)
}
})
}
@@ -554,7 +580,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
text: (
field: TextField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
@@ -563,25 +589,25 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
})
},
textarea: (
field: TextareaField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
})
},
upload: (
field: UploadField,
schema: Schema,
config: SanitizedConfig,
adapter: MongooseAdapter,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
@@ -591,7 +617,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
})
},
}

View File

@@ -3,7 +3,7 @@ import type { PathToQuery } from 'payload/database'
import type { Field } from 'payload/types'
import type { Operator } from 'payload/types'
import objectID from 'bson-objectid'
import ObjectIdImport from 'bson-objectid'
import mongoose from 'mongoose'
import { getLocalizedPaths } from 'payload/database'
import { fieldAffectsData } from 'payload/types'
@@ -14,6 +14,8 @@ import type { MongooseAdapter } from '..'
import { operatorMap } from './operatorMap'
import { sanitizeQueryValue } from './sanitizeQueryValue'
const ObjectId = ObjectIdImport
type SearchParam = {
path?: string
rawQuery?: unknown
@@ -195,16 +197,20 @@ export async function buildSearchParam({
if (field.type === 'relationship' || field.type === 'upload') {
let hasNumberIDRelation
let multiIDCondition = '$or'
if (operatorKey === '$ne') multiIDCondition = '$and'
const result = {
value: {
$or: [{ [path]: { [operatorKey]: formattedValue } }],
[multiIDCondition]: [{ [path]: { [operatorKey]: formattedValue } }],
},
}
if (typeof formattedValue === 'string') {
if (mongoose.Types.ObjectId.isValid(formattedValue)) {
result.value.$or.push({ [path]: { [operatorKey]: objectID(formattedValue) } })
result.value[multiIDCondition].push({
[path]: { [operatorKey]: ObjectId(formattedValue) },
})
} else {
;(Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]).forEach(
(relationTo) => {
@@ -225,11 +231,13 @@ export async function buildSearchParam({
)
if (hasNumberIDRelation)
result.value.$or.push({ [path]: { [operatorKey]: parseFloat(formattedValue) } })
result.value[multiIDCondition].push({
[path]: { [operatorKey]: parseFloat(formattedValue) },
})
}
}
if (result.value.$or.length > 1) {
if (result.value[multiIDCondition].length > 1) {
return result
}
}

View File

@@ -16,7 +16,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
) {
const VersionModel = this.versions[collection]
const collectionConfig = this.payload.collections[collection].config
const options = withSession(this, req.transactionID)
const options = await withSession(this, req)
let hasNearConstraint
let sort
@@ -58,6 +58,14 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
useEstimatedCount,
}
if (this.collation) {
const defaultLocale = 'en'
paginationOptions.collation = {
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
...this.collation,
}
}
if (
!useEstimatedCount &&
Object.keys(versionQuery).length === 0 &&
@@ -83,12 +91,14 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
}
const result = await VersionModel.paginate(versionQuery, paginationOptions)
const docs = JSON.parse(JSON.stringify(result.docs))
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
return {
...result,
docs: docs.map((doc) => {
// eslint-disable-next-line no-param-reassign
doc = {
_id: doc.parent,
id: doc.parent,

View File

@@ -1,6 +1,8 @@
import type { CommitTransaction } from 'payload/database'
export const commitTransaction: CommitTransaction = async function commitTransaction(id) {
if (id instanceof Promise) return
if (!this.sessions[id]?.inTransaction()) {
return
}

View File

@@ -1,27 +1,35 @@
import type { RollbackTransaction } from 'payload/database'
export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction(
id = '',
incomingID = '',
) {
let transactionID: number | string
if (incomingID instanceof Promise) {
transactionID = await incomingID
} else {
transactionID = incomingID
}
// if multiple operations are using the same transaction, the first will flow through and delete the session.
// subsequent calls should be ignored.
if (!this.sessions[id]) {
if (!this.sessions[transactionID]) {
return
}
// when session exists but is not inTransaction something unexpected is happening to the session
if (!this.sessions[id].inTransaction()) {
if (!this.sessions[transactionID].inTransaction()) {
this.payload.logger.warn('rollbackTransaction called when no transaction exists')
delete this.sessions[id]
delete this.sessions[transactionID]
return
}
// the first call for rollback should be aborted and deleted causing any other operations with the same transaction to fail
try {
await this.sessions[id].abortTransaction()
await this.sessions[id].endSession()
await this.sessions[transactionID].abortTransaction()
await this.sessions[transactionID].endSession()
} catch (error) {
// ignore the error as it is likely a race condition from multiple errors
}
delete this.sessions[id]
delete this.sessions[transactionID]
}

View File

@@ -12,18 +12,17 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
) {
const Model = this.globals
const options = {
...withSession(this, req.transactionID),
...(await withSession(this, req)),
lean: true,
new: true,
}
let result
result = await Model.findOneAndUpdate({ globalType: slug }, data, options)
result = JSON.parse(JSON.stringify(result))
result = this.jsonParse ? JSON.parse(JSON.stringify(result)) : result
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
return result

View File

@@ -3,6 +3,7 @@ import type { PayloadRequest, TypeWithID } from 'payload/types'
import type { MongooseAdapter } from '.'
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
import { withSession } from './withSession'
export async function updateGlobalVersion<T extends TypeWithID>(
@@ -19,7 +20,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
const VersionModel = this.versions[global]
const whereToUse = where || { id: { equals: id } }
const options = {
...withSession(this, req.transactionID),
...(await withSession(this, req)),
lean: true,
new: true,
}
@@ -30,16 +31,14 @@ export async function updateGlobalVersion<T extends TypeWithID>(
where: whereToUse,
})
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
let doc = await VersionModel.findOneAndUpdate(query, versionData, options)
const result = JSON.parse(JSON.stringify(doc))
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
const verificationToken = doc._verificationToken
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
doc._verificationToken = verificationToken
}
return result
return sanitizeInternalFields(doc)
}

View File

@@ -14,7 +14,7 @@ export const updateOne: UpdateOne = async function updateOne(
const where = id ? { id: { equals: id } } : whereArg
const Model = this.collections[collection]
const options = {
...withSession(this, req.transactionID),
...(await withSession(this, req)),
lean: true,
new: true,
}
@@ -32,9 +32,7 @@ export const updateOne: UpdateOne = async function updateOne(
handleError(error, req)
}
result = JSON.parse(JSON.stringify(result))
result.id = result._id
result = sanitizeInternalFields(result)
result = this.jsonParse ? JSON.parse(JSON.stringify(result)) : result
return result
return sanitizeInternalFields(result)
}

View File

@@ -3,6 +3,7 @@ import type { PayloadRequest } from 'payload/types'
import type { MongooseAdapter } from '.'
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
import { withSession } from './withSession'
export const updateVersion: UpdateVersion = async function updateVersion(
@@ -12,7 +13,7 @@ export const updateVersion: UpdateVersion = async function updateVersion(
const VersionModel = this.versions[collection]
const whereToUse = where || { id: { equals: id } }
const options = {
...withSession(this, req.transactionID),
...(await withSession(this, req)),
lean: true,
new: true,
}
@@ -23,16 +24,14 @@ export const updateVersion: UpdateVersion = async function updateVersion(
where: whereToUse,
})
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
const result = JSON.parse(JSON.stringify(doc))
let doc = await VersionModel.findOneAndUpdate(query, versionData, options)
const verificationToken = doc._verificationToken
// custom id type reset
result.id = result._id
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
if (verificationToken) {
result._verificationToken = verificationToken
doc._verificationToken = verificationToken
}
return result
return sanitizeInternalFields(doc)
}

View File

@@ -1,11 +1,13 @@
const internalFields = ['__v']
const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc: T): T =>
Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => {
if (key === '_id') {
const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc: T): T => {
const id = incomingDoc._id ? JSON.parse(JSON.stringify(incomingDoc._id)) : incomingDoc.id
return Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => {
if (key === '_id' || key === 'id') {
return {
...newDoc,
id: val,
id,
}
}
@@ -18,5 +20,6 @@ const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc:
[key]: val,
}
}, {} as T)
}
export default sanitizeInternalFields

View File

@@ -1,4 +1,5 @@
import type { ClientSession } from 'mongoose'
import type { PayloadRequest } from 'payload/types'
import type { MongooseAdapter } from './index'
@@ -6,9 +7,15 @@ import type { MongooseAdapter } from './index'
* returns the session belonging to the transaction of the req.session if exists
* @returns ClientSession
*/
export function withSession(
export async function withSession(
db: MongooseAdapter,
transactionID?: number | string,
): { session: ClientSession } | object {
return db.sessions[transactionID] ? { session: db.sessions[transactionID] } : {}
req: PayloadRequest,
): Promise<{ session: ClientSession } | object> {
let transactionID = req.transactionID
if (transactionID instanceof Promise) {
transactionID = await req.transactionID
}
if (req) return db.sessions[transactionID] ? { session: db.sessions[transactionID] } : {}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "0.8.5",
"version": "0.8.6",
"description": "The officially supported Postgres database adapter for Payload",
"repository": {
"type": "git",

View File

@@ -18,7 +18,7 @@ export const count: Count = async function count(
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
const db = this.sessions[req.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const table = this.tables[tableName]
const { joinAliases, joins, where } = await buildQuery({

View File

@@ -9,7 +9,7 @@ export const create: Create = async function create(
this: PostgresAdapter,
{ collection: collectionSlug, data, req },
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))

View File

@@ -11,7 +11,7 @@ export async function createGlobal<T extends TypeWithID>(
this: PostgresAdapter,
{ slug, data, req = {} as PayloadRequest }: CreateGlobalArgs,
): Promise<T> {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))

View File

@@ -14,7 +14,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
this: PostgresAdapter,
{ autosave, globalSlug, req = {} as PayloadRequest, versionData }: CreateGlobalVersionArgs,
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug)
const tableName = this.tableNameMap.get(`_${toSnakeCase(global.slug)}${this.versionsSuffix}`)

View File

@@ -19,7 +19,7 @@ export async function createVersion<T extends TypeWithID>(
versionData,
}: CreateVersionArgs<T>,
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const defaultTableName = toSnakeCase(collection.slug)

View File

@@ -12,7 +12,7 @@ export const deleteMany: DeleteMany = async function deleteMany(
this: PostgresAdapter,
{ collection, req = {} as PayloadRequest, where },
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const collectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))

View File

@@ -15,7 +15,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
this: PostgresAdapter,
{ collection: collectionSlug, req = {} as PayloadRequest, where: whereArg },
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))

View File

@@ -13,7 +13,7 @@ export const deleteVersions: DeleteVersions = async function deleteVersion(
this: PostgresAdapter,
{ collection, locale, req = {} as PayloadRequest, where: where },
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(

View File

@@ -31,7 +31,7 @@ export const findMany = async function find({
tableName,
where: whereArg,
}: Args) {
const db = adapter.sessions[req.transactionID]?.db || adapter.drizzle
const db = adapter.sessions[await req.transactionID]?.db || adapter.drizzle
const table = adapter.tables[tableName]
const limit = limitArg ?? 10

View File

@@ -110,5 +110,6 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
err,
msg: parseError(err, `Error running migration ${migration.name}`),
})
process.exit(1)
}
}

View File

@@ -79,6 +79,7 @@ export async function migrateFresh(
err,
msg: parseError(err, `Error running migration ${migration.name}. Rolling back`),
})
process.exit(1)
}
}
}

View File

@@ -98,6 +98,7 @@ export async function migrateRefresh(this: PostgresAdapter) {
err,
msg: parseError(err, `Error running migration ${migration.name}. Rolling back.`),
})
process.exit(1)
}
}
}

View File

@@ -261,10 +261,10 @@ export const getTableColumnFromPath = ({
tableType = 'numbers'
columnName = 'number'
}
newTableName = `${tableName}_${tableType}`
newTableName = `${rootTableName}_${tableType}`
const joinConstraints = [
eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
eq(adapter.tables[newTableName].path, `${constraintPath}${field.name}`),
eq(adapter.tables[rootTableName].id, adapter.tables[newTableName].parent),
like(adapter.tables[newTableName].path, `${constraintPath}${field.name}`),
]
if (locale && field.localized && adapter.payload.config.localization) {

View File

@@ -1,6 +1,8 @@
import type { CommitTransaction } from 'payload/database'
export const commitTransaction: CommitTransaction = async function commitTransaction(id) {
if (id instanceof Promise) return
// if the session was deleted it has already been aborted
if (!this.sessions[id]) {
return

View File

@@ -1,17 +1,19 @@
import type { RollbackTransaction } from 'payload/database'
export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction(
id = '',
incomingID = '',
) {
const transactionID = incomingID instanceof Promise ? await incomingID : incomingID
// if multiple operations are using the same transaction, the first will flow through and delete the session.
// subsequent calls should be ignored.
if (!this.sessions[id]) {
if (!this.sessions[transactionID]) {
return
}
// end the session promise in failure by calling reject
await this.sessions[id].reject()
await this.sessions[transactionID].reject()
// delete the session causing any other operations with the same transaction to fail
delete this.sessions[id]
delete this.sessions[transactionID]
}

View File

@@ -12,7 +12,7 @@ export const updateOne: UpdateOne = async function updateOne(
this: PostgresAdapter,
{ id, collection: collectionSlug, data, draft, locale, req, where: whereArg },
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const whereToUse = whereArg || { id: { equals: id } }

View File

@@ -11,7 +11,7 @@ export async function updateGlobal<T extends TypeWithID>(
this: PostgresAdapter,
{ slug, data, req = {} as PayloadRequest }: UpdateGlobalArgs,
): Promise<T> {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))

View File

@@ -20,7 +20,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
where: whereArg,
}: UpdateGlobalVersionArgs<T>,
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const globalConfig: SanitizedGlobalConfig = this.payload.globals.config.find(
({ slug }) => slug === global,
)

View File

@@ -20,7 +20,7 @@ export async function updateVersion<T extends TypeWithID>(
where: whereArg,
}: UpdateVersionArgs<T>,
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const whereToUse = whereArg || { id: { equals: id } }
const tableName = this.tableNameMap.get(

View File

@@ -4,8 +4,10 @@ import { fieldAffectsData, fieldHasSubFields } from 'payload/types'
export const hasLocalesTable = (fields: Field[]): boolean => {
return fields.some((field) => {
// arrays always get a separate table
if (field.type === 'array') return false
if (fieldAffectsData(field) && field.localized) return true
if (fieldHasSubFields(field) && field.type !== 'array') return hasLocalesTable(field.fields)
if (fieldHasSubFields(field)) return hasLocalesTable(field.fields)
if (field.type === 'tabs') return field.tabs.some((tab) => hasLocalesTable(tab.fields))
return false
})

View File

@@ -10,8 +10,8 @@
"license": "MIT",
"homepage": "https://payloadcms.com",
"author": "Payload CMS, Inc.",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",

View File

@@ -10,8 +10,8 @@
"license": "MIT",
"homepage": "https://payloadcms.com",
"author": "Payload CMS, Inc.",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",

View File

@@ -10,8 +10,8 @@
"license": "MIT",
"homepage": "https://payloadcms.com",
"author": "Payload CMS, Inc.",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.22.0",
"version": "2.28.0",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",
@@ -96,7 +96,7 @@
"is-plain-object": "5.0.0",
"isomorphic-fetch": "3.0.0",
"joi": "17.9.2",
"json-schema-to-typescript": "11.0.3",
"json-schema-to-typescript": "14.0.5",
"jsonwebtoken": "9.0.1",
"jwt-decode": "3.1.2",
"md5": "2.3.0",
@@ -112,7 +112,7 @@
"passport-jwt": "4.0.1",
"passport-local": "1.0.0",
"pino": "8.15.0",
"pino-pretty": "10.2.0",
"pino-pretty": "10.3.1",
"pluralize": "8.0.0",
"probe-image-size": "6.0.0",
"process": "0.11.10",
@@ -129,7 +129,7 @@
"react-router-dom": "5.3.4",
"react-router-navigation-prompt": "1.9.6",
"react-select": "5.7.4",
"react-toastify": "8.2.0",
"react-toastify": "10.0.5",
"sanitize-filename": "1.6.3",
"sass": "1.69.4",
"scheduler": "0.23.0",
@@ -199,7 +199,7 @@
"postcss-loader": "6.2.1",
"postcss-preset-env": "9.0.0",
"release-it": "16.1.3",
"rimraf": "3.0.2",
"rimraf": "4.4.1",
"sass-loader": "12.6.0",
"serve-static": "1.15.0",
"swc-loader": "^0.2.3",

View File

@@ -17,11 +17,12 @@ import reduceFieldsToValues from '../../forms/Form/reduceFieldsToValues'
import { reduceFieldsToValuesWithValidation } from '../../forms/Form/reduceFieldsToValuesWithValidation'
import { useConfig } from '../../utilities/Config'
import { useDocumentInfo } from '../../utilities/DocumentInfo'
import { useEditDepth } from '../../utilities/EditDepth'
import { useLocale } from '../../utilities/Locale'
import './index.scss'
const baseClass = 'autosave'
const Autosave: React.FC<Props> = ({ id, collection, global, publishedDocUpdatedAt }) => {
const Autosave: React.FC<Props> = ({ id, collection, global, onSave, publishedDocUpdatedAt }) => {
const {
routes: { admin, api },
serverURL,
@@ -34,6 +35,7 @@ const Autosave: React.FC<Props> = ({ id, collection, global, publishedDocUpdated
const { dispatchFields, setSubmitted } = useForm()
const history = useHistory()
const { i18n, t } = useTranslation('version')
const depth = useEditDepth()
let interval = 800
const validateDrafts =
@@ -77,15 +79,19 @@ const Autosave: React.FC<Props> = ({ id, collection, global, publishedDocUpdated
if (res.status === 201) {
const json = await res.json()
history.replace(`${admin}/collections/${collection.slug}/${json.doc.id}`, {
state: {
data: json.doc,
},
})
if (depth === 1) {
history.replace(`${admin}/collections/${collection.slug}/${json.doc.id}`, {
state: {
data: json.doc,
},
})
} else {
onSave(json)
}
} else {
toast.error(t('error:autosaving'))
}
}, [serverURL, api, collection?.slug, locale, i18n.language, history, admin, t])
}, [serverURL, api, collection?.slug, locale, i18n.language, history, admin, t, depth, onSave])
useEffect(() => {
// If no ID, but this is used for a collection doc,

View File

@@ -1,9 +1,11 @@
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
import type { SanitizedGlobalConfig } from '../../../../globals/config/types'
import type { CollectionEditViewProps } from '../../views/types'
export type Props = {
collection?: SanitizedCollectionConfig
global?: SanitizedGlobalConfig
id?: number | string
onSave?: CollectionEditViewProps['onSave']
publishedDocUpdatedAt: string
}

View File

@@ -54,9 +54,12 @@ const DateTime: React.FC<Props> = (props) => {
const onChange = (incomingDate: Date) => {
const newDate = incomingDate
if (newDate instanceof Date && ['dayOnly', 'default', 'monthOnly'].includes(pickerAppearance)) {
const tzOffset = incomingDate.getTimezoneOffset() / 60
newDate.setHours(12 - tzOffset, 0)
if (newDate instanceof Date) {
newDate.setMilliseconds(0)
if (['dayOnly', 'default', 'monthOnly'].includes(pickerAppearance)) {
const tzOffset = incomingDate.getTimezoneOffset() / 60
newDate.setHours(12 - tzOffset, 0)
}
}
if (typeof onChangeFromProps === 'function') onChangeFromProps(newDate)
}

View File

@@ -18,7 +18,7 @@ import './index.scss'
const baseClass = 'delete-documents'
const DeleteMany: React.FC<Props> = (props) => {
const { collection: { labels: { plural }, slug } = {}, resetParams } = props
const { collection: { slug, labels: { plural } } = {}, resetParams } = props
const { permissions } = useAuth()
const {
@@ -41,7 +41,7 @@ const DeleteMany: React.FC<Props> = (props) => {
const handleDelete = useCallback(() => {
setDeleting(true)
requests
void requests
.delete(`${serverURL}${api}/${slug}${getQueryParams()}`, {
headers: {
'Accept-Language': i18n.language,
@@ -60,7 +60,15 @@ const DeleteMany: React.FC<Props> = (props) => {
}
if (json.errors) {
toast.error(json.message)
let message = json.message
if (json.errors) {
json.errors.forEach((error) => {
message = message + '\n' + error.message
})
}
toast.error(message)
} else {
addDefaultError()
}

View File

@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
import type { CollectionPermission, GlobalPermission } from '../../../../auth'
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
import type { SanitizedGlobalConfig } from '../../../../globals/config/types'
import type { CollectionEditViewProps } from '../../views/types'
import { getTranslation } from '../../../../utilities/getTranslation'
import { formatDate } from '../../../utilities/formatDate'
@@ -34,6 +35,7 @@ export const DocumentControls: React.FC<{
id?: string
isAccountView?: boolean
isEditing?: boolean
onSave?: CollectionEditViewProps['onSave']
permissions?: CollectionPermission | GlobalPermission
}> = (props) => {
const {
@@ -45,6 +47,7 @@ export const DocumentControls: React.FC<{
hasSavePermission,
isAccountView,
isEditing,
onSave,
permissions,
} = props
@@ -111,6 +114,7 @@ export const DocumentControls: React.FC<{
collection={collection}
global={global}
id={id}
onSave={onSave}
publishedDocUpdatedAt={publishedDoc?.updatedAt || data?.createdAt}
/>
</li>

View File

@@ -1,7 +1,7 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '../Button'
import Button from '../Button'
import './index.scss'
const handleDragOver = (e: DragEvent) => {
@@ -12,12 +12,13 @@ const handleDragOver = (e: DragEvent) => {
const baseClass = 'dropzone'
type Props = {
onChange: (e: FileList) => void
className?: string
mimeTypes?: string[]
onChange: (e: FileList) => void
onPasteUrlClick?: () => void
}
export const Dropzone: React.FC<Props> = ({ onChange, className, mimeTypes }) => {
export const Dropzone: React.FC<Props> = ({ className, mimeTypes, onChange, onPasteUrlClick }) => {
const dropRef = React.useRef<HTMLDivElement>(null)
const [dragging, setDragging] = React.useState(false)
const inputRef = React.useRef(null)
@@ -98,24 +99,31 @@ export const Dropzone: React.FC<Props> = ({ onChange, className, mimeTypes }) =>
const classes = [baseClass, className, dragging ? 'dragging' : ''].filter(Boolean).join(' ')
return (
<div ref={dropRef} className={classes}>
<div className={classes} ref={dropRef}>
<Button
size="small"
buttonStyle="secondary"
className={`${baseClass}__file-button`}
onClick={() => {
inputRef.current.click()
}}
className={`${baseClass}__file-button`}
size="small"
>
{t('selectFile')}
{t('upload:selectFile')}
</Button>
<Button
buttonStyle="secondary"
className={`${baseClass}__file-button`}
onClick={onPasteUrlClick}
size="small"
>
{t('upload:pasteURL')}
</Button>
<input
accept={mimeTypes?.join(',')}
className={`${baseClass}__hidden-input`}
onChange={handleFileSelection}
ref={inputRef}
type="file"
accept={mimeTypes?.join(',')}
onChange={handleFileSelection}
className={`${baseClass}__hidden-input`}
/>
<p className={`${baseClass}__label`}>

View File

@@ -1,69 +1,120 @@
import { useModal } from '@faceless-ui/modal'
import React, { useRef, useState } from 'react'
import React, { forwardRef, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ReactCrop, { type Crop as CropType } from 'react-image-crop'
import ReactCrop from 'react-image-crop'
import 'react-image-crop/dist/ReactCrop.css'
import type { Data } from '../../forms/Form/types'
import type { UploadEdits } from '../../../../uploads/types'
import Plus from '../../icons/Plus'
import { useUploadEdits } from '../../utilities/UploadEdits'
import { editDrawerSlug } from '../../views/collections/Edit/Upload'
import Button from '../Button'
import './index.scss'
const baseClass = 'edit-upload'
const Input: React.FC<{ name: string; onChange: (value: string) => void; value: string }> = ({
name,
onChange,
value,
}) => (
<div className={`${baseClass}__input`}>
{name}
<input name={name} onChange={(e) => onChange(e.target.value)} type="number" value={value} />
</div>
)
type Props = {
name: string
onChange: (value: string) => void
value: string
}
export const EditUpload: React.FC<{
doc?: Data
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => {
const { name, onChange, value } = props
return (
<div className={`${baseClass}__input`}>
{name}
<input
name={name}
onChange={(e) => onChange(e.target.value)}
ref={ref}
type="number"
value={value}
/>
</div>
)
})
type FocalPosition = {
x: number
y: number
}
export type EditUploadProps = {
fileName: string
fileSrc: string
imageCacheTag?: string
initialCrop?: UploadEdits['crop']
initialFocalPoint?: FocalPosition
onSave?: (uploadEdits: UploadEdits) => void
showCrop?: boolean
showFocalPoint?: boolean
}> = ({ doc, fileName, fileSrc, imageCacheTag, showCrop, showFocalPoint }) => {
}
const defaultCrop: UploadEdits['crop'] = {
height: 100,
unit: '%',
width: 100,
x: 0,
y: 0,
}
export const EditUpload: React.FC<EditUploadProps> = ({
fileName,
fileSrc,
imageCacheTag,
initialCrop,
initialFocalPoint,
onSave,
showCrop,
showFocalPoint,
}) => {
const { closeModal } = useModal()
const { t } = useTranslation(['general', 'upload'])
const { updateUploadEdits, uploadEdits } = useUploadEdits()
const [crop, setCrop] = useState<CropType>({
height: uploadEdits?.crop?.height || 100,
unit: '%',
width: uploadEdits?.crop?.width || 100,
x: uploadEdits?.crop?.x || 0,
y: uploadEdits?.crop?.y || 0,
})
const [crop, setCrop] = useState<UploadEdits['crop']>(() => ({
...defaultCrop,
...(initialCrop || {}),
}))
const [focalPosition, setFocalPosition] = useState<{ x: number; y: number }>({
x: uploadEdits?.focalPoint?.x || doc.focalX || 50,
y: uploadEdits?.focalPoint?.y || doc.focalY || 50,
})
const defaultFocalPosition: FocalPosition = {
x: 50,
y: 50,
}
const [focalPosition, setFocalPosition] = useState<FocalPosition>(() => ({
...defaultFocalPosition,
...initialFocalPoint,
}))
const [checkBounds, setCheckBounds] = useState<boolean>(false)
const [originalHeight, setOriginalHeight] = useState<number>(0)
const [originalWidth, setOriginalWidth] = useState<number>(0)
const [uncroppedPixelHeight, setUncroppedPixelHeight] = useState<number>(0)
const [uncroppedPixelWidth, setUncroppedPixelWidth] = useState<number>(0)
const focalWrapRef = useRef<HTMLDivElement | undefined>()
const imageRef = useRef<HTMLImageElement | undefined>()
const cropRef = useRef<HTMLDivElement | undefined>()
const heightInputRef = useRef<HTMLInputElement | null>(null)
const widthInputRef = useRef<HTMLInputElement | null>(null)
const [imageLoaded, setImageLoaded] = useState<boolean>(false)
const onImageLoad = (e) => {
// set the default image height/width on load
setUncroppedPixelHeight(e.currentTarget.naturalHeight)
setUncroppedPixelWidth(e.currentTarget.naturalWidth)
setImageLoaded(true)
}
const fineTuneCrop = ({ dimension, value }: { dimension: 'height' | 'width'; value: string }) => {
const intValue = parseInt(value)
if (dimension === 'width' && intValue >= originalWidth) return null
if (dimension === 'height' && intValue >= originalHeight) return null
if (dimension === 'width' && intValue >= uncroppedPixelWidth) return null
if (dimension === 'height' && intValue >= uncroppedPixelHeight) return null
const percentage = 100 * (intValue / (dimension === 'width' ? originalWidth : originalHeight))
const percentage =
100 * (intValue / (dimension === 'width' ? uncroppedPixelWidth : uncroppedPixelHeight))
if (percentage === 100 || percentage === 0) return null
@@ -87,10 +138,13 @@ export const EditUpload: React.FC<{
}
const saveEdits = () => {
updateUploadEdits({
crop: crop || undefined,
focalPoint: focalPosition ? focalPosition : undefined,
})
if (typeof onSave === 'function')
onSave({
crop: crop ? crop : undefined,
focalPoint: focalPosition,
heightInPixels: Number(heightInputRef?.current?.value ?? uncroppedPixelHeight),
widthInPixels: Number(widthInputRef?.current?.value ?? uncroppedPixelWidth),
})
closeModal(editDrawerSlug)
}
@@ -132,6 +186,7 @@ export const EditUpload: React.FC<{
aria-label={t('general:applyChanges')}
buttonStyle="primary"
className={`${baseClass}__save`}
disabled={!imageLoaded}
onClick={() => saveEdits()}
>
{t('general:applyChanges')}
@@ -144,7 +199,7 @@ export const EditUpload: React.FC<{
className={`${baseClass}__focal-wrapper`}
ref={focalWrapRef}
style={{
aspectRatio: `${originalWidth / originalHeight}`,
aspectRatio: `${uncroppedPixelWidth / uncroppedPixelHeight}`,
}}
>
{showCrop ? (
@@ -159,10 +214,7 @@ export const EditUpload: React.FC<{
>
<img
alt={t('upload:setCropArea')}
onLoad={(e) => {
setOriginalHeight(e.currentTarget.naturalHeight)
setOriginalWidth(e.currentTarget.naturalWidth)
}}
onLoad={onImageLoad}
ref={imageRef}
src={fileSrcToUse}
/>
@@ -170,10 +222,7 @@ export const EditUpload: React.FC<{
) : (
<img
alt={t('upload:setFocalPoint')}
onLoad={(e) => {
setOriginalHeight(e.currentTarget.naturalHeight)
setOriginalWidth(e.currentTarget.naturalWidth)
}}
onLoad={onImageLoad}
ref={imageRef}
src={fileSrcToUse}
/>
@@ -224,12 +273,14 @@ export const EditUpload: React.FC<{
<Input
name={`${t('upload:width')} (px)`}
onChange={(value) => fineTuneCrop({ dimension: 'width', value })}
value={((crop.width / 100) * originalWidth).toFixed(0)}
ref={widthInputRef}
value={((crop.width / 100) * uncroppedPixelWidth).toFixed(0)}
/>
<Input
name={`${t('upload:height')} (px)`}
onChange={(value) => fineTuneCrop({ dimension: 'height', value })}
value={((crop.height / 100) * originalHeight).toFixed(0)}
ref={heightInputRef}
value={((crop.height / 100) * uncroppedPixelHeight).toFixed(0)}
/>
</div>
</div>

View File

@@ -22,6 +22,7 @@
padding-top: base(0.25);
padding-bottom: base(0.25);
padding-left: base(0.25);
padding-right: base(0.25);
.rs__multi-value {
margin: calc(#{base(0.125)} - #{$style-stroke-width-s * 2});

View File

@@ -187,7 +187,7 @@ const RelationshipField: React.FC<Props> = (props) => {
options.forEach((opt) => {
if (opt.options) {
opt.options.some((subOpt) => {
if (subOpt?.value === val.value) {
if (subOpt?.value == val.value) {
matchedOption = subOpt
return true
}
@@ -200,7 +200,7 @@ const RelationshipField: React.FC<Props> = (props) => {
return matchedOption
}
return options.find((opt) => opt.value === val)
return options.find((opt) => opt.value == val)
})
}
@@ -215,7 +215,7 @@ const RelationshipField: React.FC<Props> = (props) => {
options.forEach((opt) => {
if (opt?.options) {
opt.options.some((subOpt) => {
if (subOpt?.value === valueWithRelation.value) {
if (subOpt?.value == valueWithRelation.value) {
matchedOption = subOpt
return true
}
@@ -227,7 +227,7 @@ const RelationshipField: React.FC<Props> = (props) => {
return matchedOption
}
return options.find((opt) => opt.value === value)
return options.find((opt) => opt.value == value)
}
return undefined

View File

@@ -139,7 +139,7 @@ const Relationship: React.FC<Props> = (props) => {
})
if (!errorLoading) {
relationsToFetch.reduce(async (priorRelation, relation) => {
await relationsToFetch.reduce(async (priorRelation, relation) => {
const relationFilterOption = filterOptionsResult?.[relation]
let lastLoadedPageToUse
if (search !== searchArg) {
@@ -197,12 +197,17 @@ const Relationship: React.FC<Props> = (props) => {
query.where.and.push(relationFilterOption)
}
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
const response = await fetch(
`${serverURL}${api}/${relation}?${qs.stringify(query, {
strictNullHandling: true,
})}`,
{
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
},
})
)
if (response.ok) {
const data: PaginatedDocs<unknown> = await response.json()
@@ -269,7 +274,7 @@ const Relationship: React.FC<Props> = (props) => {
)
const updateSearch = useDebouncedCallback((searchArg: string, valueArg: Value | Value[]) => {
getResults({ search: searchArg, sort: true, value: valueArg })
void getResults({ search: searchArg, sort: true, value: valueArg })
setSearch(searchArg)
}, 300)
@@ -280,7 +285,7 @@ const Relationship: React.FC<Props> = (props) => {
updateSearch(searchArg, valueArg, searchArg !== '')
}
},
[search, updateSearch],
[initialLoadedPageState, search, updateSearch],
)
// ///////////////////////////////////
@@ -294,15 +299,14 @@ const Relationship: React.FC<Props> = (props) => {
value,
})
Object.entries(relationMap).reduce(async (priorRelation, [relation, ids]) => {
void Object.entries(relationMap).reduce(async (priorRelation, [relation, ids]) => {
await priorRelation
const idsToLoad = ids.filter((id) => {
return !options.find(
(optionGroup) =>
optionGroup?.options?.find(
(option) => option.value === id && option.relationTo === relation,
),
return !options.find((optionGroup) =>
optionGroup?.options?.find(
(option) => option.value === id && option.relationTo === relation,
),
)
})
@@ -320,12 +324,17 @@ const Relationship: React.FC<Props> = (props) => {
}
if (!errorLoading) {
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
const response = await fetch(
`${serverURL}${api}/${relation}?${qs.stringify(query, {
strictNullHandling: true,
})}`,
{
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
},
})
)
const collection = collections.find((coll) => coll.slug === relation)
let docs = []
@@ -507,7 +516,7 @@ const Relationship: React.FC<Props> = (props) => {
onMenuOpen={() => {
if (!hasLoadedFirstPage) {
setIsLoading(true)
getResults({
void getResults({
onSuccess: () => {
setHasLoadedFirstPage(true)
setIsLoading(false)
@@ -517,7 +526,7 @@ const Relationship: React.FC<Props> = (props) => {
}
}}
onMenuScrollToBottom={() => {
getResults({
void getResults({
lastFullyLoadedRelation,
search,
sort: false,

View File

@@ -10,6 +10,12 @@
}
}
.has-many {
.rs__input-container {
overflow: hidden;
}
}
html[data-theme='light'] {
.field-type.text {
&.error {

View File

@@ -36,6 +36,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
const showError = valid === false && submitted
const prevValid = useRef(valid)
const prevErrorMessage = useRef(field?.errorMessage)
const prevValue = useRef(value)
// Method to return from `useField`, used to
@@ -128,8 +129,9 @@ const useField = <T,>(options: Options): FieldType<T> => {
// Only dispatch if the validation result has changed
// This will prevent unnecessary rerenders
if (valid !== prevValid.current) {
if (valid !== prevValid.current || errorMessage !== prevErrorMessage.current) {
prevValid.current = valid
prevErrorMessage.current = errorMessage
if (typeof dispatchField === 'function') {
dispatchField({

View File

@@ -11,7 +11,6 @@ import type { AuthContext } from './types'
import { requests } from '../../../api'
import useDebounce from '../../../hooks/useDebounce'
import { useConfig } from '../Config'
import { useLocale } from '../Locale'
const Context = createContext({} as AuthContext)
@@ -21,9 +20,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const [user, setUser] = useState<User | null>()
const [tokenInMemory, setTokenInMemory] = useState<string>()
const [tokenExpiration, setTokenExpiration] = useState<number>()
const [strategy, setStrategy] = useState<string>()
const { pathname } = useLocation()
const { push } = useHistory()
const { code } = useLocale()
const config = useConfig()
@@ -39,9 +38,50 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const { closeAllModals, openModal } = useModal()
const [lastLocationChange, setLastLocationChange] = useState(0)
const debouncedLocationChange = useDebounce(lastLocationChange, 10000)
const userIDRef = React.useRef<null | number | string>()
const id = user?.id
const refreshPermissions = useCallback(
async ({ locale }: { locale?: string } = {}) => {
const params = {
locale,
}
try {
const request = await requests.get(
`${serverURL}${api}/access${qs.stringify(params, { addQueryPrefix: true })}`,
{
headers: {
'Accept-Language': i18n.language,
},
},
)
if (request.status === 200) {
const json: Permissions = await request.json()
setPermissions(json)
} else {
throw new Error(`Fetching permissions failed with status code ${request.status}`)
}
} catch (e) {
toast.error(`Refreshing permissions failed: ${e.message}`)
}
},
[serverURL, api, i18n],
)
const setActiveUser = React.useCallback(
async (userToSet: User | null) => {
if ((userIDRef.current && !userToSet?.id) || userToSet?.id) {
// refresh on logout and login
await refreshPermissions()
}
userIDRef.current = userToSet?.id || null
setUser(userToSet)
},
[refreshPermissions],
)
const redirectToInactivityRoute = useCallback(() => {
if (window.location.pathname.startsWith(admin)) {
const redirectParam = `?redirect=${encodeURIComponent(
@@ -57,6 +97,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const revokeTokenAndExpire = useCallback(() => {
setTokenInMemory(undefined)
setTokenExpiration(undefined)
setStrategy(undefined)
}, [])
const setTokenAndExpiration = useCallback(
@@ -65,6 +106,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (token && json?.exp) {
setTokenInMemory(token)
setTokenExpiration(json.exp)
if (json.strategy) {
setStrategy(json.strategy)
}
} else {
revokeTokenAndExpire()
}
@@ -88,10 +132,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (request.status === 200) {
const json = await request.json()
setUser(json.user)
await setActiveUser(json.user)
setTokenAndExpiration(json)
} else {
setUser(null)
await setActiveUser(null)
redirectToInactivityRoute()
}
} catch (e) {
@@ -105,9 +149,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
serverURL,
api,
userSlug,
i18n,
redirectToInactivityRoute,
i18n.language,
setActiveUser,
setTokenAndExpiration,
redirectToInactivityRoute,
],
)
@@ -123,13 +168,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (request.status === 200) {
const json = await request.json()
if (!skipSetUser) {
setUser(json.user)
await setActiveUser(json.user)
setTokenAndExpiration(json)
}
return json.user
}
setUser(null)
await setActiveUser(null)
redirectToInactivityRoute()
return null
} catch (e) {
@@ -137,36 +182,22 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
return null
}
},
[serverURL, api, userSlug, i18n, redirectToInactivityRoute, setTokenAndExpiration],
[
serverURL,
api,
userSlug,
i18n,
redirectToInactivityRoute,
setTokenAndExpiration,
setActiveUser,
],
)
const logOut = useCallback(() => {
setUser(null)
const logOut = useCallback(async () => {
await setActiveUser(null)
revokeTokenAndExpire()
requests.post(`${serverURL}${api}/${userSlug}/logout`)
}, [serverURL, api, userSlug, revokeTokenAndExpire])
const refreshPermissions = useCallback(async () => {
const params = {
locale: code,
}
try {
const request = await requests.get(`${serverURL}${api}/access?${qs.stringify(params)}`, {
headers: {
'Accept-Language': i18n.language,
},
})
if (request.status === 200) {
const json: Permissions = await request.json()
setPermissions(json)
} else {
throw new Error(`Fetching permissions failed with status code ${request.status}`)
}
} catch (e) {
toast.error(`Refreshing permissions failed: ${e.message}`)
}
}, [serverURL, api, i18n, code])
void requests.post(`${serverURL}${api}/${userSlug}/logout`)
}, [serverURL, api, userSlug, revokeTokenAndExpire, setActiveUser])
const fetchFullUser = React.useCallback(async () => {
try {
@@ -180,7 +211,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const json = await request.json()
if (json?.user) {
setUser(json.user)
await setActiveUser(json.user)
if (json?.token) {
setTokenAndExpiration(json)
}
@@ -199,28 +230,39 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
})
if (autoLoginResult.status === 200) {
const autoLoginJson = await autoLoginResult.json()
setUser(autoLoginJson.user)
await setActiveUser(autoLoginJson.user)
if (autoLoginJson?.token) {
setTokenAndExpiration(autoLoginJson)
}
} else {
setUser(null)
await setActiveUser(null)
revokeTokenAndExpire()
}
} else {
setUser(null)
await setActiveUser(null)
revokeTokenAndExpire()
}
}
} catch (e) {
toast.error(`Fetching user failed: ${e.message}`)
}
}, [serverURL, api, userSlug, i18n, autoLogin, setTokenAndExpiration, revokeTokenAndExpire])
}, [
serverURL,
api,
userSlug,
i18n,
autoLogin,
setTokenAndExpiration,
revokeTokenAndExpire,
setActiveUser,
])
// On mount, get user and set
useEffect(() => {
fetchFullUser()
}, [fetchFullUser])
if (id === undefined || id !== userIDRef.current) {
void fetchFullUser()
}
}, [fetchFullUser, id])
// When location changes, refresh cookie
useEffect(() => {
@@ -233,13 +275,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
setLastLocationChange(Date.now())
}, [pathname])
// When user changes, get new access
useEffect(() => {
if (id) {
refreshPermissions()
}
}, [i18n, id, api, serverURL, refreshPermissions])
useEffect(() => {
let reminder: ReturnType<typeof setTimeout>
const now = Math.round(new Date().getTime() / 1000)
@@ -266,8 +301,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (remainingTime > 0) {
forceLogOut = setTimeout(
() => {
setUser(null)
async () => {
await setActiveUser(null)
revokeTokenAndExpire()
redirectToInactivityRoute()
},
@@ -278,7 +313,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
return () => {
if (forceLogOut) clearTimeout(forceLogOut)
}
}, [tokenExpiration, closeAllModals, i18n, redirectToInactivityRoute, revokeTokenAndExpire])
}, [
tokenExpiration,
closeAllModals,
i18n,
redirectToInactivityRoute,
revokeTokenAndExpire,
setActiveUser,
])
return (
<Context.Provider
@@ -289,8 +331,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
refreshCookie,
refreshCookieAsync,
refreshPermissions,
setUser,
setUser: setActiveUser,
strategy,
token: tokenInMemory,
tokenExpiration,
user,
}}
>

View File

@@ -2,12 +2,14 @@ import type { Permissions, User } from '../../../../auth/types'
export type AuthContext<T = User> = {
fetchFullUser: () => Promise<void>
logOut: () => void
logOut: () => Promise<void>
permissions?: Permissions
refreshCookie: (forceRefresh?: boolean) => void
refreshCookieAsync: () => Promise<User>
refreshPermissions: () => Promise<void>
setUser: (user: T) => void
refreshPermissions: ({ locale }?: { locale?: string }) => Promise<void>
setUser: (user: T) => Promise<void>
strategy?: string
token?: string
tokenExpiration?: number
user?: T | null
}

View File

@@ -13,7 +13,7 @@ const LocaleContext = createContext({} as Locale)
export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const { localization } = useConfig()
const { user } = useAuth()
const { refreshPermissions, user } = useAuth()
const defaultLocale =
localization && localization.defaultLocale ? localization.defaultLocale : 'en'
const searchParams = useSearchParams()
@@ -26,6 +26,22 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child
const { getPreference, setPreference } = usePreferences()
const localeFromParams = searchParams.locale
const handleLocaleChange = React.useCallback(
(newLocaleCode: string) => {
if (!localization) return
if (localization.localeCodes.indexOf(newLocaleCode) > -1) {
setLocaleCode(newLocaleCode)
setLocale(findLocaleFromCode(localization, newLocaleCode))
if (user) {
void setPreference('locale', newLocaleCode)
void refreshPermissions({ locale: newLocaleCode })
}
}
},
[localization, setPreference, user, refreshPermissions],
)
useEffect(() => {
if (!localization) {
return
@@ -33,31 +49,23 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child
// set locale from search param
if (localeFromParams && localization.localeCodes.indexOf(localeFromParams as string) > -1) {
setLocaleCode(localeFromParams as string)
setLocale(findLocaleFromCode(localization, localeFromParams as string))
if (user) setPreference('locale', localeFromParams)
handleLocaleChange(localeFromParams as string)
return
}
// set locale from preferences or default
;(async () => {
const initializeLocale = async () => {
let preferenceLocale: string
let isPreferenceInConfig: boolean
if (user) {
preferenceLocale = await getPreference<string>('locale')
isPreferenceInConfig =
preferenceLocale && localization.localeCodes.indexOf(preferenceLocale) > -1
if (isPreferenceInConfig) {
setLocaleCode(preferenceLocale)
setLocale(findLocaleFromCode(localization, preferenceLocale))
return
}
setPreference('locale', defaultLocale)
handleLocaleChange(preferenceLocale)
return
}
setLocaleCode(defaultLocale)
setLocale(findLocaleFromCode(localization, defaultLocale))
})()
}, [defaultLocale, getPreference, localeFromParams, setPreference, user, localization])
handleLocaleChange(defaultLocale)
}
void initializeLocale()
}, [defaultLocale, getPreference, handleLocaleChange, localeFromParams, localization, user])
return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
}

View File

@@ -3,11 +3,13 @@ import React from 'react'
import type { UploadEdits } from '../../../../uploads/types'
export type UploadEditsContext = {
resetUploadEdits: () => void
updateUploadEdits: (edits: UploadEdits) => void
uploadEdits: UploadEdits
}
const Context = React.createContext<UploadEditsContext>({
resetUploadEdits: undefined,
updateUploadEdits: undefined,
uploadEdits: undefined,
})
@@ -15,6 +17,10 @@ const Context = React.createContext<UploadEditsContext>({
export const UploadEditsProvider = ({ children }) => {
const [uploadEdits, setUploadEdits] = React.useState<UploadEdits>(undefined)
const resetUploadEdits = () => {
setUploadEdits({})
}
const updateUploadEdits = (edits: UploadEdits) => {
setUploadEdits((prevEdits) => ({
...(prevEdits || {}),
@@ -22,7 +28,11 @@ export const UploadEditsProvider = ({ children }) => {
}))
}
return <Context.Provider value={{ updateUploadEdits, uploadEdits }}>{children}</Context.Provider>
return (
<Context.Provider value={{ resetUploadEdits, updateUploadEdits, uploadEdits }}>
{children}
</Context.Provider>
)
}
export const useUploadEdits = (): UploadEditsContext => React.useContext(Context)

View File

@@ -65,7 +65,7 @@ const DefaultGlobalView: React.FC<DefaultGlobalViewProps> = (props) => {
}, [global.slug, location.pathname, global?.admin?.components?.views?.Edit, setViewActions])
return (
<main className={baseClass}>
<main className={`${baseClass} ${baseClass}--${global.slug}`}>
<OperationContext.Provider value="update">
<SetStepNav global={global} />
<Form

View File

@@ -25,7 +25,7 @@ const Logout: React.FC<{ inactivity?: boolean }> = (props) => {
const redirect = query.get('redirect')
useEffect(() => {
logOut()
void logOut()
}, [logOut])
return (

View File

@@ -3,12 +3,14 @@ import { useTranslation } from 'react-i18next'
import Button from '../../elements/Button'
import MinimalTemplate from '../../templates/Minimal'
import { useAuth } from '../../utilities/Auth'
import { useConfig } from '../../utilities/Config'
import Meta from '../../utilities/Meta'
const Unauthorized: React.FC = () => {
const { t } = useTranslation('general')
const config = useConfig()
const { user } = useAuth()
const {
admin: { logoutRoute },
routes: { admin },
@@ -20,11 +22,11 @@ const Unauthorized: React.FC = () => {
keywords={t('error:unauthorized')}
title={t('error:unauthorized')}
/>
<h2>{t('error:unauthorized')}</h2>
<h2>{user ? t('general:unauthorized') : t('error:unauthorized')}</h2>
<p>{t('error:notAllowedToAccessPage')}</p>
<br />
<Button el="link" to={`${admin}${logoutRoute}`}>
{t('authentication:logOut')}
<Button el="link" to={`${admin}${user ? '' : logoutRoute}`}>
{user ? t('general:backToDashboard') : t('authentication:logOut')}
</Button>
</MinimalTemplate>
)

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