Compare commits
81 Commits
db-postgre
...
db-postgre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
769bee82cd | ||
|
|
55eb6e7583 | ||
|
|
a907480a94 | ||
|
|
9c25754eed | ||
|
|
7c9ec9c4e0 | ||
|
|
a0bd7060c4 | ||
|
|
169da5c3d8 | ||
|
|
25932c0db6 | ||
|
|
3bce3d4240 | ||
|
|
e2a13deff6 | ||
|
|
5d99adfd38 | ||
|
|
819753a637 | ||
|
|
b7d65ab717 | ||
|
|
e365300bee | ||
|
|
4d9f494a80 | ||
|
|
fa6d4596dc | ||
|
|
08b02ecba2 | ||
|
|
eba777c3a0 | ||
|
|
bfe6918681 | ||
|
|
e5d6cdae38 | ||
|
|
218f2ead03 | ||
|
|
e9c1222182 | ||
|
|
c8ed6454a7 | ||
|
|
4077598777 | ||
|
|
65d7d54ba3 | ||
|
|
6690c37c4e | ||
|
|
0efc610210 | ||
|
|
cc99c3a619 | ||
|
|
24a8dc7aa3 | ||
|
|
90764efa9a | ||
|
|
d05e3b0411 | ||
|
|
e4bc281fc2 | ||
|
|
9d05b82dc6 | ||
|
|
f2284f3d1b | ||
|
|
1347b6cc36 | ||
|
|
0a56d50334 | ||
|
|
02999a5659 | ||
|
|
365127bee4 | ||
|
|
b67e97aa7f | ||
|
|
61e8ce1743 | ||
|
|
034aa68cd4 | ||
|
|
268e6c485e | ||
|
|
4c1a5dca44 | ||
|
|
a12d1f4755 | ||
|
|
d55be73992 | ||
|
|
b9f236ae50 | ||
|
|
1d38e6d5d5 | ||
|
|
2f3c994cea | ||
|
|
0586f236bb | ||
|
|
d582619ead | ||
|
|
17fc2d13d0 | ||
|
|
800ffd2611 | ||
|
|
661ca74364 | ||
|
|
ec73b461a8 | ||
|
|
94885f3c65 | ||
|
|
31d0b309fe | ||
|
|
c86526b5c8 | ||
|
|
28a065072f | ||
|
|
efc0bc9ec9 | ||
|
|
ade1d27c95 | ||
|
|
1040731e32 | ||
|
|
30f28898b6 | ||
|
|
6cb0470906 | ||
|
|
170ea5badc | ||
|
|
cfb56589eb | ||
|
|
f312bac065 | ||
|
|
3dd3f5b135 | ||
|
|
59f4d125ab | ||
|
|
b2b2ee3338 | ||
|
|
7308abaabd | ||
|
|
9b1d0b2d0f | ||
|
|
9014f1fa63 | ||
|
|
ba75d876e3 | ||
|
|
f2b2e5cda9 | ||
|
|
f751f69239 | ||
|
|
f7ac9ff52a | ||
|
|
ba7a043a99 | ||
|
|
b149180db4 | ||
|
|
4efb9dd867 | ||
|
|
7002ca78b9 | ||
|
|
44ca3a4073 |
75
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
75
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
@@ -1,14 +1,58 @@
|
||||
name: Bug Report v3
|
||||
description: Create a bug report for Payload v3 (beta)
|
||||
labels: ['status: needs-triage', 'v3']
|
||||
name: Functionality Bug
|
||||
description: '[REPRODUCTION REQUIRED] - Create a bug report'
|
||||
labels: ['status: needs-triage', 'v3', 'validate-reproduction']
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: reproduction-link
|
||||
attributes:
|
||||
label: Link to reproduction
|
||||
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.
|
||||
label: Link to the code that reproduces this issue
|
||||
description: >-
|
||||
_REQUIRED_: Please provide a link to your reproduction. Note, if the URL is invalid (404 or a private repository), we may close the issue.
|
||||
Either use `npx create-payload-app@beta -t blank` then push to a repo or follow the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md) for more information.
|
||||
validations:
|
||||
required: false
|
||||
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: dropdown
|
||||
attributes:
|
||||
label: Which area(s) are affected? (Select all that apply)
|
||||
multiple: true
|
||||
options:
|
||||
- 'Not sure'
|
||||
- 'area: core'
|
||||
- 'area: docs'
|
||||
- 'area: templates'
|
||||
- 'area: ui'
|
||||
- 'db-mongodb'
|
||||
- 'db-postgres'
|
||||
- 'db-sqlite'
|
||||
- 'db-vercel-postgres'
|
||||
- 'plugin: cloud'
|
||||
- 'plugin: cloud-storage'
|
||||
- 'plugin: form-builder'
|
||||
- 'plugin: nested-docs'
|
||||
- 'plugin: richtext-lexical'
|
||||
- 'plugin: richtext-slate'
|
||||
- 'plugin: search'
|
||||
- 'plugin: sentry'
|
||||
- 'plugin: seo'
|
||||
- 'plugin: stripe'
|
||||
- 'plugin: other'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -22,25 +66,6 @@ body:
|
||||
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.
|
||||
|
||||
41
.github/ISSUE_TEMPLATE/2.design_issue.yml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/2.design_issue.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Design Issue
|
||||
description: '[SCREENSHOT REQUIRED] - Create a design issue report'
|
||||
labels: ['status: needs-triage', 'v3', 'area: ui']
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Bug.
|
||||
description: >-
|
||||
_REQUIRED:_ Please a screenshot/video of the issue along with a detailed description of the problem.
|
||||
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: textarea
|
||||
attributes:
|
||||
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: 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.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Contributors should be able to follow the steps provided in order to reproduce the bug.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: These steps are used to add integration tests to ensure the same issue does not happen again. Thanks in advance!
|
||||
@@ -1,11 +1,11 @@
|
||||
name: Bug Report
|
||||
description: Create a bug report for Payload
|
||||
name: v2 Bug Report
|
||||
description: Report a bug for Payload v2. ONLY CRITICAL bugs will be fixed in v2.
|
||||
labels: ['status: needs-triage', 'v2']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
*Note:* Feature requests should be opened as [discussions](https://github.com/payloadcms/payload/discussions/new?category=feature-requests-ideas).
|
||||
ONLY CRITICAL bugs will be fixed in v2.
|
||||
- type: input
|
||||
id: reproduction-link
|
||||
attributes:
|
||||
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,23 +1,23 @@
|
||||
## Description
|
||||
<!--
|
||||
|
||||
<!-- Please include a summary of the pull request and any related issues it fixes. Please also include relevant motivation and context. -->
|
||||
Thank you for the PR! Please go through the checklist below and make sure you've completed all the steps.
|
||||
|
||||
- [ ] I have read and understand the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository.
|
||||
Please review the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository if you haven't already.
|
||||
|
||||
## Type of change
|
||||
The following items will ensure that your PR is handled as smoothly as possible:
|
||||
|
||||
<!-- Please delete options that are not relevant. -->
|
||||
- PR Title must follow conventional commits format. For example, `feat: my new feature`, `fix(plugin-seo): my fix`.
|
||||
- Minimal description explained as if explained to someone not immediately familiar with the code.
|
||||
- Provide before/after screenshots or code diffs if applicable.
|
||||
- Link any related issues/discussions from GitHub or Discord.
|
||||
- Add review comments if necessary to explain to the reviewer the logic behind a change
|
||||
|
||||
- [ ] 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
|
||||
### What?
|
||||
|
||||
## Checklist:
|
||||
### Why?
|
||||
|
||||
- [ ] 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
|
||||
### How?
|
||||
|
||||
Fixes #
|
||||
|
||||
-->
|
||||
|
||||
13
.github/actions/release-commenter/.eslintrc.js
vendored
Normal file
13
.github/actions/release-commenter/.eslintrc.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
es6: true,
|
||||
node: true,
|
||||
},
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
}
|
||||
8
.github/actions/release-commenter/.prettierrc.js
vendored
Normal file
8
.github/actions/release-commenter/.prettierrc.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
74
.github/actions/release-commenter/README.md
vendored
Normal file
74
.github/actions/release-commenter/README.md
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
# Release Commenter
|
||||
|
||||
This GitHub Action automatically comments on and/or labels Issues and PRs when a fix is released for them.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 🔧 Heavily modified version of https://github.com/apexskier/github-release-commenter
|
||||
|
||||
## Fork Modifications
|
||||
|
||||
- Filters to closed PRs only
|
||||
- Adds tag filter to support non-linear releases
|
||||
- Better logging
|
||||
- Moved to pnpm
|
||||
- Uses @vercel/ncc for packaging
|
||||
- Comments on locked issues by unlocking then re-locking
|
||||
|
||||
## How it works
|
||||
|
||||
Use this action in a workflow [triggered by a release](https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#release). It will scan commits between that and the prior release, find associated Issues and PRs, and comment on them to let people know a release has been made. Associated Issues and PRs can be directly [linked](https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue) to the commit or manually linked from a PR associated with the commit.
|
||||
|
||||
## Inputs
|
||||
|
||||
**GITHUB_TOKEN**
|
||||
|
||||
A GitHub personal access token with repo scope, such as [`secrets.GITHUB_TOKEN`](https://docs.github.com/en/free-pro-team@latest/actions/reference/authentication-in-a-workflow#about-the-github_token-secret).
|
||||
|
||||
**comment-template** (optional)
|
||||
|
||||
Override the comment posted on Issues and PRs. Set to the empty string to disable commenting. Several variables strings will be automatically replaced:
|
||||
|
||||
- `{release_link}` - a markdown link to the release
|
||||
- `{release_name}` - the release's name
|
||||
- `{release_tag}` - the release's tag
|
||||
|
||||
**label-template** (optional)
|
||||
|
||||
Add the given label. Multiple labels can be separated by commas. Several variable strings will be automatically replaced:
|
||||
|
||||
- `{release_name}` - the release's name
|
||||
- `{release_tag}` - the release's tag
|
||||
|
||||
**skip-label** (optional)
|
||||
|
||||
Skip processing if any of the given labels are present. Same processing rules as **label-template**. Default is "dependencies".
|
||||
|
||||
## Example
|
||||
|
||||
```yml
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
steps:
|
||||
- uses: apexskier/github-release-commenter@v1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
comment-template: |
|
||||
Release {release_link} addresses this.
|
||||
```
|
||||
|
||||
## Known limitations
|
||||
|
||||
These are some known limitations of this action. I'd like to try to address them in the future.
|
||||
|
||||
- Non-linear releases aren't supported. For example, releasing a patch to a prior major release after a new major release has been bumped.
|
||||
- Non-sequential releases aren't supported. For example, if you release multiple prereleases between two official releases, this will only create a comment for the first prerelease in which a fix is released, not the final release.
|
||||
- The first release for a project will be ignored. This is intentional, as the use case is unlikely. Most projects will either have several alphas that don't need release comments, or won't use issues/PRs for the first commit.
|
||||
- If a large number of things are commented on, you may see the error `Error: You have triggered an abuse detection mechanism. Please wait a few minutes before you try again.`. Consider using the `skip-label` input to reduce your load on the GitHub API.
|
||||
|
||||
## Versions
|
||||
|
||||
Workflows will automatically update the tags `v1` and `latest`, allowing you to reference one of those instead of locking to a specific release.
|
||||
32
.github/actions/release-commenter/action.yml
vendored
Normal file
32
.github/actions/release-commenter/action.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Release Commenter
|
||||
description: Comment on PRs and Issues when a fix is released
|
||||
branding:
|
||||
icon: message-square
|
||||
color: blue
|
||||
inputs:
|
||||
GITHUB_TOKEN:
|
||||
description: |
|
||||
A GitHub personal access token with repo scope, such as
|
||||
secrets.GITHUB_TOKEN.
|
||||
required: true
|
||||
comment-template:
|
||||
description: |
|
||||
Text template for the comment string.
|
||||
required: false
|
||||
default: |
|
||||
Included in release {release_link}
|
||||
label-template:
|
||||
description: Add the given label. Multiple labels can be separated by commas.
|
||||
required: false
|
||||
skip-label:
|
||||
description: Skip commenting if any of the given label are present. Multiple labels can be separated by commas.
|
||||
required: false
|
||||
default: "dependencies"
|
||||
tag-filter:
|
||||
description: |
|
||||
Filter tags by a regular expression. Must be escaped. e.g. 'v\\d' to isolate tags between major versions.
|
||||
required: false
|
||||
default: null
|
||||
runs:
|
||||
using: node20
|
||||
main: dist/index.js
|
||||
34199
.github/actions/release-commenter/dist/index.js
vendored
Normal file
34199
.github/actions/release-commenter/dist/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
.github/actions/release-commenter/jest.config.js
vendored
Normal file
7
.github/actions/release-commenter/jest.config.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/dist/'],
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': ['@swc/jest'],
|
||||
},
|
||||
}
|
||||
34
.github/actions/release-commenter/package.json
vendored
Normal file
34
.github/actions/release-commenter/package.json
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "release-commenter",
|
||||
"version": "0.0.0",
|
||||
"description": "GitHub Action to automatically comment on PRs and Issues when a fix is released.",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"build": "pnpm build:typecheck && pnpm build:ncc",
|
||||
"build:ncc": "ncc build src/index.ts -t -o dist",
|
||||
"build:typecheck": "tsc",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.3.0",
|
||||
"@actions/github": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/webhooks-types": "^7.5.1",
|
||||
"@swc/jest": "^0.2.36",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^20.16.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"@vercel/ncc": "0.38.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^7.32.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.3.3",
|
||||
"ts-jest": "^26.5.6",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
||||
5419
.github/actions/release-commenter/pnpm-lock.yaml
generated
vendored
Normal file
5419
.github/actions/release-commenter/pnpm-lock.yaml
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
266
.github/actions/release-commenter/src/__snapshots__/index.test.ts.snap
vendored
Normal file
266
.github/actions/release-commenter/src/__snapshots__/index.test.ts.snap
vendored
Normal file
@@ -0,0 +1,266 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`tests feature tests can apply labels 1`] = `
|
||||
[
|
||||
[
|
||||
{
|
||||
"issue_number": 123,
|
||||
"labels": [
|
||||
":dart: landed",
|
||||
"release-current_tag_name",
|
||||
"Release Name",
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"issue_number": 7,
|
||||
"labels": [
|
||||
":dart: landed",
|
||||
"release-current_tag_name",
|
||||
"Release Name",
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`tests main test 1`] = `
|
||||
{
|
||||
"graphql": [MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
"
|
||||
{
|
||||
resource(url: "http://repository/commit/SHA1") {
|
||||
... on Commit {
|
||||
messageHeadlineHTML
|
||||
messageBodyHTML
|
||||
associatedPullRequests(first: 10) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
bodyHTML
|
||||
number
|
||||
state
|
||||
labels(first: 10) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
timelineItems(itemTypes: [CONNECTED_EVENT, DISCONNECTED_EVENT], first: 100) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
nodes {
|
||||
... on ConnectedEvent {
|
||||
__typename
|
||||
isCrossRepository
|
||||
subject {
|
||||
... on Issue {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DisconnectedEvent {
|
||||
__typename
|
||||
isCrossRepository
|
||||
subject {
|
||||
... on Issue {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
",
|
||||
],
|
||||
[
|
||||
"
|
||||
{
|
||||
resource(url: "http://repository/commit/SHA2") {
|
||||
... on Commit {
|
||||
messageHeadlineHTML
|
||||
messageBodyHTML
|
||||
associatedPullRequests(first: 10) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
bodyHTML
|
||||
number
|
||||
state
|
||||
labels(first: 10) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
timelineItems(itemTypes: [CONNECTED_EVENT, DISCONNECTED_EVENT], first: 100) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
nodes {
|
||||
... on ConnectedEvent {
|
||||
__typename
|
||||
isCrossRepository
|
||||
subject {
|
||||
... on Issue {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DisconnectedEvent {
|
||||
__typename
|
||||
isCrossRepository
|
||||
subject {
|
||||
... on Issue {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
",
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"rest": {
|
||||
"issues": {
|
||||
"addLabels": [MockFunction],
|
||||
"createComment": [MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
{
|
||||
"body": "Included in release [current_tag_name](http://current_release). Replacements: current_tag_name, current_tag_name.",
|
||||
"issue_number": 3,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"body": "Included in release [current_tag_name](http://current_release). Replacements: current_tag_name, current_tag_name.",
|
||||
"issue_number": 123,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"body": "Included in release [current_tag_name](http://current_release). Replacements: current_tag_name, current_tag_name.",
|
||||
"issue_number": 7,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"get": [MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
{
|
||||
"issue_number": 3,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"issue_number": 123,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"issue_number": 7,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"repos": {
|
||||
"compareCommits": [MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
{
|
||||
"base": "prior_tag_name",
|
||||
"head": "current_tag_name",
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"listReleases": [MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
{
|
||||
"per_page": 100,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
399
.github/actions/release-commenter/src/index.test.ts
vendored
Normal file
399
.github/actions/release-commenter/src/index.test.ts
vendored
Normal file
@@ -0,0 +1,399 @@
|
||||
import type * as githubModule from '@actions/github'
|
||||
import type * as coreModule from '@actions/core'
|
||||
import { mock } from 'node:test'
|
||||
|
||||
jest.mock('@actions/core')
|
||||
jest.mock('@actions/github')
|
||||
|
||||
type Mocked<T> = {
|
||||
-readonly [P in keyof T]: T[P] extends Function ? jest.Mock<T[P]> : jest.Mocked<Partial<T[P]>>
|
||||
}
|
||||
|
||||
const github = require('@actions/github') as jest.Mocked<Mocked<typeof githubModule>>
|
||||
const core = require('@actions/core') as jest.Mocked<Mocked<typeof coreModule>>
|
||||
|
||||
describe('tests', () => {
|
||||
let mockOctokit: any = {}
|
||||
let currentTag: string = 'current_tag_name'
|
||||
|
||||
;(core.warning as any) = jest.fn(console.warn.bind(console))
|
||||
;(core.error as any) = jest.fn(console.error.bind(console))
|
||||
|
||||
let commentTempate: string = ''
|
||||
let labelTemplate: string | null = null
|
||||
const skipLabelTemplate: string | null = 'skip,test'
|
||||
let tagFilter: string | RegExp | null = null
|
||||
|
||||
let simpleMockOctokit: any = {}
|
||||
|
||||
beforeEach(() => {
|
||||
tagFilter = null
|
||||
currentTag = 'current_tag_name'
|
||||
;(github.context as any) = {
|
||||
payload: {
|
||||
repo: {
|
||||
owner: 'owner',
|
||||
repo: 'repo',
|
||||
},
|
||||
release: {
|
||||
tag_name: currentTag,
|
||||
},
|
||||
repository: { html_url: 'http://repository' },
|
||||
},
|
||||
}
|
||||
|
||||
github.getOctokit.mockReset().mockImplementationOnce(((token: string) => {
|
||||
expect(token).toBe('GITHUB_TOKEN_VALUE')
|
||||
return mockOctokit
|
||||
}) as any)
|
||||
;(core.getInput as any).mockImplementation((key: string) => {
|
||||
if (key == 'GITHUB_TOKEN') {
|
||||
return 'GITHUB_TOKEN_VALUE'
|
||||
}
|
||||
if (key == 'comment-template') {
|
||||
return commentTempate
|
||||
}
|
||||
if (key == 'label-template') {
|
||||
return labelTemplate
|
||||
}
|
||||
if (key == 'skip-label') {
|
||||
return skipLabelTemplate
|
||||
}
|
||||
if (key == 'tag-filter') {
|
||||
return tagFilter
|
||||
}
|
||||
fail(`Unexpected input key ${key}`)
|
||||
})
|
||||
|
||||
commentTempate =
|
||||
'Included in release {release_link}. Replacements: {release_name}, {release_tag}.'
|
||||
labelTemplate = null
|
||||
simpleMockOctokit = {
|
||||
rest: {
|
||||
issues: {
|
||||
get: jest.fn(() => Promise.resolve({ data: { locked: false } })),
|
||||
createComment: jest.fn(() => Promise.resolve()),
|
||||
addLabels: jest.fn(() => Promise.resolve()),
|
||||
},
|
||||
repos: {
|
||||
listReleases: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: [
|
||||
{
|
||||
name: 'Release Name',
|
||||
tag_name: 'current_tag_name',
|
||||
html_url: 'http://current_release',
|
||||
},
|
||||
{
|
||||
tag_name: 'prior_tag_name',
|
||||
html_url: 'http://prior_release',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
compareCommits: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { commits: [{ sha: 'SHA1' }] },
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
graphql: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
resource: {
|
||||
messageHeadlineHTML: '',
|
||||
messageBodyHTML:
|
||||
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
|
||||
associatedPullRequests: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
expect(core.error).not.toHaveBeenCalled()
|
||||
expect(core.warning).not.toHaveBeenCalled()
|
||||
expect(core.setFailed).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('main test', async () => {
|
||||
mockOctokit = {
|
||||
...simpleMockOctokit,
|
||||
rest: {
|
||||
issues: {
|
||||
get: jest.fn(() => Promise.resolve({ data: { locked: false } })),
|
||||
createComment: jest.fn(() => Promise.resolve()),
|
||||
addLabels: jest.fn(() => Promise.resolve()),
|
||||
},
|
||||
repos: {
|
||||
listReleases: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: [
|
||||
{
|
||||
tag_name: 'current_tag_name',
|
||||
html_url: 'http://current_release',
|
||||
},
|
||||
{
|
||||
tag_name: 'prior_tag_name',
|
||||
html_url: 'http://prior_release',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
compareCommits: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { commits: [{ sha: 'SHA1' }, { sha: 'SHA2' }] },
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
graphql: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
resource: {
|
||||
messageHeadlineHTML:
|
||||
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #3.">Closes</span> <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="718013420" data-permission-text="Title is private" data-url="https://github.com/apexskier/github-release-commenter/issues/1" data-hovercard-type="issue" data-hovercard-url="/apexskier/github-release-commenter/issues/1/hovercard" href="https://github.com/apexskier/github-release-commenter/issues/1">#1</a>',
|
||||
messageBodyHTML:
|
||||
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
|
||||
associatedPullRequests: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
bodyHTML:
|
||||
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #4.">Closes</span> <span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #5.">Closes</span>',
|
||||
number: 9,
|
||||
labels: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [{ name: 'label1' }, { name: 'label2' }],
|
||||
},
|
||||
timelineItems: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [
|
||||
{
|
||||
isCrossRepository: true,
|
||||
__typename: 'ConnectedEvent',
|
||||
subject: { number: 1 },
|
||||
},
|
||||
{
|
||||
isCrossRepository: false,
|
||||
__typename: 'ConnectedEvent',
|
||||
subject: { number: 2 },
|
||||
},
|
||||
{
|
||||
isCrossRepository: false,
|
||||
__typename: 'DisconnectedEvent',
|
||||
subject: { number: 2 },
|
||||
},
|
||||
{
|
||||
isCrossRepository: false,
|
||||
__typename: 'ConnectedEvent',
|
||||
subject: { number: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
bodyHTML: '',
|
||||
number: 42,
|
||||
labels: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [{ name: 'label1' }, { name: 'skip' }],
|
||||
},
|
||||
timelineItems: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [
|
||||
{
|
||||
isCrossRepository: true,
|
||||
__typename: 'ConnectedEvent',
|
||||
subject: { number: 82 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
jest.isolateModules(() => {
|
||||
require('./index')
|
||||
})
|
||||
|
||||
await new Promise<void>(setImmediate)
|
||||
|
||||
expect(mockOctokit).toMatchSnapshot()
|
||||
expect(mockOctokit.rest.issues.createComment).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
describe('can filter tags', () => {
|
||||
const v3prev = 'v3.0.1'
|
||||
const v3current = 'v3.0.2'
|
||||
const v2prev = 'v2.0.1'
|
||||
const v2current = 'v2.0.2'
|
||||
|
||||
const listReleasesData = [
|
||||
{
|
||||
name: 'Current Release Name',
|
||||
tag_name: v3current,
|
||||
html_url: 'http://v3.0.2',
|
||||
},
|
||||
{
|
||||
name: 'Prev Release Name',
|
||||
tag_name: v3prev,
|
||||
html_url: 'http://v3.0.1',
|
||||
},
|
||||
{
|
||||
name: 'v2 Current Release Name',
|
||||
tag_name: v2current,
|
||||
html_url: 'http://v2.0.2',
|
||||
},
|
||||
{
|
||||
name: 'v2 Prev Release Name',
|
||||
tag_name: v2prev,
|
||||
html_url: 'http://v2.0.1',
|
||||
},
|
||||
]
|
||||
|
||||
it.each`
|
||||
description | prevTag | currentTag | filter
|
||||
${'no filter'} | ${v3prev} | ${v3current} | ${null}
|
||||
${'v3'} | ${v3prev} | ${v3current} | ${'v\\d'}
|
||||
${'v2'} | ${v2prev} | ${v2current} | ${'v\\d'}
|
||||
`('should filter tags with $description', async ({ prevTag, currentTag, filter }) => {
|
||||
// @ts-ignore
|
||||
github.context.payload.release.tag_name = currentTag
|
||||
|
||||
tagFilter = filter
|
||||
|
||||
mockOctokit = {
|
||||
...simpleMockOctokit,
|
||||
rest: {
|
||||
issues: {
|
||||
get: jest.fn(() => Promise.resolve({ data: { locked: false } })),
|
||||
createComment: jest.fn(() => Promise.resolve()),
|
||||
addLabels: jest.fn(() => Promise.resolve()),
|
||||
},
|
||||
repos: {
|
||||
listReleases: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: listReleasesData,
|
||||
}),
|
||||
),
|
||||
compareCommits: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { commits: [{ sha: 'SHA1' }] },
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
graphql: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
resource: {
|
||||
messageHeadlineHTML: '',
|
||||
messageBodyHTML:
|
||||
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
|
||||
associatedPullRequests: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
jest.isolateModules(() => {
|
||||
require('./index')
|
||||
})
|
||||
|
||||
await new Promise<void>(resolve => setImmediate(() => resolve()))
|
||||
|
||||
expect(github.getOctokit).toHaveBeenCalled()
|
||||
expect(mockOctokit.rest.repos.compareCommits.mock.calls).toEqual([
|
||||
[{ base: prevTag, head: currentTag }],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('feature tests', () => {
|
||||
beforeEach(() => {
|
||||
mockOctokit = simpleMockOctokit
|
||||
})
|
||||
|
||||
it('can disable comments', async () => {
|
||||
commentTempate = ''
|
||||
|
||||
jest.isolateModules(() => {
|
||||
require('./index')
|
||||
})
|
||||
|
||||
await new Promise<void>(resolve => setImmediate(() => resolve()))
|
||||
|
||||
expect(github.getOctokit).toHaveBeenCalled()
|
||||
expect(mockOctokit.rest.issues.createComment).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should unlock and comment', async () => {
|
||||
mockOctokit = {
|
||||
...simpleMockOctokit,
|
||||
rest: {
|
||||
...simpleMockOctokit.rest,
|
||||
issues: {
|
||||
// Return locked for both issues to be commented on
|
||||
get: jest.fn(() => Promise.resolve({ data: { locked: true } })),
|
||||
lock: jest.fn(() => Promise.resolve()),
|
||||
unlock: jest.fn(() => Promise.resolve()),
|
||||
createComment: jest.fn(() => Promise.resolve()),
|
||||
},
|
||||
},
|
||||
graphql: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
resource: {
|
||||
messageHeadlineHTML: '',
|
||||
messageBodyHTML:
|
||||
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
|
||||
associatedPullRequests: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
jest.isolateModules(() => {
|
||||
require('./index')
|
||||
})
|
||||
|
||||
await new Promise<void>(resolve => setImmediate(() => resolve()))
|
||||
|
||||
expect(github.getOctokit).toHaveBeenCalled()
|
||||
|
||||
// Should call once for both linked issues
|
||||
expect(mockOctokit.rest.issues.unlock).toHaveBeenCalledTimes(2)
|
||||
expect(mockOctokit.rest.issues.createComment).toHaveBeenCalledTimes(2)
|
||||
expect(mockOctokit.rest.issues.lock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it.skip('can apply labels', async () => {
|
||||
labelTemplate = ':dart: landed,release-{release_tag},{release_name}'
|
||||
|
||||
jest.isolateModules(() => {
|
||||
require('./index')
|
||||
})
|
||||
|
||||
await new Promise<void>(resolve => setImmediate(() => resolve()))
|
||||
|
||||
expect(github.getOctokit).toHaveBeenCalled()
|
||||
expect(mockOctokit.rest.issues.addLabels.mock.calls).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
349
.github/actions/release-commenter/src/index.ts
vendored
Normal file
349
.github/actions/release-commenter/src/index.ts
vendored
Normal file
@@ -0,0 +1,349 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import type * as Webhooks from '@octokit/webhooks-types'
|
||||
|
||||
const closesMatcher = /aria-label="This (?:commit|pull request) closes issue #(\d+)\."/g
|
||||
|
||||
const releaseLinkTemplateRegex = /{release_link}/g
|
||||
const releaseNameTemplateRegex = /{release_name}/g
|
||||
const releaseTagTemplateRegex = /{release_tag}/g
|
||||
|
||||
;(async function main() {
|
||||
try {
|
||||
const payload = github.context.payload as Webhooks.EventPayloadMap['release']
|
||||
|
||||
const githubToken = core.getInput('GITHUB_TOKEN')
|
||||
const tagFilter = core.getInput('tag-filter') || undefined // Accept tag filter as an input
|
||||
const octokit = github.getOctokit(githubToken)
|
||||
|
||||
const commentTemplate = core.getInput('comment-template')
|
||||
const labelTemplate = core.getInput('label-template') || null
|
||||
const skipLabelTemplate = core.getInput('skip-label') || null
|
||||
|
||||
// Fetch the releases with the optional tag filter applied
|
||||
const { data: rawReleases } = await octokit.rest.repos.listReleases({
|
||||
...github.context.repo,
|
||||
per_page: 100,
|
||||
})
|
||||
|
||||
// Get the current release tag or latest tag
|
||||
const currentTag = payload?.release?.tag_name || rawReleases?.[0]?.tag_name
|
||||
|
||||
let releases = rawReleases
|
||||
|
||||
// Filter releases by the tag filter if provided
|
||||
if (tagFilter) {
|
||||
core.info(`Filtering releases by tag filter: ${tagFilter}`)
|
||||
// Get the matching part of the current release tag
|
||||
const regexMatch = currentTag.match(tagFilter)?.[0]
|
||||
if (!regexMatch) {
|
||||
core.error(`Current release tag ${currentTag} does not match the tag filter ${tagFilter}`)
|
||||
return
|
||||
}
|
||||
|
||||
core.info(`Matched string from filter: ${regexMatch}`)
|
||||
|
||||
releases = releases
|
||||
.filter(release => {
|
||||
const match = release.tag_name.match(regexMatch)?.[0]
|
||||
return match
|
||||
})
|
||||
.slice(0, 2)
|
||||
}
|
||||
|
||||
core.info(`Releases: ${JSON.stringify(releases, null, 2)}`)
|
||||
|
||||
if (releases.length < 2) {
|
||||
if (!releases.length) {
|
||||
core.error(`No releases found with the provided tag filter: '${tagFilter}'`)
|
||||
return
|
||||
}
|
||||
|
||||
core.info('first release')
|
||||
return
|
||||
}
|
||||
|
||||
const [currentRelease, priorRelease] = releases
|
||||
|
||||
core.info(`${priorRelease.tag_name}...${currentRelease.tag_name}`)
|
||||
|
||||
const {
|
||||
data: { commits },
|
||||
} = await octokit.rest.repos.compareCommits({
|
||||
...github.context.repo,
|
||||
base: priorRelease.tag_name,
|
||||
head: currentRelease.tag_name,
|
||||
})
|
||||
|
||||
if (!currentRelease.name) {
|
||||
core.info('Current release has no name, will fall back to the tag name.')
|
||||
}
|
||||
const releaseLabel = currentRelease.name || currentRelease.tag_name
|
||||
|
||||
const comment = commentTemplate
|
||||
.trim()
|
||||
.split(releaseLinkTemplateRegex)
|
||||
.join(`[${releaseLabel}](${currentRelease.html_url})`)
|
||||
.split(releaseNameTemplateRegex)
|
||||
.join(releaseLabel)
|
||||
.split(releaseTagTemplateRegex)
|
||||
.join(currentRelease.tag_name)
|
||||
|
||||
const parseLabels = (rawInput: string | null) =>
|
||||
rawInput
|
||||
?.split(releaseNameTemplateRegex)
|
||||
.join(releaseLabel)
|
||||
?.split(releaseTagTemplateRegex)
|
||||
.join(currentRelease.tag_name)
|
||||
?.split(',')
|
||||
?.map(l => l.trim())
|
||||
.filter(l => l)
|
||||
|
||||
const labels = parseLabels(labelTemplate)
|
||||
const skipLabels = parseLabels(skipLabelTemplate)
|
||||
|
||||
const linkedIssuesPrs = new Set<number>()
|
||||
|
||||
await Promise.all(
|
||||
commits.map(commit =>
|
||||
(async () => {
|
||||
const query = `
|
||||
{
|
||||
resource(url: "${payload.repository.html_url}/commit/${commit.sha}") {
|
||||
... on Commit {
|
||||
messageHeadlineHTML
|
||||
messageBodyHTML
|
||||
associatedPullRequests(first: 10) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
bodyHTML
|
||||
number
|
||||
state
|
||||
labels(first: 10) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
timelineItems(itemTypes: [CONNECTED_EVENT, DISCONNECTED_EVENT], first: 100) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
nodes {
|
||||
... on ConnectedEvent {
|
||||
__typename
|
||||
isCrossRepository
|
||||
subject {
|
||||
... on Issue {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DisconnectedEvent {
|
||||
__typename
|
||||
isCrossRepository
|
||||
subject {
|
||||
... on Issue {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const response: {
|
||||
resource: null | {
|
||||
messageHeadlineHTML: string
|
||||
messageBodyHTML: string
|
||||
associatedPullRequests: {
|
||||
pageInfo: { hasNextPage: boolean }
|
||||
edges: ReadonlyArray<{
|
||||
node: {
|
||||
bodyHTML: string
|
||||
number: number
|
||||
state: 'OPEN' | 'CLOSED' | 'MERGED'
|
||||
labels: {
|
||||
pageInfo: { hasNextPage: boolean }
|
||||
nodes: ReadonlyArray<{
|
||||
name: string
|
||||
}>
|
||||
}
|
||||
timelineItems: {
|
||||
pageInfo: { hasNextPage: boolean }
|
||||
nodes: ReadonlyArray<{
|
||||
__typename: 'ConnectedEvent' | 'DisconnectedEvent'
|
||||
isCrossRepository: boolean
|
||||
subject: {
|
||||
number: number
|
||||
}
|
||||
}>
|
||||
}
|
||||
}
|
||||
}>
|
||||
}
|
||||
}
|
||||
} = await octokit.graphql(query)
|
||||
|
||||
if (!response.resource) {
|
||||
return
|
||||
}
|
||||
|
||||
// core.info(JSON.stringify(response.resource, null, 2))
|
||||
|
||||
core.info(`Checking commit: ${payload.repository.html_url}/commit/${commit.sha}`)
|
||||
|
||||
const associatedClosedPREdges = response.resource.associatedPullRequests.edges.filter(
|
||||
e => e.node.state === 'MERGED',
|
||||
)
|
||||
|
||||
if (associatedClosedPREdges.length) {
|
||||
core.info(
|
||||
` Associated Merged PRs:\n ${associatedClosedPREdges.map(pr => `${payload.repository.html_url}/pull/${pr.node.number}`).join('\n ')}`,
|
||||
)
|
||||
} else {
|
||||
core.info(' No associated merged PRs')
|
||||
}
|
||||
|
||||
const html = [
|
||||
response.resource.messageHeadlineHTML,
|
||||
response.resource.messageBodyHTML,
|
||||
...associatedClosedPREdges.map(pr => pr.node.bodyHTML),
|
||||
].join(' ')
|
||||
|
||||
for (const match of html.matchAll(closesMatcher)) {
|
||||
const [, num] = match
|
||||
linkedIssuesPrs.add(parseInt(num, 10))
|
||||
core.info(
|
||||
` Linked issue/PR from closesMatcher: ${payload.repository.html_url}/pull/${num}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (response.resource.associatedPullRequests.pageInfo.hasNextPage) {
|
||||
core.warning(`Too many PRs associated with ${commit.sha}`)
|
||||
}
|
||||
|
||||
const seen = new Set<number>()
|
||||
for (const associatedPR of associatedClosedPREdges) {
|
||||
if (associatedPR.node.timelineItems.pageInfo.hasNextPage) {
|
||||
core.warning(`Too many links for #${associatedPR.node.number}`)
|
||||
}
|
||||
if (associatedPR.node.labels.pageInfo.hasNextPage) {
|
||||
core.warning(`Too many labels for #${associatedPR.node.number}`)
|
||||
}
|
||||
// a skip labels is present on this PR
|
||||
if (
|
||||
skipLabels?.some(l => associatedPR.node.labels.nodes.some(({ name }) => name === l))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
linkedIssuesPrs.add(associatedPR.node.number)
|
||||
core.info(
|
||||
` Linked issue/PR from associated PR: ${payload.repository.html_url}/pull/${associatedPR.node.number}`,
|
||||
)
|
||||
|
||||
// These are sorted by creation date in ascending order. The latest event for a given issue/PR is all we need
|
||||
// ignore links that aren't part of this repo
|
||||
const links = associatedPR.node.timelineItems.nodes
|
||||
.filter(node => !node.isCrossRepository)
|
||||
.reverse()
|
||||
for (const link of links) {
|
||||
if (seen.has(link.subject.number)) {
|
||||
continue
|
||||
}
|
||||
if (link.__typename == 'ConnectedEvent') {
|
||||
linkedIssuesPrs.add(link.subject.number)
|
||||
core.info(
|
||||
`Linked issue/PR from connected event: ${payload.repository.html_url}/pull/${link.subject.number}`,
|
||||
)
|
||||
}
|
||||
seen.add(link.subject.number)
|
||||
}
|
||||
}
|
||||
})(),
|
||||
),
|
||||
)
|
||||
|
||||
core.info(
|
||||
`Final issues/PRs to be commented on: \n${Array.from(linkedIssuesPrs)
|
||||
.map(num => ` ${payload.repository.html_url}/pull/${num}`)
|
||||
.join('\n')}`,
|
||||
)
|
||||
|
||||
const requests: Array<Promise<unknown>> = []
|
||||
for (const issueNumber of linkedIssuesPrs) {
|
||||
const baseRequest = {
|
||||
...github.context.repo,
|
||||
issue_number: issueNumber,
|
||||
}
|
||||
if (comment) {
|
||||
const commentRequest = {
|
||||
...baseRequest,
|
||||
body: comment,
|
||||
}
|
||||
|
||||
// Check if issue is locked or not
|
||||
const { data: issue } = await octokit.rest.issues.get(baseRequest)
|
||||
|
||||
let createCommentPromise: () => Promise<void>
|
||||
if (!issue.locked) {
|
||||
createCommentPromise = async () => {
|
||||
try {
|
||||
await octokit.rest.issues.createComment(commentRequest)
|
||||
} catch (error) {
|
||||
core.error(error as Error)
|
||||
core.error(
|
||||
`Failed to comment on issue/PR: ${issueNumber}. ${payload.repository.html_url}/pull/${issueNumber}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
core.info(
|
||||
`Issue/PR is locked: ${issueNumber}. Unlocking, commenting, and re-locking. ${payload.repository.html_url}/pull/${issueNumber}`,
|
||||
)
|
||||
createCommentPromise = async () => {
|
||||
try {
|
||||
core.debug(`Unlocking issue/PR: ${issueNumber}`)
|
||||
await octokit.rest.issues.unlock(baseRequest)
|
||||
core.debug(`Commenting on issue/PR: ${issueNumber}`)
|
||||
await octokit.rest.issues.createComment(commentRequest)
|
||||
core.debug(`Re-locking issue/PR: ${issueNumber}`)
|
||||
await octokit.rest.issues.lock(baseRequest)
|
||||
} catch (error) {
|
||||
core.error(error as Error)
|
||||
core.error(
|
||||
`Failed to unlock, comment, and re-lock issue/PR: ${issueNumber}. ${payload.repository.html_url}/pull/${issueNumber}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requests.push(createCommentPromise())
|
||||
}
|
||||
if (labels) {
|
||||
const request = {
|
||||
...baseRequest,
|
||||
labels,
|
||||
}
|
||||
// core.info(JSON.stringify(request, null, 2))
|
||||
requests.push(octokit.rest.issues.addLabels(request))
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(requests)
|
||||
} catch (error) {
|
||||
core.error(error as Error)
|
||||
core.setFailed((error as Error).message)
|
||||
}
|
||||
})()
|
||||
15
.github/actions/release-commenter/tsconfig.json
vendored
Normal file
15
.github/actions/release-commenter/tsconfig.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["es2020.string"],
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
13
.github/actions/triage/.eslintrc.js
vendored
Normal file
13
.github/actions/triage/.eslintrc.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
es6: true,
|
||||
node: true,
|
||||
},
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
}
|
||||
8
.github/actions/triage/.prettierrc.js
vendored
Normal file
8
.github/actions/triage/.prettierrc.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
22
.github/actions/triage/LICENSE
vendored
Normal file
22
.github/actions/triage/LICENSE
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Payload <info@payloadcms.com>. All modification and additions are copyright of Payload.
|
||||
|
||||
---
|
||||
|
||||
Original license:
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2023, Balázs Orbán
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
21
.github/actions/triage/README.md
vendored
Normal file
21
.github/actions/triage/README.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Triage
|
||||
|
||||
Modified version of https://github.com/balazsorban44/nissuer
|
||||
|
||||
## Modifications
|
||||
|
||||
- Port to TypeScript
|
||||
- Remove issue locking
|
||||
- Remove reproduction blocklist
|
||||
- Uses `@vercel/ncc` for packaging
|
||||
|
||||
## Development
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Whenever a modification is made to the action, the action built to `dist` must be committed to the repository.
|
||||
|
||||
This is done by running:
|
||||
|
||||
```sh
|
||||
pnpm build
|
||||
```
|
||||
40
.github/actions/triage/action.yml
vendored
Normal file
40
.github/actions/triage/action.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Triage
|
||||
description: Initial triage for issues
|
||||
|
||||
inputs:
|
||||
reproduction-comment:
|
||||
description: 'Either a string or a path to a .md file inside the repository. Example: ".github/invalid-reproduction.md"'
|
||||
default: '.github/invalid-reproduction.md'
|
||||
reproduction-hosts:
|
||||
description: 'Comma-separated list of hostnames that are allowed for reproductions. Example: "github.com,codesandbox.io"'
|
||||
default: github.com
|
||||
reproduction-invalid-label:
|
||||
description: 'Label to apply to issues without a valid reproduction. Example: "invalid-reproduction"'
|
||||
default: 'invalid-reproduction'
|
||||
reproduction-issue-labels:
|
||||
description: 'Comma-separated list of issue labels. If configured, only verify reproduction URLs of issues with one of these labels present. Adding a comma at the end will handle non-labeled issues as invalid. Example: "bug,", will consider issues with the label "bug" or no label.'
|
||||
default: ''
|
||||
reproduction-link-section:
|
||||
description: 'A regular expression string with "(.*)" matching a valid URL in the issue body. The result is trimmed. Example: "### Link to reproduction(.*)### To reproduce"'
|
||||
default: '### Link to reproduction(.*)### To reproduce'
|
||||
tag-only:
|
||||
description: Log and tag only. Do not perform closing or commenting actions.
|
||||
default: false
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Checkout code
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: actions/checkout@v4
|
||||
- name: Run action
|
||||
run: node ${{ github.action_path }}/dist/index.js
|
||||
shell: sh
|
||||
# https://github.com/actions/runner/issues/665#issuecomment-676581170
|
||||
env:
|
||||
"INPUT_REPRODUCTION_COMMENT": ${{inputs.reproduction-comment}}
|
||||
"INPUT_REPRODUCTION_HOSTS": ${{inputs.reproduction-hosts}}
|
||||
"INPUT_REPRODUCTION_INVALID_LABEL": ${{inputs.reproduction-invalid-label}}
|
||||
"INPUT_REPRODUCTION_ISSUE_LABELS": ${{inputs.reproduction-issue-labels}}
|
||||
"INPUT_REPRODUCTION_LINK_SECTION": ${{inputs.reproduction-link-section}}
|
||||
"INPUT_TAG_ONLY": ${{inputs.tag-only}}
|
||||
34048
.github/actions/triage/dist/index.js
vendored
Normal file
34048
.github/actions/triage/dist/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
.github/actions/triage/jest.config.js
vendored
Normal file
7
.github/actions/triage/jest.config.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/dist/'],
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': ['@swc/jest'],
|
||||
},
|
||||
}
|
||||
34
.github/actions/triage/package.json
vendored
Normal file
34
.github/actions/triage/package.json
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "triage",
|
||||
"version": "0.0.0",
|
||||
"description": "GitHub Action to triage new issues",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"build": "pnpm build:typecheck && pnpm build:ncc",
|
||||
"build:ncc": "ncc build src/index.ts -t -o dist",
|
||||
"build:typecheck": "tsc",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.3.0",
|
||||
"@actions/github": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/webhooks-types": "^7.5.1",
|
||||
"@swc/jest": "^0.2.36",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^20.16.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"@vercel/ncc": "0.38.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^7.32.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.3.3",
|
||||
"ts-jest": "^26.5.6",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
||||
5419
.github/actions/triage/pnpm-lock.yaml
generated
vendored
Normal file
5419
.github/actions/triage/pnpm-lock.yaml
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
195
.github/actions/triage/src/index.ts
vendored
Normal file
195
.github/actions/triage/src/index.ts
vendored
Normal file
@@ -0,0 +1,195 @@
|
||||
import { debug, error, getBooleanInput, getInput, info, setFailed } from '@actions/core'
|
||||
|
||||
import { context, getOctokit } from '@actions/github'
|
||||
import { readFile, access } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
|
||||
// Ensure GITHUB_TOKEN and GITHUB_WORKSPACE are present
|
||||
if (!process.env.GITHUB_TOKEN) throw new TypeError('No GITHUB_TOKEN provided')
|
||||
if (!process.env.GITHUB_WORKSPACE) throw new TypeError('Not a GitHub workspace')
|
||||
|
||||
// Define the configuration object
|
||||
interface Config {
|
||||
invalidLink: {
|
||||
comment: string
|
||||
bugLabels: string[]
|
||||
hosts: string[]
|
||||
label: string
|
||||
linkSection: string
|
||||
}
|
||||
tagOnly: boolean
|
||||
token: string
|
||||
workspace: string
|
||||
}
|
||||
|
||||
const config: Config = {
|
||||
invalidLink: {
|
||||
comment: getInput('reproduction_comment') || '.github/invalid-reproduction.md',
|
||||
bugLabels: getInput('reproduction_issue_labels')
|
||||
.split(',')
|
||||
.map(l => l.trim()),
|
||||
hosts: (getInput('reproduction_hosts') || 'github.com').split(',').map(h => h.trim()),
|
||||
label: getInput('reproduction_invalid_label') || 'invalid-reproduction',
|
||||
linkSection:
|
||||
getInput('reproduction_link_section') || '### Link to reproduction(.*)### To reproduce',
|
||||
},
|
||||
tagOnly: getBooleanOrUndefined('tag_only') || false,
|
||||
token: process.env.GITHUB_TOKEN,
|
||||
workspace: process.env.GITHUB_WORKSPACE,
|
||||
}
|
||||
|
||||
// Attempt to parse JSON, return parsed object or error
|
||||
function tryParse(json: string): Record<string, unknown> {
|
||||
try {
|
||||
return JSON.parse(json)
|
||||
} catch (e) {
|
||||
setFailed(`Could not parse JSON: ${e instanceof Error ? e.message : e}`)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieves a boolean input or undefined based on environment variables
|
||||
function getBooleanOrUndefined(value: string): boolean | undefined {
|
||||
const variable = process.env[`INPUT_${value.toUpperCase()}`]
|
||||
return variable === undefined || variable === '' ? undefined : getBooleanInput(value)
|
||||
}
|
||||
|
||||
// Returns the appropriate label match type
|
||||
function getLabelMatch(value: string | undefined): 'name' | 'description' {
|
||||
return value === 'name' ? 'name' : 'description'
|
||||
}
|
||||
|
||||
// Function to check if an issue contains a valid reproduction link
|
||||
async function checkValidReproduction(): Promise<void> {
|
||||
const { issue, action } = context.payload as {
|
||||
issue: { number: number; body: string; labels: { name: string }[] } | undefined
|
||||
action: string
|
||||
}
|
||||
|
||||
if (action !== 'opened' || !issue?.body) return
|
||||
|
||||
const labels = issue.labels.map(l => l.name)
|
||||
|
||||
const issueMatchingLabel =
|
||||
labels.length &&
|
||||
config.invalidLink.bugLabels.length &&
|
||||
labels.some(l => config.invalidLink.bugLabels.includes(l))
|
||||
|
||||
if (!issueMatchingLabel) {
|
||||
info(
|
||||
`Issue #${issue.number} does not match required labels: ${config.invalidLink.bugLabels.join(', ')}`,
|
||||
)
|
||||
info(`Issue labels: ${labels.join(', ')}`)
|
||||
return
|
||||
}
|
||||
|
||||
info(`Issue #${issue.number} labels: ${labels.join(', ')}`)
|
||||
|
||||
const { rest: client } = getOctokit(config.token)
|
||||
const common = { ...context.repo, issue_number: issue.number }
|
||||
|
||||
const labelsToRemove = labels.filter(l => config.invalidLink.bugLabels.includes(l))
|
||||
|
||||
if (await isValidReproduction(issue.body)) {
|
||||
await Promise.all(
|
||||
labelsToRemove.map(label => client.issues.removeLabel({ ...common, name: label })),
|
||||
)
|
||||
|
||||
return info(`Issue #${issue.number} contains a valid reproduction 💚`)
|
||||
}
|
||||
|
||||
info(`Invalid reproduction, issue will be closed/labeled/commented...`)
|
||||
|
||||
// Adjust labels
|
||||
await Promise.all(
|
||||
labelsToRemove.map(label => client.issues.removeLabel({ ...common, name: label })),
|
||||
)
|
||||
info(`Issue #${issue.number} - validate label removed`)
|
||||
await client.issues.addLabels({ ...common, labels: [config.invalidLink.label] })
|
||||
info(`Issue #${issue.number} - labeled`)
|
||||
|
||||
// If tagOnly, do not close or comment
|
||||
if (config.tagOnly) {
|
||||
info('Tag-only enabled, no closing/commenting actions taken')
|
||||
return
|
||||
}
|
||||
|
||||
// Perform closing and commenting actions
|
||||
await client.issues.update({ ...common, state: 'closed' })
|
||||
info(`Issue #${issue.number} - closed`)
|
||||
|
||||
const comment = join(config.workspace, config.invalidLink.comment)
|
||||
await client.issues.createComment({ ...common, body: await getCommentBody(comment) })
|
||||
info(`Issue #${issue.number} - commented`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an issue contains a valid/accessible link to a reproduction.
|
||||
*
|
||||
* Returns `true` if the link is valid.
|
||||
* @param body - The body content of the issue
|
||||
*/
|
||||
async function isValidReproduction(body: string): Promise<boolean> {
|
||||
const linkSectionRe = new RegExp(config.invalidLink.linkSection, 'is')
|
||||
const link = body.match(linkSectionRe)?.[1]?.trim()
|
||||
|
||||
if (!link) {
|
||||
info('Missing link')
|
||||
info(`Link section regex: ${linkSectionRe}`)
|
||||
info(`Link section: ${body}`)
|
||||
return false
|
||||
}
|
||||
|
||||
info(`Checking validity of link: ${link}`)
|
||||
|
||||
if (!URL.canParse(link)) {
|
||||
info(`Invalid URL: ${link}`)
|
||||
return false
|
||||
}
|
||||
|
||||
const url = new URL(link)
|
||||
|
||||
if (!config.invalidLink.hosts.includes(url.hostname)) {
|
||||
info('Link did not match allowed reproduction hosts')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify that the link can be accessed
|
||||
const response = await fetch(link)
|
||||
const isOk = response.status < 400 || response.status >= 500
|
||||
|
||||
info(`Link status: ${response.status}`)
|
||||
if (!isOk) {
|
||||
info(`Link returned status ${response.status}`)
|
||||
}
|
||||
return isOk
|
||||
} catch (error) {
|
||||
info(`Error fetching link: ${(error as Error).message}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return either a file's content or a string
|
||||
* @param {string} pathOrComment
|
||||
*/
|
||||
async function getCommentBody(pathOrComment: string) {
|
||||
try {
|
||||
await access(pathOrComment)
|
||||
return await readFile(pathOrComment, 'utf8')
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') return pathOrComment
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const { token, workspace, ...safeConfig } = config
|
||||
info('Configuration:')
|
||||
info(JSON.stringify(safeConfig, null, 2))
|
||||
|
||||
await checkValidReproduction()
|
||||
}
|
||||
|
||||
run().catch(setFailed)
|
||||
15
.github/actions/triage/tsconfig.json
vendored
Normal file
15
.github/actions/triage/tsconfig.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["es2020.string"],
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": false, // Undo this
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
18
.github/comments/invalid-reproduction.md
vendored
Normal file
18
.github/comments/invalid-reproduction.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
We cannot recreate the issue with the provided information. **Please add a reproduction in order for us to be able to investigate.**
|
||||
|
||||
### Why was this issue marked with the `invalid-reproduction` label?
|
||||
|
||||
To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with `create-payload-app@beta -t blank` or a forked/branched version of this repository with tests added (more info in the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)).
|
||||
|
||||
To make sure the issue is resolved as quickly as possible, please make sure that the reproduction is as **minimal** as possible. This means that you should **remove unnecessary code, files, and dependencies** that do not contribute to the issue. Ensure your reproduction does not depend on secrets, 3rd party registries, private dependencies, or any other data that cannot be made public. Avoid a reproduction including a whole monorepo (unless relevant to the issue). The easier it is to reproduce the issue, the quicker we can help.
|
||||
|
||||
Please test your reproduction against the latest version of Payload to make sure your issue has not already been fixed.
|
||||
|
||||
### I added a link, why was it still marked?
|
||||
|
||||
Ensure the link is pointing to a codebase that is accessible (e.g. not a private repository). "[example.com](http://example.com/)", "n/a", "will add later", etc. are not acceptable links -- we need to see a public codebase. See the above section for accepted links.
|
||||
|
||||
### Useful Resources
|
||||
|
||||
- [Reproduction Guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)
|
||||
- [Contributing to Payload](https://www.youtube.com/watch?v=08Qa3ggR9rw)
|
||||
31
.github/dependabot.yml
vendored
31
.github/dependabot.yml
vendored
@@ -31,17 +31,44 @@ updates:
|
||||
labels:
|
||||
- dependencies
|
||||
groups:
|
||||
production:
|
||||
production-deps:
|
||||
dependency-type: production
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
patterns:
|
||||
- '*'
|
||||
dev:
|
||||
exclude-patterns:
|
||||
- 'drizzle*'
|
||||
dev-deps:
|
||||
dependency-type: development
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
patterns:
|
||||
- '*'
|
||||
exclude-patterns:
|
||||
- 'drizzle*'
|
||||
|
||||
# Only bump patch versions for 2.x
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
target-branch: main
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: sunday
|
||||
timezone: America/Detroit
|
||||
time: '06:00'
|
||||
commit-message:
|
||||
prefix: 'chore(deps)'
|
||||
labels:
|
||||
- dependencies
|
||||
groups:
|
||||
production-deps:
|
||||
dependency-type: production
|
||||
update-types:
|
||||
- patch
|
||||
patterns:
|
||||
- '*'
|
||||
exclude-patterns:
|
||||
- 'drizzle*'
|
||||
|
||||
4026
.github/pnpm-lock.yaml
generated
vendored
Normal file
4026
.github/pnpm-lock.yaml
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
.github/pnpm-workspace.yaml
vendored
Normal file
2
.github/pnpm-workspace.yaml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
- 'actions/*'
|
||||
26
.github/workflows/lock-issues.yml
vendored
Normal file
26
.github/workflows/lock-issues.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: lock-issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run nightly at 12am EST
|
||||
- cron: '0 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
lock_issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Lock issues
|
||||
uses: dessant/lock-threads@v5
|
||||
with:
|
||||
process-only: 'issues'
|
||||
issue-inactive-days: '1'
|
||||
exclude-any-issue-labels: 'status: awaiting-reply'
|
||||
log-output: true
|
||||
issue-comment: >
|
||||
This issue has been automatically locked.
|
||||
|
||||
Please open a new issue if this issue persists with any additional detail.
|
||||
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
with:
|
||||
filters: |
|
||||
needs_build:
|
||||
- '.github/workflows/**'
|
||||
- '.github/workflows/main.yml'
|
||||
- 'packages/**'
|
||||
- 'test/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
|
||||
32
.github/workflows/post-release.yml
vendored
Normal file
32
.github/workflows/post-release.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: post-release
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
post_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# Only needed if debugging on a branch other than default
|
||||
# ref: ${{ github.event.release.target_commitish || github.ref }}
|
||||
- run: echo "npm_version=$(npm pkg get version | tr -d '"')" >> "$GITHUB_ENV"
|
||||
- uses: ./.github/actions/release-commenter
|
||||
continue-on-error: true
|
||||
env:
|
||||
ACTIONS_STEP_DEBUG: true
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag-filter: 'v\d'
|
||||
|
||||
|
||||
# Change to blank to disable commenting
|
||||
# comment-template: ''
|
||||
|
||||
comment-template: |
|
||||
🚀 This is included in version {release_link}
|
||||
@@ -1,15 +1,20 @@
|
||||
name: label-author
|
||||
name: triage
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
types:
|
||||
- opened
|
||||
issues:
|
||||
types: [opened]
|
||||
types:
|
||||
- opened
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
debug-context:
|
||||
@@ -18,10 +23,10 @@ jobs:
|
||||
- name: View context attributes
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: console.log(context)
|
||||
script: console.log({ context })
|
||||
|
||||
label-created-by:
|
||||
name: Label pr/issue on opening
|
||||
name: label-on-open
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Tag with 'created-by'
|
||||
@@ -42,6 +47,8 @@ jobs:
|
||||
'PatrikKozak',
|
||||
'tylandavis',
|
||||
'paulpopus',
|
||||
'r1tsuu',
|
||||
'GermanJablo',
|
||||
];
|
||||
|
||||
const type = context.payload.pull_request ? 'pull_request' : 'issue';
|
||||
@@ -78,3 +85,18 @@ jobs:
|
||||
labels: [label],
|
||||
});
|
||||
console.log(`Added '${label}' label.`);
|
||||
|
||||
triage:
|
||||
name: initial-triage
|
||||
if: github.event_name == 'issues'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: ./.github/actions/triage
|
||||
with:
|
||||
reproduction-comment: '.github/comments/invalid-reproduction.md'
|
||||
reproduction-link-section: '### Link to the code that reproduces this issue(.*)### Reproduction Steps'
|
||||
reproduction-issue-labels: 'validate-reproduction'
|
||||
tag-only: 'true'
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,6 +4,9 @@ dist
|
||||
/.idea/*
|
||||
!/.idea/runConfigurations
|
||||
|
||||
# Custom actions
|
||||
!.github/actions/**/dist
|
||||
|
||||
test-results
|
||||
.devcontainer
|
||||
.localstack
|
||||
@@ -134,7 +137,6 @@ out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
|
||||
62
CHANGELOG.md
62
CHANGELOG.md
@@ -1,3 +1,65 @@
|
||||
## [2.30.4](https://github.com/payloadcms/payload/compare/v2.30.3...v2.30.4) (2024-11-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **plugin-seo:** add Turkish translation ([#8918](https://github.com/payloadcms/payload/issues/8918)) ([7c9ec9c](https://github.com/payloadcms/payload/commit/7c9ec9c4e0bafd19eb0ef94e9bb4aa07df3e097e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **db-postgres:** sort by localized fields ([#9016](https://github.com/payloadcms/payload/issues/9016)) ([9c25754](https://github.com/payloadcms/payload/commit/9c25754eed87e70b053231693102378150b8b80d))
|
||||
* edit many modal draft action button order and style ([#9046](https://github.com/payloadcms/payload/issues/9046)) ([a907480](https://github.com/payloadcms/payload/commit/a907480a94f07b4afa0e0723a3738c8bd457c51f)), closes [#9045](https://github.com/payloadcms/payload/issues/9045)
|
||||
* error saving after duplicating blocks with nested items ([#8814](https://github.com/payloadcms/payload/issues/8814)) ([eba777c](https://github.com/payloadcms/payload/commit/eba777c3a0b4c489d87d3c6154d835a105fef4a2))
|
||||
* list drawer relationship not displaying ([#9011](https://github.com/payloadcms/payload/issues/9011)) ([169da5c](https://github.com/payloadcms/payload/commit/169da5c3d83bfe3559227504929aa62721ded1d9))
|
||||
* querying relationships by `id` path with REST ([#9014](https://github.com/payloadcms/payload/issues/9014)) ([a0bd706](https://github.com/payloadcms/payload/commit/a0bd7060c45df3037ee074bd83400837c0adb83a))
|
||||
|
||||
## [2.30.3](https://github.com/payloadcms/payload/compare/v2.30.2...v2.30.3) (2024-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **db-postgres:** migrate:create errors with previous schemas ([#8786](https://github.com/payloadcms/payload/issues/8786)) ([e9c1222](https://github.com/payloadcms/payload/commit/e9c12221824a9a180991722135d22ff91d07ef11))
|
||||
* duplicate with select hasMany fields ([#8734](https://github.com/payloadcms/payload/issues/8734)) ([c8ed645](https://github.com/payloadcms/payload/commit/c8ed6454a733bea09ae620517c4894701999e119))
|
||||
|
||||
## [2.30.2](https://github.com/payloadcms/payload/compare/v2.30.1...v2.30.2) (2024-10-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* applies resize after cropping if `resizeOptions` are defined ([#8535](https://github.com/payloadcms/payload/issues/8535)) ([f2284f3](https://github.com/payloadcms/payload/commit/f2284f3d1b420c543d9eee9929f961db4cbef8a1))
|
||||
* calculates correct aspect ratio dimensions on sharp based files ([#8510](https://github.com/payloadcms/payload/issues/8510)) ([9d05b82](https://github.com/payloadcms/payload/commit/9d05b82dc67b967e55176b92f7b20d4486883201)), closes [#8317](https://github.com/payloadcms/payload/issues/8317)
|
||||
* **db-postgres:** build indexes for relationships ([#8446](https://github.com/payloadcms/payload/issues/8446)) ([d05e3b0](https://github.com/payloadcms/payload/commit/d05e3b0411c2e705bd7a26e93ba55f2b7532e41b))
|
||||
* **db-postgres:** port many various fixes from 3.0 ([#8468](https://github.com/payloadcms/payload/issues/8468)) ([1347b6c](https://github.com/payloadcms/payload/commit/1347b6cc36c33043755b4e31d7731ba19b8c985f))
|
||||
* **db-postgres:** select hasMany nested to array + tab/group ([#8739](https://github.com/payloadcms/payload/issues/8739)) ([0efc610](https://github.com/payloadcms/payload/commit/0efc6102104729a162912157a7e34293ec9764a5))
|
||||
* **richtext-lexical:** add target _blank for new-tab in linkFeature ([#8571](https://github.com/payloadcms/payload/issues/8571)) ([61e8ce1](https://github.com/payloadcms/payload/commit/61e8ce17439301cb16aee92887e97a69cac4e044)), closes [#8569](https://github.com/payloadcms/payload/issues/8569)
|
||||
|
||||
## [2.30.1](https://github.com/payloadcms/payload/compare/v2.30.0...v2.30.1) (2024-10-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **db-mongodb:** properly filters out `number` field values with the `exists` operator filter ([#8415](https://github.com/payloadcms/payload/issues/8415)) ([0586f23](https://github.com/payloadcms/payload/commit/0586f236bbf04163a0d9b226772849cb3d977864)), closes [#8181](https://github.com/payloadcms/payload/issues/8181)
|
||||
* sorting by id incorrectly orders by version.id ([#8450](https://github.com/payloadcms/payload/issues/8450)) ([1d38e6d](https://github.com/payloadcms/payload/commit/1d38e6d5d5b56a91aa8f59a461d40f28b1750f8c))
|
||||
|
||||
## [2.30.0](https://github.com/payloadcms/payload/compare/v2.29.0...v2.30.0) (2024-09-27)
|
||||
|
||||
* export toast from react toastify in payload ([#8438](https://github.com/payloadcms/payload/issues/8438)) ([17fc2d1](https://github.com/payloadcms/payload/commit/17fc2d13d06b6de01f839c27fd706bc0d6a185eb))
|
||||
|
||||
## [2.29.0](https://github.com/payloadcms/payload/compare/v2.28.0...v2.29.0) (2024-09-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add new option to disable JOI validation ([#8067](https://github.com/payloadcms/payload/issues/8067)) ([28a0650](https://github.com/payloadcms/payload/commit/28a065072fcad2dc768e44d79609eb5ab8a3fdfd))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **db-postgres:** localized items in arrays with versions ([#8334](https://github.com/payloadcms/payload/issues/8334)) ([c86526b](https://github.com/payloadcms/payload/commit/c86526b5c81ff484e66fbe6e7c727fdcc1f93c77))
|
||||
* **db-postgres:** querying on array within a relationship field ([#8153](https://github.com/payloadcms/payload/issues/8153)) ([170ea5b](https://github.com/payloadcms/payload/commit/170ea5badcff154514b8166ac92177d89a3fa5f8))
|
||||
* **db-postgres:** sanitize tab/group path for table name ([#8010](https://github.com/payloadcms/payload/issues/8010)) ([ba7a043](https://github.com/payloadcms/payload/commit/ba7a043a99f58fad39a62ac471eeb7309a39bba0))
|
||||
* treat empty strings as null / undefined for `exists` queries ([#8336](https://github.com/payloadcms/payload/issues/8336)) ([31d0b30](https://github.com/payloadcms/payload/commit/31d0b309fe5df1e37ed2a938959c1ef87834d987)), closes [#7714](https://github.com/payloadcms/payload/issues/7714)
|
||||
|
||||
## [2.28.0](https://github.com/payloadcms/payload/compare/v2.27.0...v2.28.0) (2024-09-04)
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,10 @@ If you want to add contributions to this repository, please follow the instructi
|
||||
|
||||
The [Examples Directory](./examples) is a great resource for learning how to setup Payload in a variety of different ways, but you can also find great examples in our blog and throughout our social media.
|
||||
|
||||
If you'd like to run the examples, you can either copy them to a folder outside this repo or run them directly by (1) navigating to the example's subfolder (`cd examples/your-example-folder`) and (2) using the `--ignore-workspace` flag to bypass workspace restrictions (e.g., `pnpm --ignore-workspace install` or `pnpm --ignore-workspace dev`).
|
||||
|
||||
You can see more examples at:
|
||||
|
||||
- [Examples Directory](./examples)
|
||||
- [Payload Blog](https://payloadcms.com/blog)
|
||||
- [Payload YouTube](https://www.youtube.com/@payloadcms)
|
||||
|
||||
@@ -30,7 +30,8 @@ It's often best practice to write your Collections in separate files and then im
|
||||
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`defaultSort`** | Pass a top-level field to sort by default in the collection List view. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`dbName`** | Custom table or collection name depending on the database adapter. Auto-generated from slug if not defined.
|
||||
| **`dbName`** | Custom table or collection name depending on the database adapter. Auto-generated from slug if not defined. |
|
||||
| **`db`** | Set custom database operations for this Collection. [More](/docs/database/overview#collection-operations) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ export default buildConfig({
|
||||
// collections go here
|
||||
],
|
||||
localization: {
|
||||
locales: ['en', 'es', 'de'],
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'es', 'de'], // required
|
||||
defaultLocale: 'en', // required
|
||||
fallback: true,
|
||||
},
|
||||
})
|
||||
@@ -54,7 +54,7 @@ export default buildConfig({
|
||||
rtl: true,
|
||||
},
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
defaultLocale: 'en', // required
|
||||
fallback: true,
|
||||
},
|
||||
})
|
||||
@@ -87,7 +87,7 @@ export default buildConfig({
|
||||
code: 'nb',
|
||||
},
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
defaultLocale: 'en', // required
|
||||
fallback: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -66,8 +66,8 @@ export default buildConfig({
|
||||
admin: {
|
||||
bundler: webpackBundler(), // or viteBundler()
|
||||
},
|
||||
db: mongooseAdapter({}) // or postgresAdapter({}),
|
||||
editor: lexicalEditor({}) // or slateEditor({})
|
||||
db: mongooseAdapter({}), // or postgresAdapter({})
|
||||
editor: lexicalEditor({}), // or slateEditor({})
|
||||
collections: [
|
||||
{
|
||||
slug: 'pages',
|
||||
|
||||
@@ -71,3 +71,104 @@ export default buildConfig({
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
## Collection Operations
|
||||
|
||||
To configure Collection database operations in your Payload application, your Collection config has methods that can override default database operations for that Collection.
|
||||
|
||||
The override methods receive arguments useful for augmenting operations such as Field data, the collection slug, and the req.
|
||||
|
||||
Here is an example:
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
export const Collection: CollectionConfig => {
|
||||
return {
|
||||
slug: 'collection-db-operations',
|
||||
db: {
|
||||
// Create a document in a custom db
|
||||
create: async ({ collection, data, req }) => {
|
||||
const doc = await fetch(`https://example.com/api/${collection}/create`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'x-app-user': `payload_${req.payload.user}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => response.json())
|
||||
|
||||
return doc
|
||||
},
|
||||
|
||||
// Delete a document in a custom db
|
||||
deleteOne: async ({ collection, data, req }) => {
|
||||
const docs = await fetch(`https://example.com/api/${collection}/delete/${data.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'x-app-user': `payload_${req.payload.user}`
|
||||
}
|
||||
}).then(response => response.json())
|
||||
|
||||
return docs
|
||||
},
|
||||
|
||||
// Delete many documents in a custom db
|
||||
deleteMany: async ({ collection, data, req }) => {
|
||||
const docs = await fetch(`https://example.com/api/${collection}/delete`, {
|
||||
method: 'DELETE'
|
||||
headers: {
|
||||
'x-app-user': `payload_${req.payload.user}`
|
||||
}
|
||||
body: JSON.stringify(data),
|
||||
}).then(response => response.json())
|
||||
|
||||
return docs
|
||||
},
|
||||
|
||||
// Find documents in a custom db
|
||||
find: async ({ collection, data, req, where, limit }) => {
|
||||
const docs = await fetch(`https://example.com/api/${collection}/find`, {
|
||||
headers: {
|
||||
'x-app-user': `payload_${req.payload.user}`
|
||||
}
|
||||
body: JSON.stringify({data, where, limit}),
|
||||
}).then(response => response.json())
|
||||
|
||||
return { docs }
|
||||
},
|
||||
|
||||
// Find one document in a custom db
|
||||
findOne: async ({ collection, data, req }) => {
|
||||
const doc = await fetch(`https://example.com/api/${collection}/find/${data.id}`, {
|
||||
headers: {
|
||||
'x-app-user': `payload_${req.payload.user}`
|
||||
}
|
||||
}).then(response => response.json())
|
||||
|
||||
return doc
|
||||
},
|
||||
|
||||
// Update one document in an custom db
|
||||
updateOne: async ({ collection, data, req }) => {
|
||||
const doc = await fetch(`https://example.com/api/${collection}/update/${data.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'x-app-user': `payload_${req.payload.user}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => response.json())
|
||||
|
||||
return { ...doc, updated: true }
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -144,6 +144,10 @@ export default addLastModified
|
||||
|
||||
### Available Plugins
|
||||
|
||||
You can discover existing plugins by browsing the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin).
|
||||
Payload supports both official plugins, maintained by the Payload team, and community plugins, developed by external contributors.
|
||||
|
||||
You can discover existing plugins by browsing the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin). These plugins offer a wide range of functionality. Some are maintained by the Payload team, while others are community-built. While we encourage users to explore them, please note that only official plugins are maintained and supported by the Payload team. For community plugins, support may vary as they are developed and maintained independently.
|
||||
|
||||
For maintainers building plugins for others to use, please add the topic to help others find it. If you would like one to be built by the core Payload team, [open a Feature Request](https://github.com/payloadcms/payload/discussions) in our GitHub Discussions board. We would be happy to review your code and maybe feature you and your plugin where appropriate.
|
||||
|
||||
For a list of official plugins, check the [Payload monorepo](https://github.com/payloadcms/payload/tree/main/packages).
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"copyfiles": "2.4.1",
|
||||
"cross-env": "7.0.3",
|
||||
"dotenv": "8.6.0",
|
||||
"drizzle-orm": "0.29.3",
|
||||
"drizzle-orm": "0.32.1",
|
||||
"express": "4.18.2",
|
||||
"form-data": "3.0.1",
|
||||
"fs-extra": "10.1.0",
|
||||
|
||||
@@ -33,15 +33,15 @@
|
||||
"md5": "2.3.0",
|
||||
"mini-css-extract-plugin": "1.6.2",
|
||||
"path-browserify": "1.0.1",
|
||||
"postcss": "8.4.31",
|
||||
"postcss": "8.4.47",
|
||||
"postcss-loader": "6.2.1",
|
||||
"postcss-preset-env": "9.0.0",
|
||||
"process": "0.11.10",
|
||||
"sass-loader": "12.6.0",
|
||||
"style-loader": "^2.0.0",
|
||||
"swc-loader": "^0.2.3",
|
||||
"swc-minify-webpack-plugin": "^2.1.0",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"swc-loader": "^0.2.6",
|
||||
"swc-minify-webpack-plugin": "^2.1.3",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "^5.78.0",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"bin": {
|
||||
|
||||
@@ -16,38 +16,38 @@ export function getValidTemplates(): ProjectTemplate[] {
|
||||
return [
|
||||
{
|
||||
name: 'blank',
|
||||
description: 'Blank Template',
|
||||
type: 'starter',
|
||||
url: 'https://github.com/payloadcms/payload/templates/blank',
|
||||
description: 'Blank Template',
|
||||
url: 'https://github.com/payloadcms/payload/templates/blank#v2.30.3',
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
description: 'Website Template',
|
||||
type: 'starter',
|
||||
url: 'https://github.com/payloadcms/payload/templates/website',
|
||||
description: 'Website Template',
|
||||
url: 'https://github.com/payloadcms/payload/templates/website#v2.30.3',
|
||||
},
|
||||
{
|
||||
name: 'ecommerce',
|
||||
description: 'E-commerce Template',
|
||||
type: 'starter',
|
||||
url: 'https://github.com/payloadcms/payload/templates/ecommerce',
|
||||
description: 'E-commerce Template',
|
||||
url: 'https://github.com/payloadcms/payload/templates/ecommerce#v2.30.3',
|
||||
},
|
||||
{
|
||||
name: 'plugin',
|
||||
description: 'Template for creating a Payload plugin',
|
||||
type: 'plugin',
|
||||
description: 'Template for creating a Payload plugin',
|
||||
url: 'https://github.com/payloadcms/payload-plugin-template',
|
||||
},
|
||||
{
|
||||
name: 'payload-demo',
|
||||
description: 'Payload demo site at https://demo.payloadcms.com',
|
||||
type: 'starter',
|
||||
description: 'Payload demo site at https://demo.payloadcms.com',
|
||||
url: 'https://github.com/payloadcms/public-demo',
|
||||
},
|
||||
{
|
||||
name: 'payload-website',
|
||||
description: 'Payload website CMS at https://payloadcms.com',
|
||||
type: 'starter',
|
||||
description: 'Payload website CMS at https://payloadcms.com',
|
||||
url: 'https://github.com/payloadcms/website-cms',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import slugify from '@sindresorhus/slugify'
|
||||
import arg from 'arg'
|
||||
import commandExists from 'command-exists'
|
||||
|
||||
import type { CliArgs, PackageManager } from './types'
|
||||
|
||||
@@ -68,7 +67,7 @@ export class Main {
|
||||
const template = await parseTemplate(this.args, validTemplates)
|
||||
|
||||
const projectDir = projectName === '.' ? process.cwd() : `./${slugify(projectName)}`
|
||||
const packageManager = await getPackageManager(this.args)
|
||||
const packageManager = getPackageManager(this.args)
|
||||
|
||||
if (template.type !== 'plugin') {
|
||||
const dbDetails = await selectDb(this.args, projectName)
|
||||
@@ -109,7 +108,7 @@ export class Main {
|
||||
}
|
||||
}
|
||||
|
||||
async function getPackageManager(args: CliArgs): Promise<PackageManager> {
|
||||
function getPackageManager(args: CliArgs): PackageManager {
|
||||
let packageManager: PackageManager = 'npm'
|
||||
|
||||
if (args['--use-npm']) {
|
||||
@@ -119,15 +118,22 @@ async function getPackageManager(args: CliArgs): Promise<PackageManager> {
|
||||
} else if (args['--use-pnpm']) {
|
||||
packageManager = 'pnpm'
|
||||
} else {
|
||||
try {
|
||||
if (await commandExists('yarn')) {
|
||||
packageManager = 'yarn'
|
||||
} else if (await commandExists('pnpm')) {
|
||||
packageManager = 'pnpm'
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
packageManager = 'npm'
|
||||
}
|
||||
packageManager = getEnvironmentPackageManager()
|
||||
}
|
||||
|
||||
return packageManager
|
||||
}
|
||||
|
||||
function getEnvironmentPackageManager(): PackageManager {
|
||||
const userAgent = process.env.npm_config_user_agent || ''
|
||||
|
||||
if (userAgent.startsWith('yarn')) {
|
||||
return 'yarn'
|
||||
}
|
||||
|
||||
if (userAgent.startsWith('pnpm')) {
|
||||
return 'pnpm'
|
||||
}
|
||||
|
||||
return 'npm'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "1.7.2",
|
||||
"version": "1.7.3",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -55,6 +55,30 @@ const handleNonHasManyValues = (formattedValue, operator, path) => {
|
||||
}
|
||||
}
|
||||
|
||||
const buildExistsQuery = (formattedValue, path) => {
|
||||
if (formattedValue) {
|
||||
return {
|
||||
rawQuery: {
|
||||
$and: [
|
||||
{ [path]: { $exists: true } },
|
||||
{ [path]: { $ne: null } },
|
||||
{ [path]: { $ne: '' } }, // Exclude null and empty string
|
||||
],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
rawQuery: {
|
||||
$or: [
|
||||
{ [path]: { $exists: false } },
|
||||
{ [path]: { $eq: null } },
|
||||
{ [path]: { $eq: '' } }, // Treat empty string as null / undefined
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const sanitizeQueryValue = ({
|
||||
field,
|
||||
hasCustomID,
|
||||
@@ -102,10 +126,18 @@ export const sanitizeQueryValue = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'number' && typeof formattedValue === 'string') {
|
||||
if (field.type === 'number') {
|
||||
if (typeof formattedValue === 'string' && operator !== 'exists') {
|
||||
formattedValue = Number(val)
|
||||
}
|
||||
|
||||
if (operator === 'exists') {
|
||||
formattedValue = val === 'true' ? true : val === 'false' ? false : Boolean(val)
|
||||
|
||||
return buildExistsQuery(formattedValue, path)
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'date' && typeof val === 'string' && operator !== 'exists') {
|
||||
formattedValue = new Date(val)
|
||||
if (Number.isNaN(Date.parse(formattedValue))) {
|
||||
@@ -189,6 +221,12 @@ export const sanitizeQueryValue = ({
|
||||
$regex: formattedValue.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'),
|
||||
}
|
||||
}
|
||||
|
||||
if (operator === 'exists') {
|
||||
formattedValue = formattedValue === 'true' || formattedValue === true
|
||||
|
||||
return buildExistsQuery(formattedValue, path)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "0.8.6",
|
||||
"version": "0.8.10",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,12 +26,12 @@
|
||||
"dependencies": {
|
||||
"@libsql/client": "^0.3.1",
|
||||
"console-table-printer": "2.11.2",
|
||||
"drizzle-kit": "0.20.14-1f2c838",
|
||||
"drizzle-orm": "0.29.3",
|
||||
"drizzle-kit": "0.23.2-df9e596",
|
||||
"drizzle-orm": "0.32.1",
|
||||
"pg": "8.11.3",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "9.0.0"
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { Payload } from 'payload'
|
||||
import type { Connect } from 'payload/database'
|
||||
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { numeric, timestamp, varchar } from 'drizzle-orm/pg-core'
|
||||
import { Pool } from 'pg'
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { PostgresAdapter } from './types'
|
||||
|
||||
import { pushDevSchema } from './utilities/pushDevSchema'
|
||||
|
||||
const connectWithReconnect = async function ({
|
||||
adapter,
|
||||
payload,
|
||||
@@ -48,6 +48,7 @@ const connectWithReconnect = async function ({
|
||||
|
||||
export const connect: Connect = async function connect(this: PostgresAdapter, payload) {
|
||||
this.schema = {
|
||||
pgSchema: this.pgSchema,
|
||||
...this.tables,
|
||||
...this.relations,
|
||||
...this.enums,
|
||||
@@ -77,76 +78,10 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
|
||||
|
||||
// Only push schema if not in production
|
||||
if (
|
||||
process.env.NODE_ENV === 'production' ||
|
||||
process.env.PAYLOAD_MIGRATING === 'true' ||
|
||||
this.push === false
|
||||
)
|
||||
return
|
||||
|
||||
const { pushSchema } = require('drizzle-kit/payload')
|
||||
|
||||
// This will prompt if clarifications are needed for Drizzle to push new schema
|
||||
const { apply, hasDataLoss, statementsToExecute, warnings } = await pushSchema(
|
||||
this.schema,
|
||||
this.drizzle,
|
||||
)
|
||||
|
||||
if (warnings.length) {
|
||||
let message = `Warnings detected during schema push: \n\n${warnings.join('\n')}\n\n`
|
||||
|
||||
if (hasDataLoss) {
|
||||
message += `DATA LOSS WARNING: Possible data loss detected if schema is pushed.\n\n`
|
||||
}
|
||||
|
||||
message += `Accept warnings and push schema to database?`
|
||||
|
||||
const { confirm: acceptWarnings } = await prompts(
|
||||
{
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
initial: false,
|
||||
message,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Exit if user does not accept warnings.
|
||||
// Q: Is this the right type of exit for this interaction?
|
||||
if (!acceptWarnings) {
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
await apply()
|
||||
|
||||
// Migration table def in order to use query using drizzle
|
||||
const migrationsSchema = this.pgSchema.table('payload_migrations', {
|
||||
name: varchar('name'),
|
||||
batch: numeric('batch'),
|
||||
created_at: timestamp('created_at'),
|
||||
updated_at: timestamp('updated_at'),
|
||||
})
|
||||
|
||||
const devPush = await this.drizzle
|
||||
.select()
|
||||
.from(migrationsSchema)
|
||||
.where(eq(migrationsSchema.batch, '-1'))
|
||||
|
||||
if (!devPush.length) {
|
||||
await this.drizzle.insert(migrationsSchema).values({
|
||||
name: 'dev',
|
||||
batch: '-1',
|
||||
})
|
||||
} else {
|
||||
await this.drizzle
|
||||
.update(migrationsSchema)
|
||||
.set({
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.where(eq(migrationsSchema.batch, '-1'))
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
process.env.PAYLOAD_MIGRATING !== 'true' &&
|
||||
this.push !== false
|
||||
) {
|
||||
await pushDevSchema(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Count } from 'payload/database'
|
||||
import type { SanitizedCollectionConfig } from 'payload/types'
|
||||
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { count as sqlCount } from 'drizzle-orm'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { ChainedMethods } from './find/chainMethods'
|
||||
@@ -51,8 +51,7 @@ export const count: Count = async function count(
|
||||
methods: selectCountMethods,
|
||||
query: db
|
||||
.select({
|
||||
count: sql<number>`count
|
||||
(DISTINCT ${this.tables[tableName].id})`,
|
||||
count: sqlCount(),
|
||||
})
|
||||
.from(table)
|
||||
.where(where),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-restricted-syntax, no-await-in-loop */
|
||||
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
|
||||
import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
|
||||
import type { CreateMigration } from 'payload/database'
|
||||
|
||||
import fs from 'fs'
|
||||
@@ -43,12 +43,13 @@ const getDefaultDrizzleSnapshot = (): DrizzleSnapshotJSON => ({
|
||||
schemas: {},
|
||||
tables: {},
|
||||
},
|
||||
dialect: 'pg',
|
||||
dialect: 'postgresql',
|
||||
enums: {},
|
||||
prevId: '00000000-0000-0000-0000-00000000000',
|
||||
schemas: {},
|
||||
sequences: {},
|
||||
tables: {},
|
||||
version: '5',
|
||||
version: '7',
|
||||
})
|
||||
|
||||
export const createMigration: CreateMigration = async function createMigration(
|
||||
@@ -60,7 +61,7 @@ export const createMigration: CreateMigration = async function createMigration(
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
|
||||
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/payload')
|
||||
const { generateDrizzleJson, generateMigration, upPgSnapshot } = require('drizzle-kit/api')
|
||||
|
||||
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
|
||||
const formattedDate = yyymmdd.replace(/\D/g, '')
|
||||
@@ -76,6 +77,12 @@ export const createMigration: CreateMigration = async function createMigration(
|
||||
|
||||
let drizzleJsonBefore = getDefaultDrizzleSnapshot()
|
||||
|
||||
if (this.schemaName) {
|
||||
drizzleJsonBefore.schemas = {
|
||||
[this.schemaName]: this.schemaName,
|
||||
}
|
||||
}
|
||||
|
||||
// Get latest migration snapshot
|
||||
const latestSnapshot = fs
|
||||
.readdirSync(dir)
|
||||
@@ -92,6 +99,11 @@ export const createMigration: CreateMigration = async function createMigration(
|
||||
}
|
||||
|
||||
const drizzleJsonAfter = generateDrizzleJson(this.schema)
|
||||
|
||||
if (drizzleJsonBefore.version < drizzleJsonAfter.version) {
|
||||
drizzleJsonBefore = upPgSnapshot(drizzleJsonBefore)
|
||||
}
|
||||
|
||||
const sqlStatementsUp = await generateMigration(drizzleJsonBefore, drizzleJsonAfter)
|
||||
const sqlStatementsDown = await generateMigration(drizzleJsonAfter, drizzleJsonBefore)
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ export const buildFindManyArgs = ({
|
||||
depth,
|
||||
fields,
|
||||
path: '',
|
||||
tablePath: '',
|
||||
topLevelArgs: result,
|
||||
topLevelTableName: tableName,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FindArgs } from 'payload/database'
|
||||
import type { Field, PayloadRequest, TypeWithID } from 'payload/types'
|
||||
|
||||
import { inArray, sql } from 'drizzle-orm'
|
||||
import { inArray, count as sqlCount } from 'drizzle-orm'
|
||||
|
||||
import type { PostgresAdapter } from '../types'
|
||||
import type { ChainedMethods } from './chainMethods'
|
||||
@@ -143,8 +143,7 @@ export const findMany = async function find({
|
||||
methods: selectCountMethods,
|
||||
query: db
|
||||
.select({
|
||||
count: sql<number>`count
|
||||
(DISTINCT ${adapter.tables[tableName].id})`,
|
||||
count: sqlCount(),
|
||||
})
|
||||
.from(table)
|
||||
.where(where),
|
||||
|
||||
@@ -15,6 +15,7 @@ type TraverseFieldArgs = {
|
||||
depth?: number
|
||||
fields: Field[]
|
||||
path: string
|
||||
tablePath: string
|
||||
topLevelArgs: Record<string, unknown>
|
||||
topLevelTableName: string
|
||||
}
|
||||
@@ -27,6 +28,7 @@ export const traverseFields = ({
|
||||
depth,
|
||||
fields,
|
||||
path,
|
||||
tablePath,
|
||||
topLevelArgs,
|
||||
topLevelTableName,
|
||||
}: TraverseFieldArgs) => {
|
||||
@@ -38,6 +40,7 @@ export const traverseFields = ({
|
||||
currentArgs,
|
||||
currentTableName,
|
||||
depth,
|
||||
tablePath,
|
||||
fields: field.fields,
|
||||
path,
|
||||
topLevelArgs,
|
||||
@@ -50,6 +53,7 @@ export const traverseFields = ({
|
||||
if (field.type === 'tabs') {
|
||||
field.tabs.forEach((tab) => {
|
||||
const tabPath = tabHasName(tab) ? `${path}${tab.name}_` : path
|
||||
const tabTablePath = tabHasName(tab) ? `${tablePath}${toSnakeCase(tab.name)}_` : tablePath
|
||||
|
||||
traverseFields({
|
||||
_locales,
|
||||
@@ -59,6 +63,7 @@ export const traverseFields = ({
|
||||
depth,
|
||||
fields: tab.fields,
|
||||
path: tabPath,
|
||||
tablePath: tabTablePath,
|
||||
topLevelArgs,
|
||||
topLevelTableName,
|
||||
})
|
||||
@@ -79,7 +84,7 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
const arrayTableName = adapter.tableNameMap.get(
|
||||
`${currentTableName}_${path}${toSnakeCase(field.name)}`,
|
||||
`${currentTableName}_${tablePath}${toSnakeCase(field.name)}`,
|
||||
)
|
||||
|
||||
const arrayTableNameWithLocales = `${arrayTableName}${adapter.localesSuffix}`
|
||||
@@ -95,6 +100,7 @@ export const traverseFields = ({
|
||||
depth,
|
||||
fields: field.fields,
|
||||
path: '',
|
||||
tablePath: '',
|
||||
topLevelArgs,
|
||||
topLevelTableName,
|
||||
})
|
||||
@@ -147,6 +153,7 @@ export const traverseFields = ({
|
||||
currentArgs: withBlock,
|
||||
currentTableName: tableName,
|
||||
depth,
|
||||
tablePath: '',
|
||||
fields: block.fields,
|
||||
path: '',
|
||||
topLevelArgs,
|
||||
@@ -163,6 +170,7 @@ export const traverseFields = ({
|
||||
adapter,
|
||||
currentArgs,
|
||||
currentTableName,
|
||||
tablePath: `${tablePath}${toSnakeCase(field.name)}_`,
|
||||
depth,
|
||||
fields: field.fields,
|
||||
path: `${path}${field.name}_`,
|
||||
|
||||
@@ -14,11 +14,11 @@ export const init: Init = async function init(this: PostgresAdapter) {
|
||||
if (this.schemaName) {
|
||||
this.pgSchema = pgSchema(this.schemaName)
|
||||
} else {
|
||||
this.pgSchema = { table: pgTable }
|
||||
this.pgSchema = { enum: pgEnum, table: pgTable }
|
||||
}
|
||||
|
||||
if (this.payload.config.localization) {
|
||||
this.enums.enum__locales = pgEnum(
|
||||
this.enums.enum__locales = this.pgSchema.enum(
|
||||
'_locales',
|
||||
this.payload.config.localization.locales.map(({ code }) => code) as [string, ...string[]],
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
|
||||
let latestBatch = 0
|
||||
let migrationsInDB = []
|
||||
|
||||
const hasMigrationTable = await migrationTableExists(this.drizzle)
|
||||
const hasMigrationTable = await migrationTableExists(this)
|
||||
|
||||
if (hasMigrationTable) {
|
||||
;({ docs: migrationsInDB } = await payload.find({
|
||||
@@ -80,7 +80,7 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
|
||||
}
|
||||
|
||||
async function runMigrationFile(payload: Payload, migration: Migration, batch: number) {
|
||||
const { generateDrizzleJson } = require('drizzle-kit/payload')
|
||||
const { generateDrizzleJson } = require('drizzle-kit/api')
|
||||
|
||||
const start = Date.now()
|
||||
const req = { payload } as PayloadRequest
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function migrateDown(this: PostgresAdapter): Promise<void> {
|
||||
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
|
||||
})
|
||||
|
||||
const tableExists = await migrationTableExists(this.drizzle)
|
||||
const tableExists = await migrationTableExists(this)
|
||||
if (tableExists) {
|
||||
await payload.delete({
|
||||
id: migration.id,
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function migrateRefresh(this: PostgresAdapter) {
|
||||
msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`,
|
||||
})
|
||||
|
||||
const tableExists = await migrationTableExists(this.drizzle)
|
||||
const tableExists = await migrationTableExists(this)
|
||||
if (tableExists) {
|
||||
await payload.delete({
|
||||
collection: 'payload-migrations',
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function migrateReset(this: PostgresAdapter): Promise<void> {
|
||||
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
|
||||
})
|
||||
|
||||
const tableExists = await migrationTableExists(this.drizzle)
|
||||
const tableExists = await migrationTableExists(this)
|
||||
if (tableExists) {
|
||||
await payload.delete({
|
||||
id: migration.id,
|
||||
@@ -68,7 +68,7 @@ export async function migrateReset(this: PostgresAdapter): Promise<void> {
|
||||
|
||||
// Delete dev migration
|
||||
|
||||
const tableExists = await migrationTableExists(this.drizzle)
|
||||
const tableExists = await migrationTableExists(this)
|
||||
if (tableExists) {
|
||||
try {
|
||||
await payload.delete({
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function migrateStatus(this: PostgresAdapter): Promise<void> {
|
||||
})
|
||||
|
||||
let existingMigrations = []
|
||||
const hasMigrationTable = await migrationTableExists(this.drizzle)
|
||||
const hasMigrationTable = await migrationTableExists(this)
|
||||
|
||||
if (hasMigrationTable) {
|
||||
;({ existingMigrations } = await getMigrations({ payload }))
|
||||
|
||||
@@ -185,17 +185,12 @@ export const getTableColumnFromPath = ({
|
||||
if (locale && field.localized && adapter.payload.config.localization) {
|
||||
newTableName = `${tableName}${adapter.localesSuffix}`
|
||||
|
||||
joins[tableName] = eq(
|
||||
adapter.tables[tableName].id,
|
||||
adapter.tables[newTableName]._parentID,
|
||||
)
|
||||
let condition = eq(adapter.tables[tableName].id, adapter.tables[newTableName]._parentID)
|
||||
if (locale !== 'all') {
|
||||
constraints.push({
|
||||
columnName: '_locale',
|
||||
table: adapter.tables[newTableName],
|
||||
value: locale,
|
||||
})
|
||||
condition = and(condition, eq(adapter.tables[newTableName]._locale, locale))
|
||||
}
|
||||
|
||||
joins[tableName] = condition
|
||||
}
|
||||
return getTableColumnFromPath({
|
||||
adapter,
|
||||
@@ -224,17 +219,15 @@ export const getTableColumnFromPath = ({
|
||||
)
|
||||
|
||||
if (locale && field.localized && adapter.payload.config.localization) {
|
||||
joins[newTableName] = and(
|
||||
const conditions = [
|
||||
eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
|
||||
eq(adapter.tables[newTableName]._locale, locale),
|
||||
)
|
||||
]
|
||||
if (locale !== 'all') {
|
||||
constraints.push({
|
||||
columnName: '_locale',
|
||||
table: adapter.tables[newTableName],
|
||||
value: locale,
|
||||
})
|
||||
conditions.push(eq(adapter.tables[newTableName]._locale, locale))
|
||||
}
|
||||
|
||||
joins[newTableName] = and(...conditions)
|
||||
} else {
|
||||
joins[newTableName] = eq(
|
||||
adapter.tables[tableName].id,
|
||||
@@ -268,17 +261,12 @@ export const getTableColumnFromPath = ({
|
||||
]
|
||||
|
||||
if (locale && field.localized && adapter.payload.config.localization) {
|
||||
joins[newTableName] = and(
|
||||
...joinConstraints,
|
||||
eq(adapter.tables[newTableName]._locale, locale),
|
||||
)
|
||||
const conditions = [...joinConstraints]
|
||||
if (locale !== 'all') {
|
||||
constraints.push({
|
||||
columnName: 'locale',
|
||||
table: adapter.tables[newTableName],
|
||||
value: locale,
|
||||
})
|
||||
conditions.push(eq(adapter.tables[newTableName]._locale, locale))
|
||||
}
|
||||
|
||||
joins[newTableName] = and(...conditions)
|
||||
} else {
|
||||
joins[newTableName] = and(...joinConstraints)
|
||||
}
|
||||
@@ -298,24 +286,18 @@ export const getTableColumnFromPath = ({
|
||||
`${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`,
|
||||
)
|
||||
|
||||
const arrayParentTable = aliasTable || adapter.tables[tableName]
|
||||
|
||||
constraintPath = `${constraintPath}${field.name}.%.`
|
||||
if (locale && field.localized && adapter.payload.config.localization) {
|
||||
joins[newTableName] = and(
|
||||
eq(adapter.tables[tableName].id, adapter.tables[newTableName]._parentID),
|
||||
eq(adapter.tables[newTableName]._locale, locale),
|
||||
)
|
||||
const conditions = [eq(arrayParentTable.id, adapter.tables[newTableName]._parentID)]
|
||||
if (locale !== 'all') {
|
||||
constraints.push({
|
||||
columnName: '_locale',
|
||||
table: adapter.tables[newTableName],
|
||||
value: locale,
|
||||
})
|
||||
conditions.push(eq(adapter.tables[newTableName]._locale, locale))
|
||||
}
|
||||
|
||||
joins[newTableName] = and(...conditions)
|
||||
} else {
|
||||
joins[newTableName] = eq(
|
||||
adapter.tables[tableName].id,
|
||||
adapter.tables[newTableName]._parentID,
|
||||
)
|
||||
joins[newTableName] = eq(arrayParentTable.id, adapter.tables[newTableName]._parentID)
|
||||
}
|
||||
return getTableColumnFromPath({
|
||||
adapter,
|
||||
@@ -400,17 +382,18 @@ export const getTableColumnFromPath = ({
|
||||
constraints = constraints.concat(blockConstraints)
|
||||
selectFields = { ...selectFields, ...blockSelectFields }
|
||||
if (field.localized && adapter.payload.config.localization) {
|
||||
joins[newTableName] = and(
|
||||
eq(adapter.tables[tableName].id, adapter.tables[newTableName]._parentID),
|
||||
eq(adapter.tables[newTableName]._locale, locale),
|
||||
)
|
||||
if (locale) {
|
||||
constraints.push({
|
||||
columnName: '_locale',
|
||||
table: adapter.tables[newTableName],
|
||||
value: locale,
|
||||
})
|
||||
const conditions = [
|
||||
eq(
|
||||
(aliasTable || adapter.tables[tableName]).id,
|
||||
adapter.tables[newTableName]._parentID,
|
||||
),
|
||||
]
|
||||
|
||||
if (locale !== 'all') {
|
||||
conditions.push(eq(adapter.tables[newTableName]._locale, locale))
|
||||
}
|
||||
|
||||
joins[newTableName] = and(...conditions)
|
||||
} else {
|
||||
joins[newTableName] = eq(
|
||||
adapter.tables[tableName].id,
|
||||
@@ -445,21 +428,20 @@ export const getTableColumnFromPath = ({
|
||||
|
||||
// Join in the relationships table
|
||||
if (locale && field.localized && adapter.payload.config.localization) {
|
||||
joinAliases.push({
|
||||
condition: and(
|
||||
const conditions = [
|
||||
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
|
||||
eq(aliasRelationshipTable.locale, locale),
|
||||
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
|
||||
),
|
||||
table: aliasRelationshipTable,
|
||||
})
|
||||
]
|
||||
|
||||
if (locale !== 'all') {
|
||||
constraints.push({
|
||||
columnName: 'locale',
|
||||
table: aliasRelationshipTable,
|
||||
value: locale,
|
||||
})
|
||||
conditions.push(eq(aliasRelationshipTable.locale, locale))
|
||||
}
|
||||
|
||||
joinAliases.push({
|
||||
condition: and(...conditions),
|
||||
table: aliasRelationshipTable,
|
||||
})
|
||||
} else {
|
||||
// Join in the relationships table
|
||||
joinAliases.push({
|
||||
@@ -555,17 +537,14 @@ export const getTableColumnFromPath = ({
|
||||
|
||||
const parentTable = aliasTable || adapter.tables[tableName]
|
||||
|
||||
joins[newTableName] = eq(parentTable.id, adapter.tables[newTableName]._parentID)
|
||||
|
||||
aliasTable = undefined
|
||||
let condition = eq(parentTable.id, adapter.tables[newTableName]._parentID)
|
||||
|
||||
if (locale !== 'all') {
|
||||
constraints.push({
|
||||
columnName: '_locale',
|
||||
table: adapter.tables[newTableName],
|
||||
value: locale,
|
||||
})
|
||||
condition = and(condition, eq(adapter.tables[newTableName]._locale, locale))
|
||||
}
|
||||
joins[newTableName] = condition
|
||||
|
||||
aliasTable = undefined
|
||||
}
|
||||
|
||||
const targetTable = aliasTable || adapter.tables[newTableName]
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { SQL } from 'drizzle-orm'
|
||||
import type { Field, Operator, Where } from 'payload/types'
|
||||
|
||||
import { and, ilike, isNotNull, isNull, ne, notInArray, or, sql } from 'drizzle-orm'
|
||||
import { PgUUID } from 'drizzle-orm/pg-core'
|
||||
import { QueryError } from 'payload/errors'
|
||||
import { validOperators } from 'payload/types'
|
||||
|
||||
@@ -174,6 +175,7 @@ export async function parseParams({
|
||||
const sanitizedQueryValue = sanitizeQueryValue({
|
||||
adapter,
|
||||
field,
|
||||
isUUID: table?.[columnName] instanceof PgUUID,
|
||||
operator,
|
||||
relationOrPath,
|
||||
val,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { APIError } from 'payload/errors'
|
||||
import { type Field, type TabAsField, fieldAffectsData } from 'payload/types'
|
||||
import { createArrayFromCommaDelineated } from 'payload/utilities'
|
||||
import { validate as uuidValidate } from 'uuid'
|
||||
|
||||
import type { PostgresAdapter } from '../types'
|
||||
|
||||
type SanitizeQueryValueArgs = {
|
||||
adapter: PostgresAdapter
|
||||
field: Field | TabAsField
|
||||
isUUID: boolean
|
||||
operator: string
|
||||
relationOrPath: string
|
||||
val: any
|
||||
@@ -15,6 +17,7 @@ type SanitizeQueryValueArgs = {
|
||||
export const sanitizeQueryValue = ({
|
||||
adapter,
|
||||
field,
|
||||
isUUID,
|
||||
operator: operatorArg,
|
||||
relationOrPath,
|
||||
val,
|
||||
@@ -64,6 +67,16 @@ export const sanitizeQueryValue = ({
|
||||
|
||||
if (field.type === 'number' && typeof formattedValue === 'string') {
|
||||
formattedValue = Number(val)
|
||||
|
||||
if (Number.isNaN(formattedValue)) {
|
||||
formattedValue = null
|
||||
}
|
||||
}
|
||||
|
||||
if (isUUID && typeof formattedValue === 'string') {
|
||||
if (!uuidValidate(val)) {
|
||||
formattedValue = null
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'date' && operator !== 'exists') {
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
PgTableWithColumns,
|
||||
UniqueConstraintBuilder,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import { Field, fieldAffectsData } from 'payload/types'
|
||||
import type { Field } from 'payload/types'
|
||||
|
||||
import { relations } from 'drizzle-orm'
|
||||
import {
|
||||
@@ -20,10 +20,12 @@ import {
|
||||
unique,
|
||||
varchar,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import { fieldAffectsData } from 'payload/types'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types'
|
||||
|
||||
import { createIndex } from './createIndex'
|
||||
import { createTableName } from './createTableName'
|
||||
import { parentIDColumnMap } from './parentIDColumnMap'
|
||||
import { setColumnID } from './setColumnID'
|
||||
@@ -51,9 +53,17 @@ type Args = {
|
||||
tableName: string
|
||||
timestamps?: boolean
|
||||
versions: boolean
|
||||
/**
|
||||
* Tracks whether or not this table is built
|
||||
* from the result of a localized array or block field at some point
|
||||
*/
|
||||
withinLocalizedArrayOrBlock?: boolean
|
||||
}
|
||||
|
||||
type Result = {
|
||||
hasLocalizedManyNumberField: boolean
|
||||
hasLocalizedManyTextField: boolean
|
||||
hasLocalizedRelationshipField: boolean
|
||||
hasManyNumberField: 'index' | boolean
|
||||
hasManyTextField: 'index' | boolean
|
||||
relationsToBuild: Map<string, string>
|
||||
@@ -76,6 +86,7 @@ export const buildTable = ({
|
||||
tableName,
|
||||
timestamps,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
}: Args): Result => {
|
||||
const rootTableName = incomingRootTableName || tableName
|
||||
const columns: Record<string, PgColumnBuilder> = baseColumns
|
||||
@@ -124,6 +135,7 @@ export const buildTable = ({
|
||||
rootTableIDColType: rootTableIDColType || idColType,
|
||||
rootTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (timestamps) {
|
||||
@@ -328,16 +340,28 @@ export const buildTable = ({
|
||||
if (relatedCollectionCustomIDType === 'number') colType = 'numeric'
|
||||
if (relatedCollectionCustomIDType === 'text') colType = 'varchar'
|
||||
|
||||
relationshipColumns[`${relationTo}ID`] = parentIDColumnMap[colType](
|
||||
`${formattedRelationTo}_id`,
|
||||
)
|
||||
const colName = `${relationTo}ID`
|
||||
|
||||
relationshipColumns[colName] = parentIDColumnMap[colType](`${formattedRelationTo}_id`)
|
||||
|
||||
relationExtraConfig[`${relationTo}IdFk`] = (cols) =>
|
||||
foreignKey({
|
||||
name: `${relationshipsTableName}_${toSnakeCase(relationTo)}_fk`,
|
||||
columns: [cols[`${relationTo}ID`]],
|
||||
columns: [cols[colName]],
|
||||
foreignColumns: [adapter.tables[formattedRelationTo].id],
|
||||
}).onDelete('cascade')
|
||||
|
||||
const indexName = [colName]
|
||||
|
||||
if (hasLocalizedRelationshipField) {
|
||||
indexName.push('locale')
|
||||
}
|
||||
|
||||
relationExtraConfig[`${relationTo}IdIdx`] = createIndex({
|
||||
name: indexName,
|
||||
columnName: `${formattedRelationTo}_id`,
|
||||
tableName: relationshipsTableName,
|
||||
})
|
||||
})
|
||||
|
||||
relationshipsTable = adapter.pgSchema.table(
|
||||
@@ -431,5 +455,12 @@ export const buildTable = ({
|
||||
|
||||
adapter.relations[`relations_${tableName}`] = tableRelations
|
||||
|
||||
return { hasManyNumberField, hasManyTextField, relationsToBuild }
|
||||
return {
|
||||
hasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField,
|
||||
hasManyNumberField,
|
||||
hasManyTextField,
|
||||
relationsToBuild,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
integer,
|
||||
jsonb,
|
||||
numeric,
|
||||
pgEnum,
|
||||
text,
|
||||
timestamp,
|
||||
varchar,
|
||||
@@ -57,6 +56,11 @@ type Args = {
|
||||
rootTableIDColType: string
|
||||
rootTableName: string
|
||||
versions: boolean
|
||||
/**
|
||||
* Tracks whether or not this table is built
|
||||
* from the result of a localized array or block field at some point
|
||||
*/
|
||||
withinLocalizedArrayOrBlock?: boolean
|
||||
}
|
||||
|
||||
type Result = {
|
||||
@@ -91,6 +95,7 @@ export const traverseFields = ({
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
}: Args): Result => {
|
||||
const throwValidationError = true
|
||||
let hasLocalizedField = false
|
||||
@@ -152,7 +157,11 @@ export const traverseFields = ({
|
||||
switch (field.type) {
|
||||
case 'text': {
|
||||
if (field.hasMany) {
|
||||
if (field.localized) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock
|
||||
|
||||
if (isLocalized) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
|
||||
@@ -181,7 +190,12 @@ export const traverseFields = ({
|
||||
|
||||
case 'number': {
|
||||
if (field.hasMany) {
|
||||
if (field.localized) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
|
||||
@@ -232,7 +246,7 @@ export const traverseFields = ({
|
||||
throwValidationError,
|
||||
})
|
||||
|
||||
adapter.enums[enumName] = pgEnum(
|
||||
adapter.enums[enumName] = adapter.pgSchema.enum(
|
||||
enumName,
|
||||
field.options.map((option) => {
|
||||
if (optionIsObject(option)) {
|
||||
@@ -268,7 +282,12 @@ export const traverseFields = ({
|
||||
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
|
||||
}
|
||||
|
||||
if (field.localized) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns.locale = adapter.enums.enum__locales('locale').notNull()
|
||||
baseExtraConfig.localeIdx = (cols) =>
|
||||
index(`${selectTableName}_locale_idx`).on(cols.locale)
|
||||
@@ -342,13 +361,21 @@ export const traverseFields = ({
|
||||
_parentIDIdx: (cols) => index(`${arrayTableName}_parent_id_idx`).on(cols._parentID),
|
||||
}
|
||||
|
||||
if (field.localized && adapter.payload.config.localization) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
||||
baseExtraConfig._localeIdx = (cols) =>
|
||||
index(`${arrayTableName}_locale_idx`).on(cols._locale)
|
||||
}
|
||||
|
||||
const {
|
||||
hasLocalizedManyNumberField: subHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: subHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: subHasLocalizedRelationshipField,
|
||||
hasManyNumberField: subHasManyNumberField,
|
||||
hasManyTextField: subHasManyTextField,
|
||||
relationsToBuild: subRelationsToBuild,
|
||||
@@ -365,8 +392,21 @@ export const traverseFields = ({
|
||||
rootTableName,
|
||||
tableName: arrayTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock: isLocalized,
|
||||
})
|
||||
|
||||
if (subHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = subHasLocalizedManyNumberField
|
||||
}
|
||||
|
||||
if (subHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = subHasLocalizedRelationshipField
|
||||
}
|
||||
|
||||
if (subHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = subHasLocalizedManyTextField
|
||||
}
|
||||
|
||||
if (subHasManyTextField) {
|
||||
if (!hasManyTextField || subHasManyTextField === 'index')
|
||||
hasManyTextField = subHasManyTextField
|
||||
@@ -433,13 +473,21 @@ export const traverseFields = ({
|
||||
_pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path),
|
||||
}
|
||||
|
||||
if (field.localized && adapter.payload.config.localization) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
||||
baseExtraConfig._localeIdx = (cols) =>
|
||||
index(`${blockTableName}_locale_idx`).on(cols._locale)
|
||||
}
|
||||
|
||||
const {
|
||||
hasLocalizedManyNumberField: subHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: subHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: subHasLocalizedRelationshipField,
|
||||
hasManyNumberField: subHasManyNumberField,
|
||||
hasManyTextField: subHasManyTextField,
|
||||
relationsToBuild: subRelationsToBuild,
|
||||
@@ -456,8 +504,21 @@ export const traverseFields = ({
|
||||
rootTableName,
|
||||
tableName: blockTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock: isLocalized,
|
||||
})
|
||||
|
||||
if (subHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = subHasLocalizedManyNumberField
|
||||
}
|
||||
|
||||
if (subHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = subHasLocalizedRelationshipField
|
||||
}
|
||||
|
||||
if (subHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = subHasLocalizedManyTextField
|
||||
}
|
||||
|
||||
if (subHasManyTextField) {
|
||||
if (!hasManyTextField || subHasManyTextField === 'index')
|
||||
hasManyTextField = subHasManyTextField
|
||||
@@ -541,6 +602,7 @@ export const traverseFields = ({
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (groupHasLocalizedField) hasLocalizedField = true
|
||||
@@ -584,6 +646,7 @@ export const traverseFields = ({
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized,
|
||||
})
|
||||
|
||||
if (groupHasLocalizedField) hasLocalizedField = true
|
||||
@@ -628,6 +691,7 @@ export const traverseFields = ({
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (tabHasLocalizedField) hasLocalizedField = true
|
||||
@@ -672,6 +736,7 @@ export const traverseFields = ({
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (rowHasLocalizedField) hasLocalizedField = true
|
||||
@@ -691,7 +756,10 @@ export const traverseFields = ({
|
||||
relationships.add(field.relationTo)
|
||||
}
|
||||
|
||||
if (field.localized && adapter.payload.config.localization) {
|
||||
if (
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock
|
||||
) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
break
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type { NumberField } from 'payload/types'
|
||||
|
||||
type Args = {
|
||||
@@ -6,10 +5,29 @@ type Args = {
|
||||
locale?: string
|
||||
numberRows: Record<string, unknown>[]
|
||||
ref: Record<string, unknown>
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
|
||||
export const transformHasManyNumber = ({ field, locale, numberRows, ref }: Args) => {
|
||||
const result = numberRows.map(({ number }) => number)
|
||||
export const transformHasManyNumber = ({
|
||||
field,
|
||||
locale,
|
||||
numberRows,
|
||||
ref,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
let result: unknown[]
|
||||
|
||||
if (withinArrayOrBlockLocale) {
|
||||
result = numberRows.reduce((acc, { locale, number }) => {
|
||||
if (locale === withinArrayOrBlockLocale) {
|
||||
acc.push(number)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
} else {
|
||||
result = numberRows.map(({ number }) => number)
|
||||
}
|
||||
|
||||
if (locale) {
|
||||
ref[field.name][locale] = result
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type { TextField } from 'payload/types'
|
||||
|
||||
type Args = {
|
||||
@@ -6,10 +5,29 @@ type Args = {
|
||||
locale?: string
|
||||
ref: Record<string, unknown>
|
||||
textRows: Record<string, unknown>[]
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
|
||||
export const transformHasManyText = ({ field, locale, ref, textRows }: Args) => {
|
||||
const result = textRows.map(({ text }) => text)
|
||||
export const transformHasManyText = ({
|
||||
field,
|
||||
locale,
|
||||
ref,
|
||||
textRows,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
let result: unknown[]
|
||||
|
||||
if (withinArrayOrBlockLocale) {
|
||||
result = textRows.reduce((acc, { locale, text }) => {
|
||||
if (locale === withinArrayOrBlockLocale) {
|
||||
acc.push(text)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
} else {
|
||||
result = textRows.map(({ text }) => text)
|
||||
}
|
||||
|
||||
if (locale) {
|
||||
ref[field.name][locale] = result
|
||||
|
||||
@@ -6,21 +6,31 @@ type Args = {
|
||||
locale?: string
|
||||
ref: Record<string, unknown>
|
||||
relations: Record<string, unknown>[]
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
|
||||
export const transformRelationship = ({ field, locale, ref, relations }: Args) => {
|
||||
export const transformRelationship = ({
|
||||
field,
|
||||
locale,
|
||||
ref,
|
||||
relations,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
let result: unknown
|
||||
|
||||
if (!('hasMany' in field) || field.hasMany === false) {
|
||||
const relation = relations[0]
|
||||
let relation = relations[0]
|
||||
|
||||
if (withinArrayOrBlockLocale) {
|
||||
relation = relations.find((rel) => rel.locale === withinArrayOrBlockLocale)
|
||||
}
|
||||
|
||||
if (relation) {
|
||||
// Handle hasOne Poly
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
const matchedRelation = Object.entries(relation).find(
|
||||
([key, val]) =>
|
||||
val !== null && !['id', 'locale', 'order', 'parent', 'path'].includes(key),
|
||||
)
|
||||
const matchedRelation = Object.entries(relation).find(([key, val]) => {
|
||||
return val !== null && !['id', 'locale', 'order', 'parent', 'path'].includes(key)
|
||||
})
|
||||
|
||||
if (matchedRelation) {
|
||||
const relationTo = matchedRelation[0].replace('ID', '')
|
||||
@@ -40,18 +50,26 @@ export const transformRelationship = ({ field, locale, ref, relations }: Args) =
|
||||
const transformedRelations = []
|
||||
|
||||
relations.forEach((relation) => {
|
||||
let matchedLocale = true
|
||||
|
||||
if (withinArrayOrBlockLocale) {
|
||||
matchedLocale = relation.locale === withinArrayOrBlockLocale
|
||||
}
|
||||
|
||||
// Handle hasMany
|
||||
if (!Array.isArray(field.relationTo)) {
|
||||
const relatedData = relation[`${field.relationTo}ID`]
|
||||
|
||||
if (relatedData) {
|
||||
if (relatedData && matchedLocale) {
|
||||
transformedRelations.push(relatedData)
|
||||
}
|
||||
} else {
|
||||
// Handle hasMany Poly
|
||||
const matchedRelation = Object.entries(relation).find(
|
||||
([key, val]) =>
|
||||
val !== null && !['id', 'locale', 'order', 'parent', 'path'].includes(key),
|
||||
val !== null &&
|
||||
!['id', 'locale', 'order', 'parent', 'path'].includes(key) &&
|
||||
matchedLocale,
|
||||
)
|
||||
|
||||
if (matchedRelation) {
|
||||
|
||||
@@ -55,6 +55,10 @@ type TraverseFieldsArgs = {
|
||||
* All hasMany text fields, as returned by Drizzle, keyed on an object by field path
|
||||
*/
|
||||
texts: Record<string, Record<string, unknown>[]>
|
||||
/**
|
||||
* Set to a locale if this group of fields is within a localized array or block.
|
||||
*/
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
|
||||
// Traverse fields recursively, transforming data
|
||||
@@ -71,6 +75,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
relationships,
|
||||
table,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}: TraverseFieldsArgs): T => {
|
||||
const sanitizedPath = path ? `${path}.` : path
|
||||
|
||||
@@ -88,6 +93,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
relationships,
|
||||
table,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -108,6 +114,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
relationships,
|
||||
table,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -145,6 +152,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
relationships,
|
||||
table: row,
|
||||
texts,
|
||||
withinArrayOrBlockLocale: locale,
|
||||
})
|
||||
|
||||
if ('_order' in rowResult) {
|
||||
@@ -157,7 +165,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
return arrayResult
|
||||
}, {})
|
||||
} else {
|
||||
result[field.name] = fieldData.map((row, i) => {
|
||||
result[field.name] = fieldData.reduce((acc, row, i) => {
|
||||
if (row._uuid) {
|
||||
row.id = row._uuid
|
||||
delete row._uuid
|
||||
@@ -167,7 +175,16 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
delete row._order
|
||||
}
|
||||
|
||||
return traverseFields<T>({
|
||||
if (
|
||||
!withinArrayOrBlockLocale ||
|
||||
(withinArrayOrBlockLocale && withinArrayOrBlockLocale === row._locale)
|
||||
) {
|
||||
if (row._locale) {
|
||||
delete row._locale
|
||||
}
|
||||
|
||||
acc.push(
|
||||
traverseFields<T>({
|
||||
blocks,
|
||||
config,
|
||||
dataRef: row,
|
||||
@@ -179,22 +196,27 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
relationships,
|
||||
table: row,
|
||||
texts,
|
||||
})
|
||||
})
|
||||
withinArrayOrBlockLocale,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
if (field.type === 'blocks') {
|
||||
const blockFieldPath = `${sanitizedPath}${field.name}`
|
||||
const blocksByPath = blocks[blockFieldPath]
|
||||
|
||||
if (Array.isArray(blocks[blockFieldPath])) {
|
||||
if (Array.isArray(blocksByPath)) {
|
||||
if (field.localized) {
|
||||
result[field.name] = {}
|
||||
|
||||
blocks[blockFieldPath].forEach((row) => {
|
||||
blocksByPath.forEach((row) => {
|
||||
if (row._uuid) {
|
||||
row.id = row._uuid
|
||||
delete row._uuid
|
||||
@@ -223,6 +245,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
relationships,
|
||||
table: row,
|
||||
texts,
|
||||
withinArrayOrBlockLocale: locale,
|
||||
})
|
||||
|
||||
delete blockResult._order
|
||||
@@ -233,7 +256,23 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
})
|
||||
})
|
||||
} else {
|
||||
result[field.name] = blocks[blockFieldPath].map((row, i) => {
|
||||
// Add locale-specific index to have a proper blockFieldPath for current locale
|
||||
// because blocks can be in the same array for different locales!
|
||||
if (withinArrayOrBlockLocale && config.localization) {
|
||||
for (const locale of config.localization.localeCodes) {
|
||||
let localeIndex = 0
|
||||
|
||||
for (let i = 0; i < blocksByPath.length; i++) {
|
||||
const row = blocksByPath[i]
|
||||
if (row._locale === locale) {
|
||||
row._index = localeIndex
|
||||
localeIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result[field.name] = blocksByPath.reduce((acc, row, i) => {
|
||||
delete row._order
|
||||
if (row._uuid) {
|
||||
row.id = row._uuid
|
||||
@@ -242,7 +281,20 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
const block = field.blocks.find(({ slug }) => slug === row.blockType)
|
||||
|
||||
if (block) {
|
||||
return traverseFields<T>({
|
||||
if (
|
||||
!withinArrayOrBlockLocale ||
|
||||
(withinArrayOrBlockLocale && withinArrayOrBlockLocale === row._locale)
|
||||
) {
|
||||
if (row._locale) {
|
||||
delete row._locale
|
||||
}
|
||||
if (typeof row._index === 'number') {
|
||||
i = row._index
|
||||
delete row._index
|
||||
}
|
||||
|
||||
acc.push(
|
||||
traverseFields<T>({
|
||||
blocks,
|
||||
config,
|
||||
dataRef: row,
|
||||
@@ -254,11 +306,18 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
relationships,
|
||||
table: row,
|
||||
texts,
|
||||
})
|
||||
withinArrayOrBlockLocale,
|
||||
}),
|
||||
)
|
||||
|
||||
return acc
|
||||
}
|
||||
} else {
|
||||
acc.push({})
|
||||
}
|
||||
|
||||
return {}
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +364,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
field,
|
||||
ref: result,
|
||||
relations: relationPathMatch,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -339,6 +399,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
field,
|
||||
ref: result,
|
||||
textRows: textPathMatch,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -373,6 +434,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
field,
|
||||
numberRows: numberPathMatch,
|
||||
ref: result,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -391,7 +453,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
return selectResult
|
||||
}, {})
|
||||
} else {
|
||||
result[field.name] = fieldData.map(({ value }) => value)
|
||||
let selectData = fieldData
|
||||
if (withinArrayOrBlockLocale) {
|
||||
selectData = selectData.filter(({ locale }) => locale === withinArrayOrBlockLocale)
|
||||
}
|
||||
result[field.name] = selectData.map(({ value }) => value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -404,8 +470,20 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
}[] = []
|
||||
|
||||
if (field.localized && Array.isArray(table._locales)) {
|
||||
if (!table._locales.length && config.localization) {
|
||||
config.localization.localeCodes.forEach((_locale) =>
|
||||
(table._locales as unknown[]).push({ _locale }),
|
||||
)
|
||||
}
|
||||
|
||||
table._locales.forEach((localeRow) => {
|
||||
valuesToTransform.push({ ref: localizedFieldData, table: localeRow })
|
||||
valuesToTransform.push({
|
||||
ref: localizedFieldData,
|
||||
table: {
|
||||
...table,
|
||||
...localeRow,
|
||||
},
|
||||
})
|
||||
})
|
||||
} else {
|
||||
valuesToTransform.push({ ref: result, table })
|
||||
@@ -419,35 +497,12 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
case 'tab':
|
||||
case 'group': {
|
||||
const groupFieldPrefix = `${fieldPrefix || ''}${field.name}_`
|
||||
|
||||
if (field.localized) {
|
||||
if (typeof locale === 'string' && !ref[locale]) {
|
||||
ref[locale] = {}
|
||||
delete table._locale
|
||||
}
|
||||
|
||||
Object.entries(ref).forEach(([groupLocale, groupLocaleData]) => {
|
||||
ref[groupLocale] = traverseFields<Record<string, unknown>>({
|
||||
blocks,
|
||||
config,
|
||||
dataRef: groupLocaleData as Record<string, unknown>,
|
||||
deletions,
|
||||
fieldPrefix: groupFieldPrefix,
|
||||
fields: field.fields,
|
||||
numbers,
|
||||
path: `${sanitizedPath}${field.name}`,
|
||||
relationships,
|
||||
table,
|
||||
texts,
|
||||
})
|
||||
})
|
||||
if ('_order' in ref) {
|
||||
delete ref._order
|
||||
}
|
||||
} else {
|
||||
const groupData = {}
|
||||
const locale = table._locale as string
|
||||
const refKey = field.localized && locale ? locale : field.name
|
||||
|
||||
ref[field.name] = traverseFields<Record<string, unknown>>({
|
||||
if (field.localized && locale) delete table._locale
|
||||
ref[refKey] = traverseFields<Record<string, unknown>>({
|
||||
blocks,
|
||||
config,
|
||||
dataRef: groupData as Record<string, unknown>,
|
||||
@@ -459,11 +514,12 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
relationships,
|
||||
table,
|
||||
texts,
|
||||
withinArrayOrBlockLocale: locale || withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
if ('_order' in ref) {
|
||||
delete ref._order
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ type Args = {
|
||||
[tableName: string]: Record<string, unknown>[]
|
||||
}
|
||||
texts: Record<string, unknown>[]
|
||||
/**
|
||||
* Set to a locale code if this set of fields is traversed within a
|
||||
* localized array or block field
|
||||
*/
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
|
||||
export const transformArray = ({
|
||||
@@ -43,6 +48,7 @@ export const transformArray = ({
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
const newRows: ArrayRowToInsert[] = []
|
||||
|
||||
@@ -78,6 +84,10 @@ export const transformArray = ({
|
||||
newRow.row._locale = locale
|
||||
}
|
||||
|
||||
if (withinArrayOrBlockLocale) {
|
||||
newRow.row._locale = withinArrayOrBlockLocale
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays: newRow.arrays,
|
||||
@@ -97,6 +107,7 @@ export const transformArray = ({
|
||||
row: newRow.row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
newRows.push(newRow)
|
||||
|
||||
@@ -26,6 +26,11 @@ type Args = {
|
||||
[tableName: string]: Record<string, unknown>[]
|
||||
}
|
||||
texts: Record<string, unknown>[]
|
||||
/**
|
||||
* Set to a locale code if this set of fields is traversed within a
|
||||
* localized array or block field
|
||||
*/
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
export const transformBlocks = ({
|
||||
adapter,
|
||||
@@ -41,6 +46,7 @@ export const transformBlocks = ({
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
data.forEach((blockRow, i) => {
|
||||
if (typeof blockRow.blockType !== 'string') return
|
||||
@@ -60,6 +66,7 @@ export const transformBlocks = ({
|
||||
}
|
||||
|
||||
if (field.localized && locale) newRow.row._locale = locale
|
||||
if (withinArrayOrBlockLocale) newRow.row._locale = withinArrayOrBlockLocale
|
||||
|
||||
const blockTableName = adapter.tableNameMap.get(`${baseTableName}_blocks_${blockType}`)
|
||||
|
||||
@@ -94,6 +101,7 @@ export const transformBlocks = ({
|
||||
row: newRow.row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
blocks[blockType].push(newRow)
|
||||
|
||||
@@ -58,6 +58,11 @@ type Args = {
|
||||
[tableName: string]: Record<string, unknown>[]
|
||||
}
|
||||
texts: Record<string, unknown>[]
|
||||
/**
|
||||
* Set to a locale code if this set of fields is traversed within a
|
||||
* localized array or block field
|
||||
*/
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
|
||||
export const traverseFields = ({
|
||||
@@ -81,6 +86,7 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
fields.forEach((field) => {
|
||||
let columnName = ''
|
||||
@@ -117,6 +123,7 @@ export const traverseFields = ({
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale: localeKey,
|
||||
})
|
||||
|
||||
arrays[arrayTableName] = arrays[arrayTableName].concat(newRows)
|
||||
@@ -138,6 +145,7 @@ export const traverseFields = ({
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
arrays[arrayTableName] = arrays[arrayTableName].concat(newRows)
|
||||
@@ -169,6 +177,7 @@ export const traverseFields = ({
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale: localeKey,
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -187,6 +196,7 @@ export const traverseFields = ({
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -197,6 +207,9 @@ export const traverseFields = ({
|
||||
if (typeof data[field.name] === 'object' && data[field.name] !== null) {
|
||||
if (field.localized) {
|
||||
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
|
||||
// preserve array ID if there is
|
||||
localeData._uuid = data.id || data._uuid
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays,
|
||||
@@ -218,9 +231,14 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale: localeKey,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// preserve array ID if there is
|
||||
const groupData = data[field.name] as Record<string, unknown>
|
||||
groupData._uuid = data.id || data._uuid
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays,
|
||||
@@ -228,7 +246,7 @@ export const traverseFields = ({
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix: `${columnName}_`,
|
||||
data: data[field.name] as Record<string, unknown>,
|
||||
data: groupData,
|
||||
existingLocales,
|
||||
fieldPrefix: `${fieldName}_`,
|
||||
fields: field.fields,
|
||||
@@ -241,6 +259,7 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -254,6 +273,9 @@ export const traverseFields = ({
|
||||
if (typeof data[tab.name] === 'object' && data[tab.name] !== null) {
|
||||
if (tab.localized) {
|
||||
Object.entries(data[tab.name]).forEach(([localeKey, localeData]) => {
|
||||
// preserve array ID if there is
|
||||
localeData._uuid = data.id || data._uuid
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays,
|
||||
@@ -275,9 +297,14 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale: localeKey,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const tabData = data[tab.name] as Record<string, unknown>
|
||||
// preserve array ID if there is
|
||||
tabData._uuid = data.id || data._uuid
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays,
|
||||
@@ -285,7 +312,7 @@ export const traverseFields = ({
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix: `${columnPrefix || ''}${toSnakeCase(tab.name)}_`,
|
||||
data: data[tab.name] as Record<string, unknown>,
|
||||
data: tabData,
|
||||
existingLocales,
|
||||
fieldPrefix: `${fieldPrefix || ''}${tab.name}_`,
|
||||
fields: tab.fields,
|
||||
@@ -298,6 +325,7 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -322,6 +350,7 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -339,6 +368,7 @@ export const traverseFields = ({
|
||||
existingLocales,
|
||||
fieldPrefix,
|
||||
fields: field.fields,
|
||||
forcedLocale,
|
||||
locales,
|
||||
numbers,
|
||||
parentTableName,
|
||||
@@ -348,6 +378,7 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -384,6 +415,7 @@ export const traverseFields = ({
|
||||
|
||||
transformRelationship({
|
||||
baseRow: {
|
||||
locale: withinArrayOrBlockLocale,
|
||||
path: relationshipPath,
|
||||
},
|
||||
data: fieldData,
|
||||
@@ -416,6 +448,7 @@ export const traverseFields = ({
|
||||
} else if (Array.isArray(fieldData)) {
|
||||
transformTexts({
|
||||
baseRow: {
|
||||
locale: withinArrayOrBlockLocale,
|
||||
path: textPath,
|
||||
},
|
||||
data: fieldData,
|
||||
@@ -447,6 +480,7 @@ export const traverseFields = ({
|
||||
} else if (Array.isArray(fieldData)) {
|
||||
transformNumbers({
|
||||
baseRow: {
|
||||
locale: withinArrayOrBlockLocale,
|
||||
path: numberPath,
|
||||
},
|
||||
data: fieldData,
|
||||
@@ -479,6 +513,7 @@ export const traverseFields = ({
|
||||
const newRows = transformSelects({
|
||||
id: data._uuid || data.id,
|
||||
data: data[field.name],
|
||||
locale: withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
selects[selectTableName] = selects[selectTableName].concat(newRows)
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
PgTableWithColumns,
|
||||
PgTransaction,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import type { pgEnum } from 'drizzle-orm/pg-core'
|
||||
import type { PgTableFn } from 'drizzle-orm/pg-core/table'
|
||||
import type { Payload } from 'payload'
|
||||
import type { BaseDatabaseAdapter } from 'payload/database'
|
||||
@@ -60,6 +61,13 @@ export type DrizzleTransaction = PgTransaction<
|
||||
ExtractTablesWithRelations<Record<string, unknown>>
|
||||
>
|
||||
|
||||
type Schema =
|
||||
| {
|
||||
enum: typeof pgEnum
|
||||
table: PgTableFn
|
||||
}
|
||||
| PgSchema
|
||||
|
||||
export type PostgresAdapter = BaseDatabaseAdapter & {
|
||||
drizzle: DrizzleDB
|
||||
enums: Record<string, GenericEnum>
|
||||
@@ -71,13 +79,13 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
|
||||
idType: Args['idType']
|
||||
localesSuffix?: string
|
||||
logger: DrizzleConfig['logger']
|
||||
pgSchema?: { table: PgTableFn } | PgSchema
|
||||
pgSchema?: Schema
|
||||
pool: Pool
|
||||
poolOptions: Args['pool']
|
||||
push: boolean
|
||||
relations: Record<string, GenericRelation>
|
||||
relationshipsSuffix?: string
|
||||
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
|
||||
schema: Record<string, unknown>
|
||||
schemaName?: Args['schemaName']
|
||||
sessions: {
|
||||
[id: string]: {
|
||||
@@ -116,7 +124,7 @@ declare module 'payload' {
|
||||
push: boolean
|
||||
relations: Record<string, GenericRelation>
|
||||
relationshipsSuffix?: string
|
||||
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
|
||||
schema: Record<string, unknown>
|
||||
sessions: {
|
||||
[id: string]: {
|
||||
db: DrizzleTransaction
|
||||
|
||||
@@ -54,7 +54,10 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P
|
||||
arrayRowLocaleData._locale = arrayRowLocale
|
||||
rowsByTable[tableName].locales.push(arrayRowLocaleData)
|
||||
if (!arrayRow.row.id) {
|
||||
arrayRowLocaleData._getParentID = (rows) => rows[i].id
|
||||
arrayRowLocaleData._getParentID = (rows: { _uuid: string; id: number }[]) => {
|
||||
const { id } = rows.find((each) => each._uuid === arrayRow.row._uuid)
|
||||
return id
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
|
||||
import type { DrizzleDB } from '../types'
|
||||
import type { PostgresAdapter } from '../types.js'
|
||||
|
||||
export const migrationTableExists = async (db: DrizzleDB): Promise<boolean> => {
|
||||
const queryRes = await db.execute(sql`SELECT to_regclass('public.payload_migrations');`)
|
||||
export const migrationTableExists = async (adapter: PostgresAdapter): Promise<boolean> => {
|
||||
let statement
|
||||
|
||||
// Returns table name 'payload_migrations' or null
|
||||
const exists = queryRes.rows?.[0]?.to_regclass === 'payload_migrations'
|
||||
return exists
|
||||
if (adapter.name === 'postgres') {
|
||||
const prependSchema = adapter.schemaName ? `"${adapter.schemaName}".` : ''
|
||||
statement = `SELECT to_regclass('${prependSchema}"payload_migrations"') AS exists;`
|
||||
}
|
||||
|
||||
const result = await adapter.drizzle.execute(sql.raw(statement))
|
||||
|
||||
const [row] = result.rows
|
||||
|
||||
return row && typeof row === 'object' && 'exists' in row && !!row.exists
|
||||
}
|
||||
|
||||
76
packages/db-postgres/src/utilities/pushDevSchema.ts
Normal file
76
packages/db-postgres/src/utilities/pushDevSchema.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { PostgresAdapter } from '../types.js'
|
||||
|
||||
import { requireDrizzleKit } from './requireDrizzleKit'
|
||||
|
||||
/**
|
||||
* Pushes the development schema to the database using Drizzle.
|
||||
*
|
||||
* @param {PostgresAdapter} adapter - The PostgresAdapter instance connected to the database.
|
||||
* @returns {Promise<void>} - A promise that resolves once the schema push is complete.
|
||||
*/
|
||||
export const pushDevSchema = async (adapter: PostgresAdapter) => {
|
||||
const { pushSchema } = requireDrizzleKit()
|
||||
|
||||
// This will prompt if clarifications are needed for Drizzle to push new schema
|
||||
const { apply, hasDataLoss, warnings } = await pushSchema(
|
||||
adapter.schema,
|
||||
adapter.drizzle,
|
||||
adapter.schemaName ? [adapter.schemaName] : undefined,
|
||||
)
|
||||
|
||||
if (warnings.length) {
|
||||
let message = `Warnings detected during schema push: \n\n${warnings.join('\n')}\n\n`
|
||||
|
||||
if (hasDataLoss) {
|
||||
message += `DATA LOSS WARNING: Possible data loss detected if schema is pushed.\n\n`
|
||||
}
|
||||
|
||||
message += `Accept warnings and push schema to database?`
|
||||
|
||||
const { confirm: acceptWarnings } = await prompts(
|
||||
{
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
initial: false,
|
||||
message,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Exit if user does not accept warnings.
|
||||
// Q: Is this the right type of exit for this interaction?
|
||||
if (!acceptWarnings) {
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
await apply()
|
||||
const migrationsTable = adapter.schemaName
|
||||
? `"${adapter.schemaName}"."payload_migrations"`
|
||||
: '"payload_migrations"'
|
||||
|
||||
const { drizzle } = adapter
|
||||
|
||||
const result = await drizzle.execute(
|
||||
sql.raw(`SELECT * FROM ${migrationsTable} WHERE batch = '-1'`),
|
||||
)
|
||||
|
||||
const devPush = result.rows
|
||||
|
||||
if (!devPush.length) {
|
||||
await drizzle.execute(
|
||||
sql.raw(`INSERT INTO ${migrationsTable} (name, batch) VALUES ('dev', '-1')`),
|
||||
)
|
||||
} else {
|
||||
await drizzle.execute(
|
||||
sql.raw(`UPDATE ${migrationsTable} SET updated_at = CURRENT_TIMESTAMP WHERE batch = '-1'`),
|
||||
)
|
||||
}
|
||||
}
|
||||
12
packages/db-postgres/src/utilities/requireDrizzleKit.ts
Normal file
12
packages/db-postgres/src/utilities/requireDrizzleKit.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { PostgresAdapter } from '../types'
|
||||
|
||||
type RequireDrizzleKit = () => {
|
||||
generateDrizzleJson: (args: { schema: Record<string, unknown> }) => unknown
|
||||
pushSchema: (
|
||||
schema: Record<string, unknown>,
|
||||
drizzle: PostgresAdapter['drizzle'],
|
||||
filterSchema?: string[],
|
||||
) => Promise<{ apply; hasDataLoss; warnings }>
|
||||
}
|
||||
|
||||
export const requireDrizzleKit: RequireDrizzleKit = () => require('drizzle-kit/api')
|
||||
@@ -32,7 +32,7 @@
|
||||
"eslint-plugin-perfectionist": "2.0.0",
|
||||
"eslint-plugin-playwright": "0.16.0",
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-regexp": "1.15.0"
|
||||
},
|
||||
"keywords": []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.28.0",
|
||||
"version": "2.30.4",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
@@ -55,13 +55,13 @@
|
||||
"@date-io/date-fns": "2.16.0",
|
||||
"@dnd-kit/core": "6.0.8",
|
||||
"@dnd-kit/sortable": "7.0.2",
|
||||
"@faceless-ui/modal": "2.0.1",
|
||||
"@faceless-ui/modal": "2.0.2",
|
||||
"@faceless-ui/scroll-info": "1.3.0",
|
||||
"@faceless-ui/window-info": "2.1.1",
|
||||
"@faceless-ui/window-info": "2.1.2",
|
||||
"@monaco-editor/react": "4.5.1",
|
||||
"@swc/core": "1.6.1",
|
||||
"@swc/register": "0.1.10",
|
||||
"body-parser": "1.20.2",
|
||||
"body-parser": "1.20.3",
|
||||
"body-scroll-lock": "4.0.0-beta.0",
|
||||
"bson-objectid": "2.0.4",
|
||||
"compression": "1.7.4",
|
||||
@@ -70,10 +70,10 @@
|
||||
"console-table-printer": "2.11.2",
|
||||
"dataloader": "2.2.2",
|
||||
"date-fns": "2.30.0",
|
||||
"deep-equal": "2.2.2",
|
||||
"deep-equal": "2.2.3",
|
||||
"deepmerge": "4.3.1",
|
||||
"dotenv": "8.6.0",
|
||||
"express": "4.18.2",
|
||||
"express": "4.21.0",
|
||||
"express-fileupload": "1.4.0",
|
||||
"express-rate-limit": "5.5.1",
|
||||
"file-type": "16.5.4",
|
||||
@@ -97,14 +97,14 @@
|
||||
"isomorphic-fetch": "3.0.0",
|
||||
"joi": "17.9.2",
|
||||
"json-schema-to-typescript": "14.0.5",
|
||||
"jsonwebtoken": "9.0.1",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"jwt-decode": "3.1.2",
|
||||
"md5": "2.3.0",
|
||||
"method-override": "3.0.0",
|
||||
"minimist": "1.2.8",
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.38.0",
|
||||
"nodemailer": "6.9.8",
|
||||
"nodemailer": "6.9.15",
|
||||
"object-to-formdata": "4.5.1",
|
||||
"passport": "0.6.0",
|
||||
"passport-anonymous": "1.0.1",
|
||||
@@ -132,11 +132,11 @@
|
||||
"react-toastify": "10.0.5",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"sass": "1.69.4",
|
||||
"scheduler": "0.23.0",
|
||||
"scheduler": "0.23.2",
|
||||
"scmp": "2.1.0",
|
||||
"sharp": "0.32.6",
|
||||
"swc-loader": "0.2.3",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"swc-loader": "0.2.6",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"ts-essentials": "7.0.3",
|
||||
"use-context-selector": "1.4.1",
|
||||
"uuid": "9.0.1"
|
||||
@@ -145,7 +145,7 @@
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@release-it/conventional-changelog": "7.0.0",
|
||||
"@types/asap": "2.0.0",
|
||||
"@types/body-parser": "1.19.2",
|
||||
"@types/body-parser": "1.19.5",
|
||||
"@types/body-scroll-lock": "^3.1.0",
|
||||
"@types/compression": "1.7.2",
|
||||
"@types/express": "4.17.17",
|
||||
@@ -158,14 +158,14 @@
|
||||
"@types/isomorphic-fetch": "0.0.36",
|
||||
"@types/joi": "14.3.4",
|
||||
"@types/json-schema": "7.0.12",
|
||||
"@types/jsonwebtoken": "8.5.9",
|
||||
"@types/jsonwebtoken": "9.0.7",
|
||||
"@types/method-override": "0.0.32",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/mini-css-extract-plugin": "^1.4.3",
|
||||
"@types/minimist": "1.2.2",
|
||||
"@types/mkdirp": "1.0.2",
|
||||
"@types/node-fetch": "2.6.4",
|
||||
"@types/nodemailer": "6.4.14",
|
||||
"@types/nodemailer": "6.4.16",
|
||||
"@types/passport": "1.0.12",
|
||||
"@types/passport-anonymous": "1.0.3",
|
||||
"@types/passport-jwt": "3.0.9",
|
||||
@@ -202,9 +202,9 @@
|
||||
"rimraf": "4.4.1",
|
||||
"sass-loader": "12.6.0",
|
||||
"serve-static": "1.15.0",
|
||||
"swc-loader": "^0.2.3",
|
||||
"swc-loader": "^0.2.6",
|
||||
"terser": "5.19.2",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"url-loader": "4.1.1",
|
||||
"vite": "^4.4.9",
|
||||
"webpack": "^5.78.0"
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import ObjectID from 'bson-objectid'
|
||||
|
||||
import { type BeforeDuplicate, type Field, tabHasName } from '../../../../exports/types'
|
||||
|
||||
/**
|
||||
* Creates new IDs for blocks / arrays items to avoid errors with relational databases.
|
||||
*/
|
||||
export const baseBeforeDuplicate = (args: Parameters<BeforeDuplicate>[0]) => {
|
||||
const {
|
||||
collection: { fields },
|
||||
data,
|
||||
} = args
|
||||
|
||||
traverseFields(fields, data)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
function traverseFields(fields: Field[], data: unknown) {
|
||||
if (typeof data === 'undefined' || data === null) return
|
||||
|
||||
fields.forEach((field) => {
|
||||
switch (field.type) {
|
||||
case 'array':
|
||||
if (Array.isArray(data?.[field.name])) {
|
||||
data[field.name].forEach((row) => {
|
||||
if (!row) return
|
||||
row.id = new ObjectID().toHexString()
|
||||
traverseFields(field.fields, row)
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'blocks': {
|
||||
if (Array.isArray(data?.[field.name])) {
|
||||
data[field.name].forEach((row) => {
|
||||
if (!row) return
|
||||
const configBlock = field.blocks.find((block) => block.slug === row.blockType)
|
||||
if (!configBlock) return
|
||||
row.id = new ObjectID().toHexString()
|
||||
traverseFields(configBlock.fields, row)
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'row':
|
||||
case 'collapsible':
|
||||
traverseFields(field.fields, data)
|
||||
break
|
||||
case 'tabs':
|
||||
field.tabs.forEach((tab) => {
|
||||
if (!tabHasName(tab)) {
|
||||
traverseFields(tab.fields, data)
|
||||
return
|
||||
}
|
||||
|
||||
if (data && data[tab.name]) {
|
||||
traverseFields(tab.fields, data[tab.name])
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'group':
|
||||
if (data && data[field.name]) {
|
||||
traverseFields(field.fields, data[field.name])
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import MinimalTemplate from '../../templates/Minimal'
|
||||
import { useConfig } from '../../utilities/Config'
|
||||
import Button from '../Button'
|
||||
import * as PopupList from '../Popup/PopupButtonList'
|
||||
import { baseBeforeDuplicate } from './baseBeforeDuplicate'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'duplicate'
|
||||
@@ -65,6 +66,8 @@ const Duplicate: React.FC<Props> = ({ id, slug, collection }) => {
|
||||
})
|
||||
let data = await response.json()
|
||||
|
||||
data = baseBeforeDuplicate({ collection, data, locale })
|
||||
|
||||
if (typeof collection.admin.hooks?.beforeDuplicate === 'function') {
|
||||
data = await collection.admin.hooks.beforeDuplicate({
|
||||
collection,
|
||||
@@ -73,6 +76,8 @@ const Duplicate: React.FC<Props> = ({ id, slug, collection }) => {
|
||||
})
|
||||
}
|
||||
|
||||
delete data['id']
|
||||
|
||||
if (!duplicateID) {
|
||||
if ('createdAt' in data) delete data.createdAt
|
||||
if ('updatedAt' in data) delete data.updatedAt
|
||||
|
||||
@@ -77,7 +77,7 @@ const SaveDraft: React.FC<{ action: string; disabled: boolean }> = ({ action, di
|
||||
}, [action, submit])
|
||||
|
||||
return (
|
||||
<FormSubmit className={`${baseClass}__draft`} disabled={disabled} onClick={save}>
|
||||
<FormSubmit buttonStyle="secondary" className={`${baseClass}__draft`} disabled={disabled} onClick={save}>
|
||||
{t('saveDraft')}
|
||||
</FormSubmit>
|
||||
)
|
||||
@@ -147,11 +147,11 @@ const EditMany: React.FC<Props> = (props) => {
|
||||
<div className={`${baseClass}__document-actions`}>
|
||||
{collection.versions ? (
|
||||
<React.Fragment>
|
||||
<Publish
|
||||
<SaveDraft
|
||||
action={`${serverURL}${api}/${slug}${getQueryParams()}&draft=true`}
|
||||
disabled={selected.length === 0}
|
||||
/>
|
||||
<SaveDraft
|
||||
<Publish
|
||||
action={`${serverURL}${api}/${slug}${getQueryParams()}&draft=true`}
|
||||
disabled={selected.length === 0}
|
||||
/>
|
||||
|
||||
@@ -228,6 +228,15 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
const duplicateRowState = deepCopyObject(rows[rowIndex])
|
||||
if (duplicateRowState.id) duplicateRowState.id = new ObjectID().toHexString()
|
||||
|
||||
for (const key of Object.keys(duplicateRowState).filter((key) => key.endsWith('.id'))) {
|
||||
const idState = duplicateRowState[key]
|
||||
|
||||
if (idState && typeof idState.value === 'string' && ObjectID.isValid(idState.value)) {
|
||||
duplicateRowState[key].value = new ObjectID().toHexString()
|
||||
duplicateRowState[key].initialValue = new ObjectID().toHexString()
|
||||
}
|
||||
}
|
||||
|
||||
// If there are subfields
|
||||
if (Object.keys(duplicateRowState).length > 0) {
|
||||
// Add new object containing subfield names to unflattenedRows array
|
||||
|
||||
@@ -48,6 +48,11 @@ const RelationshipCell: React.FC<CellComponentProps<RelationshipField | UploadFi
|
||||
relationTo: field.relationTo,
|
||||
value: cell,
|
||||
})
|
||||
} else if (typeof cell.id !== 'undefined' && typeof field.relationTo === 'string') {
|
||||
formattedValues.push({
|
||||
relationTo: field.relationTo,
|
||||
value: cell.id,
|
||||
})
|
||||
}
|
||||
})
|
||||
getRelationships(formattedValues)
|
||||
|
||||
@@ -39,6 +39,7 @@ export const defaults: Omit<Config, 'db' | 'editor'> = {
|
||||
schemaOutputFile: `${typeof process?.cwd === 'function' ? process.cwd() : ''}/schema.graphql`,
|
||||
},
|
||||
hooks: {},
|
||||
joiValidation: true,
|
||||
localization: false,
|
||||
maxDepth: 10,
|
||||
rateLimit: {
|
||||
|
||||
@@ -128,6 +128,7 @@ export default joi.object({
|
||||
}),
|
||||
i18n: joi.object(),
|
||||
indexSortableFields: joi.boolean(),
|
||||
joiValidation: joi.boolean(),
|
||||
local: joi.boolean(),
|
||||
localization: joi.alternatives().try(
|
||||
joi.object().keys({
|
||||
|
||||
@@ -149,6 +149,11 @@ export type InitOptions = {
|
||||
*/
|
||||
local?: boolean
|
||||
|
||||
/**
|
||||
* A previously instantiated logger instance. Must conform to the PayloadLogger interface which uses Pino
|
||||
* This allows you to bring your own logger instance and let payload use it
|
||||
*/
|
||||
logger?: PayloadLogger
|
||||
loggerDestination?: DestinationStream
|
||||
/**
|
||||
* Specify options for the built-in Pino logger that Payload uses for internal logging.
|
||||
@@ -156,11 +161,6 @@ export type InitOptions = {
|
||||
* See Pino Docs for options: https://getpino.io/#/docs/api?id=options
|
||||
*/
|
||||
loggerOptions?: LoggerOptions
|
||||
/**
|
||||
* A previously instantiated logger instance. Must conform to the PayloadLogger interface which uses Pino
|
||||
* This allows you to bring your own logger instance and let payload use it
|
||||
*/
|
||||
logger?: PayloadLogger
|
||||
|
||||
/**
|
||||
* A function that is called immediately following startup that receives the Payload instance as it's only argument.
|
||||
@@ -635,6 +635,12 @@ export type Config = {
|
||||
i18n?: i18nInitOptions
|
||||
/** Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. */
|
||||
indexSortableFields?: boolean
|
||||
/**
|
||||
* Disable JOI validation
|
||||
*
|
||||
* @default true // enabled by default
|
||||
*/
|
||||
joiValidation?: boolean
|
||||
/**
|
||||
* Translate your content to different languages/locales.
|
||||
*
|
||||
|
||||
@@ -83,6 +83,10 @@ const validateSchema = async (
|
||||
abortEarly: false,
|
||||
})
|
||||
|
||||
if (!config?.joiValidation) {
|
||||
return config
|
||||
}
|
||||
|
||||
const nestedErrors = [
|
||||
...(await validateCollections(config.collections)),
|
||||
...validateGlobals(config.globals),
|
||||
|
||||
@@ -74,6 +74,19 @@ export async function validateSearchParam({
|
||||
})
|
||||
}
|
||||
const promises = []
|
||||
|
||||
// Sanitize relation.otherRelation.id to relation.otherRelation
|
||||
if (paths.at(-1)?.path === 'id') {
|
||||
const previousField = paths.at(-2)?.field
|
||||
if (
|
||||
previousField &&
|
||||
(previousField.type === 'relationship' || previousField.type === 'upload') &&
|
||||
typeof previousField.relationTo === 'string'
|
||||
) {
|
||||
paths.pop()
|
||||
}
|
||||
}
|
||||
|
||||
promises.push(
|
||||
...paths.map(async ({ collectionSlug, field, invalid, path }, i) => {
|
||||
if (invalid) {
|
||||
@@ -110,6 +123,7 @@ export async function validateSearchParam({
|
||||
if (field.type === 'relationship' && Array.isArray(field.relationTo)) {
|
||||
fieldPath = fieldPath.replace('.value', '')
|
||||
}
|
||||
|
||||
const entityType: 'collections' | 'globals' = globalConfig ? 'globals' : 'collections'
|
||||
const entitySlug = collectionSlug || globalConfig.slug
|
||||
const segments = fieldPath.split('.')
|
||||
|
||||
@@ -23,11 +23,13 @@ export {
|
||||
useListDrawer,
|
||||
} from '../../admin/components/elements/ListDrawer'
|
||||
|
||||
export { useNav } from '../../admin/components/elements/Nav/context'
|
||||
|
||||
export { default as NavGroup } from '../../admin/components/elements/NavGroup'
|
||||
export {
|
||||
Description,
|
||||
DescriptionComponent,
|
||||
DescriptionFunction,
|
||||
} from '../../admin/components/forms/FieldDescription/types'
|
||||
|
||||
export { useNav } from '../../admin/components/elements/Nav/context'
|
||||
export { default as NavGroup } from '../../admin/components/elements/NavGroup'
|
||||
export { toast } from 'react-toastify'
|
||||
|
||||
@@ -228,6 +228,39 @@ export const generateFileData = async <T>({
|
||||
withMetadata,
|
||||
})
|
||||
|
||||
// Apply resize after cropping to ensure it conforms to resizeOptions
|
||||
if (resizeOptions) {
|
||||
const resizedAfterCrop = await sharp(croppedImage)
|
||||
.resize({
|
||||
fit: resizeOptions?.fit || 'cover',
|
||||
height: resizeOptions?.height,
|
||||
position: resizeOptions?.position || 'center',
|
||||
width: resizeOptions?.width,
|
||||
})
|
||||
.toBuffer({ resolveWithObject: true })
|
||||
|
||||
filesToSave.push({
|
||||
buffer: resizedAfterCrop.data,
|
||||
path: `${staticPath}/${fsSafeName}`,
|
||||
})
|
||||
|
||||
fileForResize = {
|
||||
...fileForResize,
|
||||
data: resizedAfterCrop.data,
|
||||
size: resizedAfterCrop.info.size,
|
||||
}
|
||||
|
||||
fileData.width = resizedAfterCrop.info.width
|
||||
fileData.height = resizedAfterCrop.info.height
|
||||
if (fileIsAnimatedType) {
|
||||
const metadata = await sharpFile.metadata()
|
||||
fileData.height = metadata.pages
|
||||
? resizedAfterCrop.info.height / metadata.pages
|
||||
: resizedAfterCrop.info.height
|
||||
}
|
||||
fileData.filesize = resizedAfterCrop.info.size
|
||||
} else {
|
||||
// If resizeOptions is not present, just save the cropped image
|
||||
filesToSave.push({
|
||||
buffer: croppedImage,
|
||||
path: `${staticPath}/${fsSafeName}`,
|
||||
@@ -238,6 +271,7 @@ export const generateFileData = async <T>({
|
||||
data: croppedImage,
|
||||
size: info.size,
|
||||
}
|
||||
|
||||
fileData.width = info.width
|
||||
fileData.height = info.height
|
||||
if (fileIsAnimatedType) {
|
||||
@@ -245,6 +279,7 @@ export const generateFileData = async <T>({
|
||||
fileData.height = metadata.pages ? info.height / metadata.pages : info.height
|
||||
}
|
||||
fileData.filesize = info.size
|
||||
}
|
||||
|
||||
if (file.tempFilePath) {
|
||||
await fs.promises.writeFile(file.tempFilePath, croppedImage) // write fileBuffer to the temp path
|
||||
|
||||
@@ -191,31 +191,6 @@ const getImageResizeAction = ({
|
||||
return hasFocalPoint ? 'resizeWithFocalPoint' : 'resize'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the image should be passed directly to sharp without payload adjusting properties.
|
||||
*
|
||||
* @param resizeConfig - object containing the requested dimensions and resize options
|
||||
* @param original - the original image size
|
||||
* @returns true if the image should passed directly to sharp
|
||||
*/
|
||||
const applyPayloadAdjustments = (
|
||||
{ fit, height, width, withoutEnlargement, withoutReduction }: ImageSize,
|
||||
original: ProbedImageSize,
|
||||
) => {
|
||||
if (fit === 'contain' || fit === 'inside') return false
|
||||
if (!isNumber(height) && !isNumber(width)) return false
|
||||
|
||||
const targetAspectRatio = width / height
|
||||
const originalAspectRatio = original.width / original.height
|
||||
if (originalAspectRatio === targetAspectRatio) return false
|
||||
|
||||
const skipEnlargement = withoutEnlargement && (original.height < height || original.width < width)
|
||||
const skipReduction = withoutReduction && (original.height > height || original.width > width)
|
||||
if (skipEnlargement || skipReduction) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the resize config. If the resize config has the `withoutReduction`
|
||||
* property set to true, the `fit` and `position` properties will be set to `contain`
|
||||
@@ -302,6 +277,18 @@ export default async function resizeAndTransformImageSizes({
|
||||
const sharpBase: Sharp | undefined = sharp(file.tempFilePath || file.data, sharpOptions).rotate() // pass rotate() to auto-rotate based on EXIF data. https://github.com/payloadcms/payload/pull/3081
|
||||
const originalImageMeta = await sharpBase.metadata()
|
||||
|
||||
let adjustedDimensions = { ...dimensions }
|
||||
|
||||
// Images with an exif orientation of 5, 6, 7, or 8 are auto-rotated by sharp
|
||||
// Need to adjust the dimensions to match the original image
|
||||
if ([5, 6, 7, 8].includes(originalImageMeta.orientation)) {
|
||||
adjustedDimensions = {
|
||||
...dimensions,
|
||||
height: dimensions.width,
|
||||
width: dimensions.height,
|
||||
}
|
||||
}
|
||||
|
||||
const resizeImageMeta = {
|
||||
height: extractHeightFromImage(originalImageMeta),
|
||||
width: originalImageMeta.width,
|
||||
@@ -324,7 +311,7 @@ export default async function resizeAndTransformImageSizes({
|
||||
if (resizeAction === 'resizeWithFocalPoint') {
|
||||
let { height: resizeHeight, width: resizeWidth } = imageResizeConfig
|
||||
|
||||
const originalAspectRatio = dimensions.width / dimensions.height
|
||||
const originalAspectRatio = adjustedDimensions.width / adjustedDimensions.height
|
||||
|
||||
// Calculate resizeWidth based on original aspect ratio if it's undefined
|
||||
if (resizeHeight && !resizeWidth) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Where } from '../../types'
|
||||
|
||||
export const appendVersionToQueryKey = (query: Where): Where => {
|
||||
export const appendVersionToQueryKey = (query: Where = {}): Where => {
|
||||
return Object.entries(query).reduce((res, [key, val]) => {
|
||||
if (['AND', 'OR', 'and', 'or'].includes(key) && Array.isArray(val)) {
|
||||
return {
|
||||
|
||||
@@ -13,5 +13,9 @@ export const getQueryDraftsSort = (sort: string): string => {
|
||||
orderBy = sort.substring(1)
|
||||
}
|
||||
|
||||
if (orderBy === 'id') {
|
||||
return `${direction}parent`
|
||||
}
|
||||
|
||||
return `${direction}version.${orderBy}`
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import type { Payload } from '../payload'
|
||||
import type { PayloadRequest } from '../types'
|
||||
import type { TypeWithVersion } from './types'
|
||||
|
||||
import { combineQueries } from '../database/combineQueries'
|
||||
import { docHasTimestamps } from '../types'
|
||||
import { appendVersionToQueryKey } from './drafts/appendVersionToQueryKey'
|
||||
|
||||
type Args = {
|
||||
config: SanitizedCollectionConfig
|
||||
@@ -32,7 +34,7 @@ export const getLatestCollectionVersion = async <T extends TypeWithID = any>({
|
||||
pagination: false,
|
||||
req,
|
||||
sort: '-updatedAt',
|
||||
where: { parent: { equals: id } },
|
||||
where: combineQueries(appendVersionToQueryKey(query.where), { parent: { equals: id } }),
|
||||
})
|
||||
;[latestVersion] = docs
|
||||
}
|
||||
|
||||
@@ -96,11 +96,19 @@ From there, create the adapter, passing in all of its required properties:
|
||||
```js
|
||||
import { azureBlobStorageAdapter } from '@payloadcms/plugin-cloud-storage/azure'
|
||||
|
||||
// if you need to obtain credentials you may do so by following the instructions here: https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad-app?tabs=javascript
|
||||
// or you can use the connection string directly.
|
||||
|
||||
const adapter = azureBlobStorageAdapter({
|
||||
connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
|
||||
containerName: process.env.AZURE_STORAGE_CONTAINER_NAME,
|
||||
allowContainerCreate: process.env.AZURE_STORAGE_ALLOW_CONTAINER_CREATE === 'true',
|
||||
baseURL: process.env.AZURE_STORAGE_ACCOUNT_BASEURL,
|
||||
/**
|
||||
* Optional: You may wish to obtain credentials that cannot be passed through in the connectionString connection option. In that case the connectionString will only be the URL to the storage account.
|
||||
* Can be one of AnonymousCredential | StorageSharedKeyCredential | TokenCredential
|
||||
**/
|
||||
credentials: new StorageSharedKeyCredential(process.env.AZURE_STORAGE_ACCOUNT_NAME, process.env.AZURE_STORAGE_ACCOUNT_KEY),
|
||||
})
|
||||
|
||||
// Now you can pass this adapter to the plugin
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"version": "1.1.3",
|
||||
"version": "1.2.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
@@ -53,6 +53,7 @@
|
||||
"@aws-sdk/client-s3": "^3.142.0",
|
||||
"@aws-sdk/lib-storage": "^3.267.0",
|
||||
"@azure/storage-blob": "^12.11.0",
|
||||
"@azure/core-http": "^3.0.0",
|
||||
"@google-cloud/storage": "^6.4.1",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/find-node-modules": "^2.1.2",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user