Compare commits

..

1 Commits

Author SHA1 Message Date
Elliot DeNolf
e9c7f9da92 chore(release): v3.0.0-alpha.61 [skip ci] 2024-04-09 14:22:12 -04:00
861 changed files with 7396 additions and 14044 deletions

38
.github/CODEOWNERS vendored
View File

@@ -1,33 +1,41 @@
# Order matters. The last matching pattern takes precedence.
### Package Exports ###
/**/exports/ @denolfe @jmikrut
### Core ###
/packages/payload/src/uploads/ @denolfe
/packages/payload/src/admin/ @jmikrut @jacobsfletch @JarrodMFlesch
### Adapters ###
/packages/richtext-*/ @AlessioGr
/packages/db-*/ @denolfe @jmikrut @DanRibbens
/packages/richtext-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
### Plugins ###
/packages/plugin-*/ @denolfe @jmikrut @DanRibbens
/packages/plugin-cloud*/ @denolfe
/packages/plugin-form-builder/ @jacobsfletch
/packages/plugin-live-preview*/ @jacobsfletch
/packages/plugin-nested-docs/ @jacobsfletch
/packages/plugin-redirects/ @jacobsfletch
/packages/plugin-search/ @jacobsfletch
/packages/plugin-sentry/ @JessChowdhury
/packages/plugin-seo/ @jacobsfletch
/packages/plugin-stripe/ @jacobsfletch
### Examples ###
/examples/ @jacobsfletch
/examples/testing/ @JarrodMFlesch
/examples/email/ @JessChowdhury
/examples/whitelabel/ @JessChowdhury
### Templates ###
/templates/ @jacobsfletch @denolfe
### Misc ###
/packages/create-payload-app/ @denolfe
/packages/eslint-*/ @denolfe
### Build Files ###
/**/package.json @denolfe
/tsconfig.json @denolfe
/**/tsconfig*.json @denolfe
/jest.config.js @denolfe
/**/jest.config.js @denolfe
/packages/eslint-config-payload/ @denolfe
/packages/payload-admin-bar/ @jacobsfletch
### Root ###
/package.json @denolfe
/scripts/ @denolfe
/.husky/ @denolfe
/.vscode/ @denolfe
/.github/ @denolfe
/.github/CODEOWNERS @denolfe

View File

@@ -4,16 +4,12 @@ on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches: ['main', 'beta']
branches: ['main', 'alpha']
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: 18.20.2
PNPM_VERSION: 8.15.7
jobs:
changes:
runs-on: ubuntu-latest
@@ -23,10 +19,6 @@ jobs:
needs_build: ${{ steps.filter.outputs.needs_build }}
templates: ${{ steps.filter.outputs.templates }}
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- uses: actions/checkout@v4
with:
fetch-depth: 25
@@ -57,19 +49,15 @@ jobs:
with:
fetch-depth: 25
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: ${{ env.PNPM_VERSION }}
version: 8
run_install: false
- name: Get pnpm store directory
@@ -100,21 +88,18 @@ jobs:
tests-unit:
runs-on: ubuntu-latest
needs: build
if: false # Disable until tests are updated for 3.0
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: ${{ env.PNPM_VERSION }}
version: 8
run_install: false
- name: Restore build
@@ -138,9 +123,9 @@ jobs:
database:
- mongodb
- postgres
- postgres-custom-schema
- postgres-uuid
- supabase
# - postgres-custom-schema
# - postgres-uuid
# - supabase
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@@ -151,19 +136,15 @@ jobs:
AWS_REGION: us-east-1
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: ${{ env.PNPM_VERSION }}
version: 8
run_install: false
- name: Restore build
@@ -237,6 +218,7 @@ jobs:
- access-control
- admin
- auth
- email
- field-error-states
- fields-relationship
- fields
@@ -246,7 +228,6 @@ jobs:
- fields__collections__Lexical
- live-preview
- localization
- plugin-cloud-storage
- plugin-form-builder
- plugin-nested-docs
- plugin-seo
@@ -254,19 +235,15 @@ jobs:
- uploads
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: ${{ env.PNPM_VERSION }}
version: 8
run_install: false
- name: Restore build
@@ -276,10 +253,6 @@ jobs:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Start LocalStack
run: pnpm docker:start
if: ${{ matrix.suite == 'plugin-cloud-storage' }}
- name: Install Playwright
run: pnpm exec playwright install --with-deps
@@ -291,7 +264,6 @@ jobs:
with:
name: test-results-${{ matrix.suite }}
path: test/test-results/
if-no-files-found: ignore
retention-days: 1
tests-type-generation:
@@ -300,19 +272,15 @@ jobs:
needs: build
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: ${{ env.PNPM_VERSION }}
version: 8
run_install: false
- name: Restore build
@@ -341,14 +309,11 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 25
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version: 18
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.10.0

View File

@@ -1,97 +0,0 @@
name: pr-title
on:
pull_request:
types:
- opened
- edited
- synchronize
permissions:
pull-requests: write
jobs:
main:
name: lint-pr-title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: |
build
chore
ci
docs
feat
fix
perf
refactor
revert
style
test
types
scopes: |
cpa
db-\*
db-mongodb
db-postgres
email-nodemailer
eslint
graphql
live-preview
live-preview-react
next
payload
plugin-cloud
plugin-cloud-storage
plugin-form-builder
plugin-nested-docs
plugin-redirects
plugin-search
plugin-sentry
plugin-seo
plugin-stripe
richtext-\*
richtext-lexical
richtext-slate
storage-\*
storage-azure
storage-gcs
storage-vercel-blob
storage-s3
translations
ui
templates
examples
# Disallow uppercase letters at the beginning of the subject
subjectPattern: ^(?![A-Z]).+$
- uses: marocchino/sticky-pull-request-comment@v2
# When the previous steps fails, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Pull Request titles must follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and have valid scopes.
${{ steps.lint_pr_title.outputs.error_message }}
```
feat(ui): add Button component
^ ^ ^
| | |__ Subject
| |_______ Scope
|____________ Type
```
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-title-lint-error
delete: true

View File

@@ -1 +1 @@
v18.20.2
v18.19.1

2
.nvmrc
View File

@@ -1 +1 @@
v18.20.2
v18.19.1

23
.vscode/launch.json vendored
View File

@@ -41,13 +41,6 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js auth",
"cwd": "${workspaceFolder}",
"name": "Run Dev Auth",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev plugin-cloud-storage",
"cwd": "${workspaceFolder}",
@@ -76,26 +69,36 @@
}
},
{
"command": "node --no-deprecation test/dev.js versions",
"command": "pnpm run dev versions",
"cwd": "${workspaceFolder}",
"name": "Run Dev Versions",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js localization",
"command": "pnpm run dev localization",
"cwd": "${workspaceFolder}",
"name": "Run Dev Localization",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js uploads",
"command": "pnpm run dev uploads",
"cwd": "${workspaceFolder}",
"name": "Run Dev Uploads",
"request": "launch",
"type": "node-terminal"
},
{
"command": "PAYLOAD_BUNDLER=vite pnpm run dev fields",
"cwd": "${workspaceFolder}",
"name": "Run Dev Fields (Vite)",
"request": "launch",
"type": "node-terminal",
"env": {
"NODE_ENV": "production"
}
},
{
"command": "pnpm run test:int live-preview",
"cwd": "${workspaceFolder}",

View File

@@ -1,10 +1,9 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
import { REST_DELETE, REST_GET, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const DELETE = REST_DELETE(config)
export const PATCH = REST_PATCH(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -4,7 +4,7 @@ label: JSON
order: 50
desc: The JSON field type will store any string in the Database. Learn how to use JSON fields, see examples and options.
keywords: json, jsonSchema, schema, validation, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
keywords: json, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
---
<Banner>
@@ -30,7 +30,6 @@ This field uses the `monaco-react` editor syntax highlighting.
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`jsonSchema`** | Provide a JSON schema that will be used for validation. [JSON schemas](https://json-schema.org/learn/getting-started-step-by-step)
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
@@ -53,7 +52,7 @@ In addition to the default [field admin config](/docs/fields/overview#admin-conf
### Example
`collections/ExampleCollection.ts`
`collections/ExampleCollection.ts
```ts
import { CollectionConfig } from 'payload/types'
@@ -69,67 +68,3 @@ export const ExampleCollection: CollectionConfig = {
],
}
```
### JSON Schema Validation
Payload JSON fields fully support the [JSON schema](https://json-schema.org/) standard. By providing a schema in your field config, the editor will be guided in the admin UI, getting typeahead for properties and their formats automatically. When the document is saved, the default validation will prevent saving any invalid data in the field according to the schema in your config.
If you only provide a URL to a schema, Payload will fetch the desired schema if it is publicly available. If not, it is recommended to add the schema directly to your config or import it from another file so that it can be implemented consistently in your project.
#### Local JSON Schema
`collections/ExampleCollection.ts`
```ts
import { CollectionConfig } from 'payload/types'
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
fields: [
{
name: 'customerJSON', // required
type: 'json', // required
jsonSchema: {
uri: 'a://b/foo.json', // required
fileMatch: ['a://b/foo.json'], // required
schema: {
type: 'object',
properties: {
foo: {
enum: ['bar', 'foobar'],
}
},
},
},
},
],
}
// {"foo": "bar"} or {"foo": "foobar"} - ok
// Attempting to create {"foo": "not-bar"} will throw an error
```
#### Remote JSON Schema
`collections/ExampleCollection.ts`
```ts
import { CollectionConfig } from 'payload/types'
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
fields: [
{
name: 'customerJSON', // required
type: 'json', // required
jsonSchema: {
uri: 'https://example.com/customer.schema.json', // required
fileMatch: ['https://example.com/customer.schema.json'], // required
},
},
],
}
// If 'https://example.com/customer.schema.json' has a JSON schema
// {"foo": "bar"} or {"foo": "foobar"} - ok
// Attempting to create {"foo": "not-bar"} will throw an error
```

View File

@@ -42,12 +42,11 @@ export const PublicUser: CollectionConfig = {
**Payload will automatically open up the following queries:**
| Query Name | Operation |
| ------------------ | ------------------- |
| **`PublicUser`** | `findByID` |
| **`PublicUsers`** | `find` |
| **`countPublicUsers`** | `count` |
| **`mePublicUser`** | `me` auth operation |
| Query Name | Operation |
| ------------------ | ------------------- |
| **`PublicUser`** | `findByID` |
| **`PublicUsers`** | `find` |
| **`mePublicUser`** | `me` auth operation |
**And the following mutations:**

View File

@@ -8,7 +8,7 @@ keywords: live preview, frontend, react, next.js, vue, nuxt.js, svelte, hook, us
While using Live Preview, the Admin panel emits a new `window.postMessage` event every time a change is made to the document. Your front-end application can listen for these events and re-render accordingly.
Wiring your front-end into Live Preview is easy. If your front-end application is built with React, Next.js, Vue or Nuxt.js, use the `useLivePreview` hook that Payload provides. In the future, all other major frameworks like Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information.
Wiring your front-end into Live Preview is easy. If your front-end application is built with React or Next.js, use the [`useLivePreview`](#react) React hook that Payload provides. In the future, all other major frameworks like Vue, Svelte, etc will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information.
By default, all hooks accept the following args:
@@ -36,10 +36,6 @@ And return the following values:
For example, `data?.relatedPosts?.[0]?.title`.
</Banner>
<Banner type="info">
It is important that the `depth` argument matches exactly with the depth of your initial page request. The depth property is used to populated relationships and uploads beyond their IDs. See [Depth](../getting-started/concepts#depth) for more information.
</Banner>
### React
If your front-end application is built with React or Next.js, you can use the `useLivePreview` hook that Payload provides.
@@ -75,40 +71,11 @@ export const PageClient: React.FC<{
}
```
### Vue
If your front-end application is built with Vue 3 or Nuxt 3, you can use the `useLivePreview` composable that Payload provides.
First, install the `@payloadcms/live-preview-vue` package:
```bash
npm install @payloadcms/live-preview-vue
```
Then, use the `useLivePreview` hook in your Vue component:
```vue
<script setup lang="ts">
import type { PageData } from '~/types';
import { defineProps } from 'vue';
import { useLivePreview } from '@payloadcms/live-preview-vue';
// Fetch the initial data on the parent component or using async state
const props = defineProps<{ initialData: PageData }>();
// The hook will take over from here and keep the preview in sync with the changes you make.
// The `data` property will contain the live data of the document only when viewed from the Preview view of the Admin UI.
const { data } = useLivePreview<PageData>({
initialData: props.initialData,
serverURL: "<PAYLOAD_SERVER_URL>",
depth: 2,
});
</script>
<template>
<h1>{{ data.title }}</h1>
</template>
```
<Banner type="info">
If is important that the `depth` argument matches exactly with the depth of your initial page
request. The depth property is used to populated relationships and uploads beyond their IDs. See
[Depth](../getting-started/concepts#depth) for more information.
</Banner>
## Building your own hook

View File

@@ -164,22 +164,6 @@ const result = await payload.findByID({
})
```
#### Count
```js
// Result will be an object with:
// {
// totalDocs: 10, // count of the documents satisfies query
// }
const result = await payload.count({
collection: 'posts', // required
locale: 'en',
where: {}, // pass a `where` query here
user: dummyUser,
overrideAccess: false,
})
```
#### Update by ID
```js

View File

@@ -90,19 +90,6 @@ Note: Collection slugs must be formatted in kebab-case
},
},
},
{
operation: "Count",
method: "GET",
path: "/api/{collection-slug}/count",
description: "Count the documents",
example: {
slug: "count",
req: true,
res: {
totalDocs: 10
},
},
},
{
operation: "Create",
method: "POST",

View File

@@ -138,7 +138,7 @@ import { CallToAction } from '../blocks/CallToAction'
Here's an overview of all the included features:
| Feature Name | Included by default | Description |
|--------------------------------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ------------------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`BoldTextFeature`** | Yes | Handles the bold text format |
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
@@ -157,8 +157,7 @@ Here's an overview of all the included features:
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
| **`BlockQuoteFeature`** | Yes | Allows you to create block-level quotes |
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an <hr> element |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
## Creating your own, custom Feature
@@ -235,19 +234,6 @@ This method employs `convertLexicalToHTML` from `@payloadcms/richtext-lexical`,
Because every `Feature` is able to provide html converters, and because the `htmlFeature` can modify those or provide their own, we need to consolidate them with the default html Converters using the `consolidateHTMLConverters` function.
#### CSS
Payload's lexical HTML converter does not generate CSS for you, but it does add classes to the generated HTML. You can use these classes to style the HTML in your frontend.
Here is some "base" CSS you can use to ensure that nested lists render correctly:
```css
/* Base CSS for Lexical HTML */
.nestedListItem, .list-check {
list-style-type: none;
}
```
#### Creating your own HTML Converter
HTML Converters are typed as `HTMLConverter`, which contains the node type it should handle, and a function that accepts the serialized node from the lexical editor, and outputs the HTML string. Here's the HTML Converter of the Upload node as an example:

View File

@@ -40,22 +40,21 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
### Collection Upload Options
| Option | Description |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`staticURL`** \* | The URL path to use to access your uploads. Relative path like `/media` will be served by payload. Full path like `https://example.com/media` needs to be served by another web server. |
| **`staticDir`** \* | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. |
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`externalFileHeaderFilter`** | Accepts existing headers and can filter/modify them. |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
| Option | Description |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`staticURL`** \* | The URL path to use to access your uploads. Relative path like `/media` will be served by payload. Full path like `https://example.com/media` needs to be served by another web server. |
| **`staticDir`** \* | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. |
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
_An asterisk denotes that a property above is required._

1
emptyModule.js Normal file
View File

@@ -0,0 +1 @@
export default {}

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.18",
"version": "3.0.0-alpha.61",
"private": true,
"type": "module",
"workspaces:": [
@@ -18,7 +18,6 @@
"build:create-payload-app": "turbo build --filter create-payload-app",
"build:db-mongodb": "turbo build --filter db-mongodb",
"build:db-postgres": "turbo build --filter db-postgres",
"build:email-nodemailer": "turbo build --filter email-nodemailer",
"build:eslint-config-payload": "turbo build --filter eslint-config-payload",
"build:graphql": "turbo build --filter graphql",
"build:live-preview": "turbo build --filter live-preview",
@@ -36,7 +35,7 @@
"build:plugin-stripe": "turbo build --filter plugin-stripe",
"build:richtext-lexical": "turbo build --filter richtext-lexical",
"build:richtext-slate": "turbo build --filter richtext-slate",
"build:tests": "pnpm --filter payload-test-suite run typecheck",
"build:tests": "pnpm --filter test run typecheck",
"build:translations": "turbo build --filter translations",
"build:ui": "turbo build --filter ui",
"clean": "turbo clean",
@@ -128,7 +127,7 @@
"lint-staged": "^14.0.1",
"minimist": "1.2.8",
"mongodb-memory-server": "^9.0",
"next": "^14.3.0-canary.7",
"next": "14.2.0-canary.22",
"node-mocks-http": "^1.14.1",
"nodemon": "3.0.3",
"open": "^10.1.0",
@@ -164,8 +163,8 @@
"react": "18.2.0"
},
"engines": {
"node": ">=18.20.2",
"pnpm": "^8.15.7"
"node": ">=18.17.0",
"pnpm": ">=8"
},
"lint-staged": {
"*.{md,mdx,yml,json}": "prettier --write",
@@ -176,7 +175,6 @@
},
"dependencies": {
"@sentry/react": "^7.77.0",
"ajv": "^8.12.0",
"passport-strategy": "1.0.0"
},
"pnpm": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.18",
"version": "3.0.0-alpha.61",
"license": "MIT",
"type": "module",
"homepage": "https://payloadcms.com",
@@ -35,7 +35,7 @@
"comment-json": "^4.2.3",
"degit": "^2.8.4",
"detect-package-manager": "^3.0.1",
"esprima-next": "^6.0.3",
"esprima": "^4.0.1",
"execa": "^5.0.0",
"figures": "^6.1.0",
"fs-extra": "^9.0.1",

View File

@@ -1,9 +1,5 @@
import fse from 'fs-extra'
import globby from 'globby'
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import type { DbDetails } from '../types.js'
@@ -19,34 +15,6 @@ export async function configurePayloadConfig(args: {
return
}
// Update package.json
const packageJsonPath =
'projectDir' in args.projectDirOrConfigPath &&
path.resolve(args.projectDirOrConfigPath.projectDir, 'package.json')
if (packageJsonPath && fse.existsSync(packageJsonPath)) {
try {
const packageObj = await fse.readJson(packageJsonPath)
const dbPackage = dbReplacements[args.dbDetails.type]
// Delete all other db adapters
Object.values(dbReplacements).forEach((p) => {
if (p.packageName !== dbPackage.packageName) {
delete packageObj.dependencies[p.packageName]
}
})
// Set version of db adapter to match payload version
packageObj.dependencies[dbPackage.packageName] = packageObj.dependencies['payload']
await fse.writeJson(packageJsonPath, packageObj, { spaces: 2 })
} catch (err: unknown) {
warning(`Unable to configure Payload in package.json`)
warning(err instanceof Error ? err.message : '')
}
}
try {
let payloadConfigPath: string | undefined
if (!('payloadConfigPath' in args.projectDirOrConfigPath)) {

View File

@@ -4,7 +4,6 @@ import * as p from '@clack/prompts'
import { parse, stringify } from 'comment-json'
import execa from 'execa'
import fs from 'fs'
import fse from 'fs-extra'
import globby from 'globby'
import path from 'path'
import { promisify } from 'util'
@@ -32,8 +31,6 @@ type InitNextArgs = Pick<CliArgs, '--debug'> & {
useDistFiles?: boolean
}
type NextConfigType = 'cjs' | 'esm'
type InitNextResult =
| {
isSrcDir: boolean
@@ -48,22 +45,11 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const nextAppDetails = args.nextAppDetails || (await getNextAppDetails(projectDir))
if (!nextAppDetails.nextAppDir) {
warning(`Could not find app directory in ${projectDir}, creating...`)
const createdAppDir = path.resolve(projectDir, nextAppDetails.isSrcDir ? 'src/app' : 'app')
fse.mkdirSync(createdAppDir, { recursive: true })
nextAppDetails.nextAppDir = createdAppDir
}
const { hasTopLevelLayout, isSrcDir, nextAppDir } =
nextAppDetails || (await getNextAppDetails(projectDir))
const { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigType } = nextAppDetails
if (!nextConfigType) {
return {
isSrcDir,
nextAppDir,
reason: `Could not determine Next Config type in ${projectDir}. Possibly try renaming next.config.js to next.config.cjs or next.config.mjs.`,
success: false,
}
if (!nextAppDir) {
return { isSrcDir, reason: `Could not find app directory in ${projectDir}`, success: false }
}
if (hasTopLevelLayout) {
@@ -83,7 +69,6 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const configurationResult = installAndConfigurePayload({
...args,
nextAppDetails,
nextConfigType,
useDistFiles: true, // Requires running 'pnpm pack-template-files' in cpa
})
@@ -111,23 +96,12 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean) {
const tsConfigPath = path.resolve(projectDir, 'tsconfig.json')
// Check if tsconfig.json exists
if (!fs.existsSync(tsConfigPath)) {
warning(`Could not find tsconfig.json to add @payload-config path.`)
return
}
const userTsConfigContent = await readFile(tsConfigPath, {
encoding: 'utf8',
})
const userTsConfig = parse(userTsConfigContent) as {
compilerOptions?: CompilerOptions
}
const hasBaseUrl =
userTsConfig?.compilerOptions?.baseUrl && userTsConfig?.compilerOptions?.baseUrl !== '.'
const baseUrl = hasBaseUrl ? userTsConfig?.compilerOptions?.baseUrl : './'
if (!userTsConfig.compilerOptions && !('extends' in userTsConfig)) {
userTsConfig.compilerOptions = {}
}
@@ -138,25 +112,20 @@ async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean)
) {
userTsConfig.compilerOptions.paths = {
...(userTsConfig.compilerOptions.paths || {}),
'@payload-config': [`${baseUrl}${isSrcDir ? 'src/' : ''}payload.config.ts`],
'@payload-config': [`./${isSrcDir ? 'src/' : ''}payload.config.ts`],
}
await writeFile(tsConfigPath, stringify(userTsConfig, null, 2), { encoding: 'utf8' })
}
}
function installAndConfigurePayload(
args: InitNextArgs & {
nextAppDetails: NextAppDetails
nextConfigType: NextConfigType
useDistFiles?: boolean
},
args: InitNextArgs & { nextAppDetails: NextAppDetails; useDistFiles?: boolean },
):
| { payloadConfigPath: string; success: true }
| { payloadConfigPath?: string; reason: string; success: false } {
const {
'--debug': debug,
nextAppDetails: { isSrcDir, nextAppDir, nextConfigPath } = {},
nextConfigType,
projectDir,
useDistFiles,
} = args
@@ -203,7 +172,6 @@ function installAndConfigurePayload(
logDebug(`nextAppDir: ${nextAppDir}`)
logDebug(`projectDir: ${projectDir}`)
logDebug(`nextConfigPath: ${nextConfigPath}`)
logDebug(`payloadConfigPath: ${path.resolve(projectDir, 'payload.config.ts')}`)
logDebug(
`isSrcDir: ${isSrcDir}. source: ${templateSrcDir}. dest: ${path.dirname(nextConfigPath)}`,
@@ -213,7 +181,7 @@ function installAndConfigurePayload(
copyRecursiveSync(templateSrcDir, path.dirname(nextConfigPath), debug)
// Wrap next.config.js with withPayload
wrapNextConfig({ nextConfigPath, nextConfigType })
wrapNextConfig({ nextConfigPath })
return {
payloadConfigPath: path.resolve(nextAppDir, '../payload.config.ts'),
@@ -223,10 +191,10 @@ function installAndConfigurePayload(
async function installDeps(projectDir: string, packageManager: PackageManager, dbType: DbType) {
const packagesToInstall = ['payload', '@payloadcms/next', '@payloadcms/richtext-lexical'].map(
(pkg) => `${pkg}@beta`,
(pkg) => `${pkg}@alpha`,
)
packagesToInstall.push(`@payloadcms/db-${dbType}@beta`)
packagesToInstall.push(`@payloadcms/db-${dbType}@alpha`)
let exitCode = 0
switch (packageManager) {
@@ -258,7 +226,6 @@ type NextAppDetails = {
isSrcDir: boolean
nextAppDir?: string
nextConfigPath?: string
nextConfigType?: NextConfigType
}
export async function getNextAppDetails(projectDir: string): Promise<NextAppDetails> {
@@ -279,7 +246,6 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
await globby(['**/app'], {
absolute: true,
cwd: projectDir,
ignore: ['**/node_modules/**'],
onlyDirectories: true,
})
)?.[0]
@@ -288,31 +254,9 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
nextAppDir = undefined
}
const configType = await getProjectType(projectDir, nextConfigPath)
const hasTopLevelLayout = nextAppDir
? fs.existsSync(path.resolve(nextAppDir, 'layout.tsx'))
: false
return { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigPath, nextConfigType: configType }
}
async function getProjectType(projectDir: string, nextConfigPath: string): Promise<'cjs' | 'esm'> {
if (nextConfigPath.endsWith('.mjs')) {
return 'esm'
}
if (nextConfigPath.endsWith('.cjs')) {
return 'cjs'
}
const packageObj = await fse.readJson(path.resolve(projectDir, 'package.json'))
const packageJsonType = packageObj.type
if (packageJsonType === 'module') {
return 'esm'
}
if (packageJsonType === 'commonjs') {
return 'cjs'
}
return 'cjs'
return { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigPath }
}

View File

@@ -10,18 +10,14 @@ const mongodbReplacement: DbAdapterReplacement = {
importReplacement: "import { mongooseAdapter } from '@payloadcms/db-mongodb'",
packageName: '@payloadcms/db-mongodb',
// Replacement between `// database-adapter-config-start` and `// database-adapter-config-end`
configReplacement: [
' db: mongooseAdapter({',
" url: process.env.DATABASE_URI || '',",
' }),',
],
configReplacement: [' db: mongooseAdapter({', ' url: process.env.DATABASE_URI,', ' }),'],
}
const postgresReplacement: DbAdapterReplacement = {
configReplacement: [
' db: postgresAdapter({',
' pool: {',
" connectionString: process.env.DATABASE_URI || '',",
' connectionString: process.env.DATABASE_URI,',
' },',
' }),',
],

View File

@@ -16,7 +16,7 @@ const dbChoiceRecord: Record<DbType, DbChoice> = {
value: 'mongodb',
},
postgres: {
dbConnectionPrefix: 'postgres://postgres:<password>@127.0.0.1:5432/',
dbConnectionPrefix: 'postgres://127.0.0.1:5432/',
title: 'PostgreSQL (beta)',
value: 'postgres',
},

View File

@@ -18,46 +18,43 @@ export function getValidTemplates(): ProjectTemplate[] {
name: 'blank-3.0',
type: 'starter',
description: 'Blank 3.0 Template',
url: 'https://github.com/payloadcms/payload/templates/blank-3.0#beta',
url: 'https://github.com/payloadcms/payload/templates/blank-3.0',
},
{
name: 'blank',
type: 'starter',
description: 'Blank Template',
url: 'https://github.com/payloadcms/payload/templates/blank',
},
{
name: 'website',
type: 'starter',
description: 'Website Template',
url: 'https://github.com/payloadcms/payload/templates/website',
},
{
name: 'ecommerce',
type: 'starter',
description: 'E-commerce Template',
url: 'https://github.com/payloadcms/payload/templates/ecommerce',
},
// Remove these until they have been updated for 3.0
// {
// name: 'blank',
// type: 'starter',
// description: 'Blank Template',
// url: 'https://github.com/payloadcms/payload/templates/blank',
// },
// {
// name: 'website',
// type: 'starter',
// description: 'Website Template',
// url: 'https://github.com/payloadcms/payload/templates/website',
// },
// {
// name: 'ecommerce',
// type: 'starter',
// description: 'E-commerce Template',
// url: 'https://github.com/payloadcms/payload/templates/ecommerce',
// },
{
name: 'plugin',
type: 'plugin',
description: 'Template for creating a Payload plugin',
url: 'https://github.com/payloadcms/payload-plugin-template#beta',
url: 'https://github.com/payloadcms/payload-plugin-template',
},
{
name: 'payload-demo',
type: 'starter',
description: 'Payload demo site at https://demo.payloadcms.com',
url: 'https://github.com/payloadcms/public-demo',
},
{
name: 'payload-website',
type: 'starter',
description: 'Payload website CMS at https://payloadcms.com',
url: 'https://github.com/payloadcms/website-cms',
},
// {
// name: 'payload-demo',
// type: 'starter',
// description: 'Payload demo site at https://demo.payloadcms.com',
// url: 'https://github.com/payloadcms/public-demo',
// },
// {
// name: 'payload-website',
// type: 'starter',
// description: 'Payload website CMS at https://payloadcms.com',
// url: 'https://github.com/payloadcms/website-cms',
// },
]
}

View File

@@ -1,159 +1,61 @@
import { parseAndModifyConfigContent, withPayloadStatement } from './wrap-next-config.js'
import { parseAndModifyConfigContent, withPayloadImportStatement } from './wrap-next-config.js'
import * as p from '@clack/prompts'
const esmConfigs = {
defaultNextConfig: `/** @type {import('next').NextConfig} */
const defaultNextConfig = `/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
`,
nextConfigWithFunc: `const nextConfig = {};
export default someFunc(nextConfig);
`,
nextConfigWithFuncMultiline: `const nextConfig = {};;
`
const nextConfigWithFunc = `const nextConfig = {
// Your Next.js config here
}
export default someFunc(nextConfig)
`
const nextConfigWithFuncMultiline = `const nextConfig = {
// Your Next.js config here
}
export default someFunc(
nextConfig
);
`,
nextConfigExportNamedDefault: `const nextConfig = {};
const wrapped = someFunc(asdf);
export { wrapped as default };
`,
nextConfigWithSpread: `const nextConfig = {
...someConfig,
};
export default nextConfig;
`,
}
)
`
const cjsConfigs = {
defaultNextConfig: `
/** @type {import('next').NextConfig} */
const nextConfig = {};
module.exports = nextConfig;
`,
anonConfig: `module.exports = {};`,
nextConfigWithFunc: `const nextConfig = {};
module.exports = someFunc(nextConfig);
`,
nextConfigWithFuncMultiline: `const nextConfig = {};
module.exports = someFunc(
nextConfig
);
`,
nextConfigExportNamedDefault: `const nextConfig = {};
const wrapped = someFunc(asdf);
module.exports = wrapped;
`,
nextConfigWithSpread: `const nextConfig = { ...someConfig };
module.exports = nextConfig;
`,
const nextConfigExportNamedDefault = `const nextConfig = {
// Your Next.js config here
}
const wrapped = someFunc(asdf)
export { wrapped as default }
`
describe('parseAndInsertWithPayload', () => {
describe('esm', () => {
const configType = 'esm'
const importStatement = withPayloadStatement[configType]
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.defaultNextConfig,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.nextConfigWithFunc,
configType,
)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
})
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.nextConfigWithFuncMultiline,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n nextConfig\n\)\)/)
})
it('should parse the config with a spread', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.nextConfigWithSpread,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
// Unsupported: export { wrapped as default }
it('should give warning with a named export as default', () => {
const warnLogSpy = jest.spyOn(p.log, 'warn').mockImplementation(() => {})
const { modifiedConfigContent, success } = parseAndModifyConfigContent(
esmConfigs.nextConfigExportNamedDefault,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(success).toBe(false)
expect(warnLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Could not automatically wrap'),
)
})
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(defaultNextConfig)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(nextConfigWithFunc)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
})
describe('cjs', () => {
const configType = 'cjs'
const requireStatement = withPayloadStatement[configType]
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.defaultNextConfig,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse anonymous default config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.anonConfig,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload({})')
})
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigWithFunc,
configType,
)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
})
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigWithFuncMultiline,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n nextConfig\n\)\)/)
})
it('should parse the config with a named export as default', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigExportNamedDefault,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload(wrapped)')
})
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(nextConfigWithFuncMultiline)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n nextConfig\n\)\)/)
})
it('should parse the config with a spread', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigWithSpread,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
// Unsupported: export { wrapped as default }
it('should give warning with a named export as default', () => {
const warnLogSpy = jest.spyOn(p.log, 'warn').mockImplementation(() => {})
const { modifiedConfigContent, success } = parseAndModifyConfigContent(
nextConfigExportNamedDefault,
)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(success).toBe(false)
expect(warnLogSpy).toHaveBeenCalledWith(expect.stringContaining('Could not automatically wrap'))
})
})

View File

@@ -1,29 +1,16 @@
import type { Program } from 'esprima-next'
import chalk from 'chalk'
import { Syntax, parseModule } from 'esprima-next'
import { parseModule } from 'esprima'
import fs from 'fs'
import { warning } from '../utils/log.js'
import { log } from '../utils/log.js'
export const withPayloadStatement = {
cjs: `const { withPayload } = require('@payloadcms/next/withPayload')\n`,
esm: `import { withPayload } from '@payloadcms/next/withPayload'\n`,
}
export const withPayloadImportStatement = `import { withPayload } from '@payloadcms/next'\n`
type NextConfigType = 'cjs' | 'esm'
export const wrapNextConfig = (args: {
nextConfigPath: string
nextConfigType: NextConfigType
}) => {
const { nextConfigPath, nextConfigType: configType } = args
export const wrapNextConfig = (args: { nextConfigPath: string }) => {
const { nextConfigPath } = args
const configContent = fs.readFileSync(nextConfigPath, 'utf8')
const { modifiedConfigContent: newConfig, success } = parseAndModifyConfigContent(
configContent,
configType,
)
const { modifiedConfigContent: newConfig, success } = parseAndModifyConfigContent(configContent)
if (!success) {
return
@@ -35,121 +22,72 @@ export const wrapNextConfig = (args: {
/**
* Parses config content with AST and wraps it with withPayload function
*/
export function parseAndModifyConfigContent(
content: string,
configType: NextConfigType,
): { modifiedConfigContent: string; success: boolean } {
content = withPayloadStatement[configType] + content
export function parseAndModifyConfigContent(content: string): {
modifiedConfigContent: string
success: boolean
} {
content = withPayloadImportStatement + content
const ast = parseModule(content, { loc: true })
const exportDefaultDeclaration = ast.body.find((p) => p.type === 'ExportDefaultDeclaration') as
| Directive
| undefined
let ast: Program | undefined
try {
ast = parseModule(content, { loc: true })
} catch (error: unknown) {
if (error instanceof Error) {
warning(`Unable to parse Next config. Error: ${error.message} `)
warnUserWrapNotSuccessful(configType)
}
return {
modifiedConfigContent: content,
success: false,
}
const exportNamedDeclaration = ast.body.find((p) => p.type === 'ExportNamedDeclaration') as
| ExportNamedDeclaration
| undefined
if (!exportDefaultDeclaration && !exportNamedDeclaration) {
throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
}
if (configType === 'esm') {
const exportDefaultDeclaration = ast.body.find(
(p) => p.type === Syntax.ExportDefaultDeclaration,
) as Directive | undefined
if (exportDefaultDeclaration && exportDefaultDeclaration.declaration?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
exportDefaultDeclaration.declaration.loc,
)
return { modifiedConfigContent, success: true }
} else if (exportNamedDeclaration) {
const exportSpecifier = exportNamedDeclaration.specifiers.find(
(s) =>
s.type === 'ExportSpecifier' &&
s.exported?.name === 'default' &&
s.local?.type === 'Identifier' &&
s.local?.name,
)
const exportNamedDeclaration = ast.body.find(
(p) => p.type === Syntax.ExportNamedDeclaration,
) as ExportNamedDeclaration | undefined
if (exportSpecifier) {
warning('Could not automatically wrap next.config.js with withPayload.')
warning('Automatic wrapping of named exports as default not supported yet.')
if (!exportDefaultDeclaration && !exportNamedDeclaration) {
throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
}
if (exportDefaultDeclaration && exportDefaultDeclaration.declaration?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
exportDefaultDeclaration.declaration.loc,
)
return { modifiedConfigContent, success: true }
} else if (exportNamedDeclaration) {
const exportSpecifier = exportNamedDeclaration.specifiers.find(
(s) =>
s.type === 'ExportSpecifier' &&
s.exported?.name === 'default' &&
s.local?.type === 'Identifier' &&
s.local?.name,
)
if (exportSpecifier) {
warning('Could not automatically wrap next.config.js with withPayload.')
warning('Automatic wrapping of named exports as default not supported yet.')
warnUserWrapNotSuccessful(configType)
return {
modifiedConfigContent: content,
success: false,
}
warnUserWrapNotSuccessful()
return {
modifiedConfigContent: content,
success: false,
}
}
warning('Could not automatically wrap Next config with withPayload.')
warnUserWrapNotSuccessful(configType)
return {
modifiedConfigContent: content,
success: false,
}
} else if (configType === 'cjs') {
// Find `module.exports = X`
const moduleExports = ast.body.find(
(p) =>
p.type === Syntax.ExpressionStatement &&
p.expression?.type === Syntax.AssignmentExpression &&
p.expression.left?.type === Syntax.MemberExpression &&
p.expression.left.object?.type === Syntax.Identifier &&
p.expression.left.object.name === 'module' &&
p.expression.left.property?.type === Syntax.Identifier &&
p.expression.left.property.name === 'exports',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any
if (moduleExports && moduleExports.expression.right?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
moduleExports.expression.right.loc,
)
return { modifiedConfigContent, success: true }
}
return {
modifiedConfigContent: content,
success: false,
}
}
warning('Could not automatically wrap Next config with withPayload.')
warnUserWrapNotSuccessful(configType)
warning('Could not automatically wrap next.config.js with withPayload.')
warnUserWrapNotSuccessful()
return {
modifiedConfigContent: content,
success: false,
}
}
function warnUserWrapNotSuccessful(configType: NextConfigType) {
function warnUserWrapNotSuccessful() {
// Output directions for user to update next.config.js
const withPayloadMessage = `
${chalk.bold(`Please manually wrap your existing next.config.js with the withPayload function. Here is an example:`)}
${withPayloadStatement[configType]}
import withPayload from '@payloadcms/next/withPayload'
const nextConfig = {
// Your Next.js config here
}
${configType === 'esm' ? 'export default withPayload(nextConfig)' : 'module.exports = withPayload(nextConfig)'}
export default withPayload(nextConfig)
`

View File

@@ -20,41 +20,32 @@ export async function writeEnvFile(args: {
return
}
const envOutputPath = path.join(projectDir, '.env')
try {
if (fs.existsSync(envOutputPath)) {
if (template?.type === 'starter') {
// Parse .env file into key/value pairs
const envFile = await fs.readFile(path.join(projectDir, '.env.example'), 'utf8')
const envWithValues: string[] = envFile
.split('\n')
.filter((e) => e)
.map((line) => {
if (line.startsWith('#') || !line.includes('=')) return line
if (template?.type === 'starter' && fs.existsSync(path.join(projectDir, '.env.example'))) {
// Parse .env file into key/value pairs
const envFile = await fs.readFile(path.join(projectDir, '.env.example'), 'utf8')
const envWithValues: string[] = envFile
.split('\n')
.filter((e) => e)
.map((line) => {
if (line.startsWith('#') || !line.includes('=')) return line
const split = line.split('=')
const key = split[0]
let value = split[1]
const split = line.split('=')
const key = split[0]
let value = split[1]
if (key === 'MONGODB_URI' || key === 'MONGO_URL' || key === 'DATABASE_URI') {
value = databaseUri
}
if (key === 'PAYLOAD_SECRET' || key === 'PAYLOAD_SECRET_KEY') {
value = payloadSecret
}
if (key === 'MONGODB_URI' || key === 'MONGO_URL' || key === 'DATABASE_URI') {
value = databaseUri
}
if (key === 'PAYLOAD_SECRET' || key === 'PAYLOAD_SECRET_KEY') {
value = payloadSecret
}
return `${key}=${value}`
})
return `${key}=${value}`
})
// Write new .env file
await fs.writeFile(envOutputPath, envWithValues.join('\n'))
} else {
const existingEnv = await fs.readFile(envOutputPath, 'utf8')
const newEnv =
existingEnv + `\nDATABASE_URI=${databaseUri}\nPAYLOAD_SECRET=${payloadSecret}\n`
await fs.writeFile(envOutputPath, newEnv)
}
// Write new .env file
await fs.writeFile(path.join(projectDir, '.env'), envWithValues.join('\n'))
} else {
const content = `DATABASE_URI=${databaseUri}\nPAYLOAD_SECRET=${payloadSecret}`
await fs.outputFile(`${projectDir}/.env`, content)

View File

@@ -21,12 +21,6 @@ export function helpMessage(): void {
console.log(chalk`
{bold USAGE}
{dim Inside of an existing Next.js project}
{dim $} {bold npx create-payload-app}
{dim Create a new project from scratch}
{dim $} {bold npx create-payload-app}
{dim $} {bold npx create-payload-app} my-project
{dim $} {bold npx create-payload-app} -n my-project -t template-name
@@ -86,7 +80,7 @@ export function successfulNextInit(): string {
}
export function moveMessage(args: { nextAppDir: string; projectDir: string }): string {
const relativeAppDir = path.relative(process.cwd(), args.nextAppDir)
const relativePath = path.relative(process.cwd(), args.nextAppDir)
return `
${header('Next Steps:')}
@@ -94,10 +88,7 @@ Payload does not support a top-level layout.tsx file in the app directory.
${chalk.bold('To continue:')}
- Create a new directory in ./${relativeAppDir} such as ./${relativeAppDir}/${chalk.bold('(app)')}
- Move all files from ./${relativeAppDir} into that directory
It is recommended to do this from your IDE if your app has existing file references.
Move all files from ./${relativePath} to a named directory such as ./${relativePath}/${chalk.bold('(app)')}
Once moved, rerun the create-payload-app command again.
`

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.0.0-beta.18",
"version": "3.0.0-alpha.61",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": {
"type": "git",

View File

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

View File

@@ -1,5 +1,5 @@
import type { Create } from 'payload/database'
import type { Document, PayloadRequestWithData } from 'payload/types'
import type { Document, PayloadRequest } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -8,7 +8,7 @@ import { withSession } from './withSession.js'
export const create: Create = async function create(
this: MongooseAdapter,
{ collection, data, req = {} as PayloadRequestWithData },
{ collection, data, req = {} as PayloadRequest },
) {
const Model = this.collections[collection]
const options = withSession(this, req.transactionID)

View File

@@ -1,5 +1,5 @@
import type { CreateGlobal } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -8,7 +8,7 @@ import { withSession } from './withSession.js'
export const createGlobal: CreateGlobal = async function createGlobal(
this: MongooseAdapter,
{ slug, data, req = {} as PayloadRequestWithData },
{ slug, data, req = {} as PayloadRequest },
) {
const Model = this.globals
const global = {

View File

@@ -1,5 +1,5 @@
import type { CreateGlobalVersion } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import type { Document } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -8,15 +8,7 @@ import { withSession } from './withSession.js'
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
this: MongooseAdapter,
{
autosave,
createdAt,
globalSlug,
parent,
req = {} as PayloadRequestWithData,
updatedAt,
versionData,
},
{ autosave, createdAt, globalSlug, parent, req = {} as PayloadRequest, updatedAt, versionData },
) {
const VersionModel = this.versions[globalSlug]
const options = withSession(this, req.transactionID)

View File

@@ -1,5 +1,5 @@
import type { CreateVersion } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import type { Document } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -13,7 +13,7 @@ export const createVersion: CreateVersion = async function createVersion(
collectionSlug,
createdAt,
parent,
req = {} as PayloadRequestWithData,
req = {} as PayloadRequest,
updatedAt,
versionData,
},

View File

@@ -1,5 +1,5 @@
import type { DeleteMany } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -7,7 +7,7 @@ import { withSession } from './withSession.js'
export const deleteMany: DeleteMany = async function deleteMany(
this: MongooseAdapter,
{ collection, req = {} as PayloadRequestWithData, where },
{ collection, req = {} as PayloadRequest, where },
) {
const Model = this.collections[collection]
const options = {

View File

@@ -1,5 +1,5 @@
import type { DeleteOne } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import type { Document } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -9,7 +9,7 @@ import { withSession } from './withSession.js'
export const deleteOne: DeleteOne = async function deleteOne(
this: MongooseAdapter,
{ collection, req = {} as PayloadRequestWithData, where },
{ collection, req = {} as PayloadRequest, where },
) {
const Model = this.collections[collection]
const options = withSession(this, req.transactionID)

View File

@@ -1,5 +1,5 @@
import type { DeleteVersions } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -7,7 +7,7 @@ import { withSession } from './withSession.js'
export const deleteVersions: DeleteVersions = async function deleteVersions(
this: MongooseAdapter,
{ collection, locale, req = {} as PayloadRequestWithData, where },
{ collection, locale, req = {} as PayloadRequest, where },
) {
const VersionsModel = this.versions[collection]
const options = {

View File

@@ -1,6 +1,6 @@
import type { PaginateOptions } from 'mongoose'
import type { Find } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import { flattenWhereToOperators } from 'payload/database'
@@ -12,16 +12,7 @@ import { withSession } from './withSession.js'
export const find: Find = async function find(
this: MongooseAdapter,
{
collection,
limit,
locale,
page,
pagination,
req = {} as PayloadRequestWithData,
sort: sortArg,
where,
},
{ collection, limit, locale, page, pagination, req = {} as PayloadRequest, sort: sortArg, where },
) {
const Model = this.collections[collection]
const collectionConfig = this.payload.collections[collection].config

View File

@@ -1,5 +1,5 @@
import type { FindGlobal } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import { combineQueries } from 'payload/database'
@@ -10,7 +10,7 @@ import { withSession } from './withSession.js'
export const findGlobal: FindGlobal = async function findGlobal(
this: MongooseAdapter,
{ slug, locale, req = {} as PayloadRequestWithData, where },
{ slug, locale, req = {} as PayloadRequest, where },
) {
const Model = this.globals
const options = {

View File

@@ -1,6 +1,6 @@
import type { PaginateOptions } from 'mongoose'
import type { FindGlobalVersions } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import { flattenWhereToOperators } from 'payload/database'
import { buildVersionGlobalFields } from 'payload/versions'
@@ -19,7 +19,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
locale,
page,
pagination,
req = {} as PayloadRequestWithData,
req = {} as PayloadRequest,
skip,
sort: sortArg,
where,

View File

@@ -1,6 +1,6 @@
import type { MongooseQueryOptions } from 'mongoose'
import type { FindOne } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import type { Document } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -10,7 +10,7 @@ import { withSession } from './withSession.js'
export const findOne: FindOne = async function findOne(
this: MongooseAdapter,
{ collection, locale, req = {} as PayloadRequestWithData, where },
{ collection, locale, req = {} as PayloadRequest, where },
) {
const Model = this.collections[collection]
const options: MongooseQueryOptions = {

View File

@@ -1,6 +1,6 @@
import type { PaginateOptions } from 'mongoose'
import type { FindVersions } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import { flattenWhereToOperators } from 'payload/database'
@@ -18,7 +18,7 @@ export const findVersions: FindVersions = async function findVersions(
locale,
page,
pagination,
req = {} as PayloadRequestWithData,
req = {} as PayloadRequest,
skip,
sort: sortArg,
where,

View File

@@ -12,7 +12,6 @@ import { createDatabaseAdapter } from 'payload/database'
import type { CollectionModel, GlobalModel } from './types.js'
import { connect } from './connect.js'
import { count } from './count.js'
import { create } from './create.js'
import { createGlobal } from './createGlobal.js'
import { createGlobalVersion } from './createGlobalVersion.js'
@@ -113,7 +112,6 @@ export function mongooseAdapter({
collections: {},
connectOptions: connectOptions || {},
connection: undefined,
count,
disableIndexHints,
globals: undefined,
mongoMemoryServer,
@@ -121,6 +119,7 @@ export function mongooseAdapter({
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
url,
versions: {},
// DatabaseAdapter
beginTransaction: transactionOptions ? beginTransaction : undefined,
commitTransaction,

View File

@@ -28,7 +28,6 @@ export const init: Init = function init(this: MongooseAdapter) {
const versionSchema = buildSchema(this.payload.config, versionCollectionFields, {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: false,
@@ -57,6 +56,12 @@ export const init: Init = function init(this: MongooseAdapter) {
this.autoPluralization === true ? undefined : collection.slug,
) as CollectionModel
this.collections[collection.slug] = model
// TS expect error only needed until we launch 2.0.0
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
this.payload.collections[collection.slug] = {
config: collection,
}
})
const model = buildGlobalModel(this.payload.config)

View File

@@ -1,4 +1,4 @@
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import {
commitTransaction,
@@ -50,7 +50,7 @@ export async function migrateFresh(
msg: `Found ${migrationFiles.length} migration files.`,
})
const req = { payload } as PayloadRequestWithData
const req = { payload } as PayloadRequest
// Run all migrate up
for (const migration of migrationFiles) {

View File

@@ -59,12 +59,17 @@ export async function buildSearchParam({
let hasCustomID = false
if (sanitizedPath === '_id') {
const customIDFieldType = payload.collections[collectionSlug]?.customIDType
const customIDfield = payload.collections[collectionSlug]?.config.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
let idFieldType: 'number' | 'text' = 'text'
if (customIDFieldType) {
idFieldType = customIDFieldType
if (customIDfield) {
if (customIDfield?.type === 'text' || customIDfield?.type === 'number') {
idFieldType = customIDfield.type
}
hasCustomID = true
}
@@ -208,11 +213,18 @@ export async function buildSearchParam({
} else {
;(Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]).forEach(
(relationTo) => {
const isRelatedToCustomNumberID =
payload.collections[relationTo]?.customIDType === 'number'
const isRelatedToCustomNumberID = payload.collections[
relationTo
]?.config?.fields.find((relatedField) => {
return (
fieldAffectsData(relatedField) &&
relatedField.name === 'id' &&
relatedField.type === 'number'
)
})
if (isRelatedToCustomNumberID) {
hasNumberIDRelation = true
if (isRelatedToCustomNumberID.type === 'number') hasNumberIDRelation = true
}
},
)

View File

@@ -1,20 +1,18 @@
import { SanitizedConfig, sanitizeConfig } from 'payload/config'
import { sanitizeConfig } from 'payload/config'
import { Config } from 'payload/config'
import { getLocalizedSortProperty } from './getLocalizedSortProperty.js'
const config = sanitizeConfig({
const config = {
localization: {
locales: ['en', 'es'],
defaultLocale: 'en',
fallback: true,
},
} as Config) as SanitizedConfig
} as Config
describe('get localized sort property', () => {
it('passes through a non-localized sort property', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config,
config: sanitizeConfig(config),
fields: [
{
name: 'title',
@@ -30,7 +28,7 @@ describe('get localized sort property', () => {
it('properly localizes an un-localized sort property', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config,
config: sanitizeConfig(config),
fields: [
{
name: 'title',
@@ -47,7 +45,7 @@ describe('get localized sort property', () => {
it('keeps specifically asked-for localized sort properties', () => {
const result = getLocalizedSortProperty({
segments: ['title', 'es'],
config,
config: sanitizeConfig(config),
fields: [
{
name: 'title',
@@ -64,7 +62,7 @@ describe('get localized sort property', () => {
it('properly localizes nested sort properties', () => {
const result = getLocalizedSortProperty({
segments: ['group', 'title'],
config,
config: sanitizeConfig(config),
fields: [
{
name: 'group',
@@ -87,7 +85,7 @@ describe('get localized sort property', () => {
it('keeps requested locale with nested sort properties', () => {
const result = getLocalizedSortProperty({
segments: ['group', 'title', 'es'],
config,
config: sanitizeConfig(config),
fields: [
{
name: 'group',
@@ -110,7 +108,7 @@ describe('get localized sort property', () => {
it('properly localizes field within row', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config,
config: sanitizeConfig(config),
fields: [
{
type: 'row',
@@ -132,7 +130,7 @@ describe('get localized sort property', () => {
it('properly localizes field within named tab', () => {
const result = getLocalizedSortProperty({
segments: ['tab', 'title'],
config,
config: sanitizeConfig(config),
fields: [
{
type: 'tabs',
@@ -159,7 +157,7 @@ describe('get localized sort property', () => {
it('properly localizes field within unnamed tab', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config,
config: sanitizeConfig(config),
fields: [
{
type: 'tabs',

View File

@@ -142,10 +142,7 @@ export const sanitizeQueryValue = ({
if (path !== '_id' || (path === '_id' && hasCustomID && field.type === 'text')) {
if (operator === 'contains') {
formattedValue = {
$options: 'i',
$regex: formattedValue.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'),
}
formattedValue = { $options: 'i', $regex: formattedValue }
}
}

View File

@@ -1,6 +1,6 @@
import type { PaginateOptions } from 'mongoose'
import type { QueryDrafts } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import { combineQueries, flattenWhereToOperators } from 'payload/database'
@@ -12,16 +12,7 @@ import { withSession } from './withSession.js'
export const queryDrafts: QueryDrafts = async function queryDrafts(
this: MongooseAdapter,
{
collection,
limit,
locale,
page,
pagination,
req = {} as PayloadRequestWithData,
sort: sortArg,
where,
},
{ collection, limit, locale, page, pagination, req = {} as PayloadRequest, sort: sortArg, where },
) {
const VersionModel = this.versions[collection]
const collectionConfig = this.payload.collections[collection].config

View File

@@ -6,10 +6,6 @@ export const commitTransaction: CommitTransaction = async function commitTransac
}
await this.sessions[id].commitTransaction()
try {
await this.sessions[id].endSession()
} catch (error) {
// ending sessions is only best effort and won't impact anything if it fails since the transaction was committed
}
await this.sessions[id].endSession()
delete this.sessions[id]
}

View File

@@ -1,5 +1,5 @@
import type { UpdateGlobal } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -8,7 +8,7 @@ import { withSession } from './withSession.js'
export const updateGlobal: UpdateGlobal = async function updateGlobal(
this: MongooseAdapter,
{ slug, data, req = {} as PayloadRequestWithData },
{ slug, data, req = {} as PayloadRequest },
) {
const Model = this.globals
const options = {

View File

@@ -1,5 +1,5 @@
import type { UpdateGlobalVersionArgs } from 'payload/database'
import type { PayloadRequestWithData, TypeWithID } from 'payload/types'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -11,7 +11,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
id,
global,
locale,
req = {} as PayloadRequestWithData,
req = {} as PayloadRequest,
versionData,
where,
}: UpdateGlobalVersionArgs<T>,

View File

@@ -1,5 +1,5 @@
import type { UpdateOne } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -9,7 +9,7 @@ import { withSession } from './withSession.js'
export const updateOne: UpdateOne = async function updateOne(
this: MongooseAdapter,
{ id, collection, data, locale, req = {} as PayloadRequestWithData, where: whereArg },
{ id, collection, data, locale, req = {} as PayloadRequest, where: whereArg },
) {
const where = id ? { id: { equals: id } } : whereArg
const Model = this.collections[collection]

View File

@@ -1,5 +1,5 @@
import type { UpdateVersion } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -7,7 +7,7 @@ import { withSession } from './withSession.js'
export const updateVersion: UpdateVersion = async function updateVersion(
this: MongooseAdapter,
{ id, collection, locale, req = {} as PayloadRequestWithData, versionData, where },
{ id, collection, locale, req = {} as PayloadRequest, versionData, where },
) {
const VersionModel = this.versions[collection]
const whereToUse = where || { id: { equals: id } }

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.18",
"version": "3.0.0-alpha.61",
"description": "The officially supported Postgres database adapter for Payload",
"repository": {
"type": "git",

View File

@@ -1,62 +0,0 @@
import type { Count } from 'payload/database'
import type { SanitizedCollectionConfig } from 'payload/types'
import { sql } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { ChainedMethods } from './find/chainMethods.js'
import type { PostgresAdapter } from './types.js'
import { chainMethods } from './find/chainMethods.js'
import buildQuery from './queries/buildQuery.js'
export const count: Count = async function count(
this: PostgresAdapter,
{ collection, locale, req, where: whereArg },
) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
const db = this.sessions[req.transactionID]?.db || this.drizzle
const table = this.tables[tableName]
const { joinAliases, joins, where } = await buildQuery({
adapter: this,
fields: collectionConfig.fields,
locale,
tableName,
where: whereArg,
})
const selectCountMethods: ChainedMethods = []
joinAliases.forEach(({ condition, table }) => {
selectCountMethods.push({
args: [table, condition],
method: 'leftJoin',
})
})
Object.entries(joins).forEach(([joinTable, condition]) => {
if (joinTable) {
selectCountMethods.push({
args: [this.tables[joinTable], condition],
method: 'leftJoin',
})
}
})
const countResult = await chainMethods({
methods: selectCountMethods,
query: db
.select({
count: sql<number>`count
(DISTINCT ${this.tables[tableName].id})`,
})
.from(table)
.where(where),
})
return { totalDocs: Number(countResult[0].count) }
}

View File

@@ -1,9 +1,8 @@
import type { Create } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export const create: Create = async function create(
@@ -13,8 +12,6 @@ export const create: Create = async function create(
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const result = await upsertRow({
adapter: this,
data,
@@ -22,7 +19,10 @@ export const create: Create = async function create(
fields: collection.fields,
operation: 'create',
req,
tableName,
tableName: getTableName({
adapter: this,
config: collection,
}),
})
return result

View File

@@ -1,21 +1,18 @@
import type { CreateGlobalArgs } from 'payload/database'
import type { PayloadRequestWithData, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function createGlobal<T extends TypeWithID>(
this: PostgresAdapter,
{ slug, data, req = {} as PayloadRequestWithData }: CreateGlobalArgs,
{ slug, data, req = {} as PayloadRequest }: CreateGlobalArgs,
): Promise<T> {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))
const result = await upsertRow<T>({
adapter: this,
data,
@@ -23,7 +20,10 @@ export async function createGlobal<T extends TypeWithID>(
fields: globalConfig.fields,
operation: 'create',
req,
tableName,
tableName: getTableName({
adapter: this,
config: globalConfig,
}),
})
return result

View File

@@ -1,28 +1,26 @@
import type { TypeWithVersion } from 'payload/database'
import type { PayloadRequestWithData, TypeWithID } from 'payload/types'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import { sql } from 'drizzle-orm'
import { type CreateGlobalVersionArgs } from 'payload/database'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function createGlobalVersion<T extends TypeWithID>(
this: PostgresAdapter,
{
autosave,
globalSlug,
req = {} as PayloadRequestWithData,
versionData,
}: CreateGlobalVersionArgs,
{ autosave, globalSlug, req = {} as PayloadRequest, versionData }: CreateGlobalVersionArgs,
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug)
const tableName = this.tableNameMap.get(`_${toSnakeCase(global.slug)}${this.versionsSuffix}`)
const tableName = getTableName({
adapter: this,
config: global,
versions: true,
})
const result = await upsertRow<TypeWithVersion<T>>({
adapter: this,

View File

@@ -1,12 +1,12 @@
import type { CreateVersionArgs, TypeWithVersion } from 'payload/database'
import type { PayloadRequestWithData, TypeWithID } from 'payload/types'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import { sql } from 'drizzle-orm'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function createVersion<T extends TypeWithID>(
@@ -15,18 +15,17 @@ export async function createVersion<T extends TypeWithID>(
autosave,
collectionSlug,
parent,
req = {} as PayloadRequestWithData,
req = {} as PayloadRequest,
versionData,
}: CreateVersionArgs<T>,
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const defaultTableName = toSnakeCase(collection.slug)
const tableName = this.tableNameMap.get(`_${defaultTableName}${this.versionsSuffix}`)
const version = { ...versionData }
if (version.id) delete version.id
const tableName = getTableName({
adapter: this,
config: collection,
versions: true,
})
const result = await upsertRow<TypeWithVersion<T>>({
adapter: this,
@@ -34,7 +33,7 @@ export async function createVersion<T extends TypeWithID>(
autosave,
latest: true,
parent,
version,
version: versionData,
},
db,
fields: buildVersionCollectionFields(collection),
@@ -44,9 +43,15 @@ export async function createVersion<T extends TypeWithID>(
})
const table = this.tables[tableName]
const relationshipsTable =
this.tables[`_${defaultTableName}${this.versionsSuffix}${this.relationshipsSuffix}`]
this.tables[
getTableName({
adapter: this,
config: collection,
relationships: true,
versions: true,
})
]
if (collection.versions.drafts) {
await db.execute(sql`

View File

@@ -1,21 +1,20 @@
import type { DeleteMany } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import { inArray } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const deleteMany: DeleteMany = async function deleteMany(
this: PostgresAdapter,
{ collection, req = {} as PayloadRequestWithData, where },
{ collection, req = {} as PayloadRequest, where },
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
const tableName = getTableName({ adapter: this, config: collectionConfig })
const result = await findMany({
adapter: this,

View File

@@ -1,25 +1,26 @@
import type { DeleteOne } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import { eq } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { getTableName } from './schema/getTableName.js'
import { transform } from './transform/read/index.js'
export const deleteOne: DeleteOne = async function deleteOne(
this: PostgresAdapter,
{ collection: collectionSlug, req = {} as PayloadRequestWithData, where: whereArg },
{ collection: collectionSlug, req = {} as PayloadRequest, where: whereArg },
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const tableName = getTableName({
adapter: this,
config: collection,
})
let docToDelete: Record<string, unknown>
const { joinAliases, joins, selectFields, where } = await buildQuery({

View File

@@ -1,25 +1,26 @@
import type { DeleteVersions } from 'payload/database'
import type { PayloadRequestWithData, SanitizedCollectionConfig } from 'payload/types'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { inArray } from 'drizzle-orm'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const deleteVersions: DeleteVersions = async function deleteVersion(
this: PostgresAdapter,
{ collection, locale, req = {} as PayloadRequestWithData, where: where },
{ collection, locale, req = {} as PayloadRequest, where: where },
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
)
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
const { docs } = await findMany({

View File

@@ -2,12 +2,13 @@ import type { Destroy } from 'payload/database'
import type { PostgresAdapter } from './types.js'
// eslint-disable-next-line @typescript-eslint/require-await
import { pushDevSchema } from './utilities/pushDevSchema.js'
export const destroy: Destroy = async function destroy(this: PostgresAdapter) {
this.enums = {}
this.schema = {}
this.tables = {}
this.relations = {}
this.fieldConstraints = {}
this.drizzle = undefined
if (process.env.NODE_ENV !== 'production') {
await pushDevSchema(this)
} else {
// TODO: this hangs test suite for some reason
// await this.pool.end()
}
}

View File

@@ -1,11 +1,10 @@
import type { Find } from 'payload/database'
import type { PayloadRequestWithData, SanitizedCollectionConfig } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const find: Find = async function find(
this: PostgresAdapter,
@@ -15,15 +14,17 @@ export const find: Find = async function find(
locale,
page = 1,
pagination,
req = {} as PayloadRequestWithData,
req = {} as PayloadRequest,
sort: sortArg,
where,
},
) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
const tableName = getTableName({
adapter: this,
config: collectionConfig,
})
return findMany({
adapter: this,

View File

@@ -1,5 +1,5 @@
import type { FindArgs } from 'payload/database'
import type { Field, PayloadRequestWithData, TypeWithID } from 'payload/types'
import type { Field, PayloadRequest, TypeWithID } from 'payload/types'
import { inArray, sql } from 'drizzle-orm'
@@ -25,7 +25,7 @@ export const findMany = async function find({
locale,
page = 1,
pagination,
req = {} as PayloadRequestWithData,
req = {} as PayloadRequest,
skip,
sort,
tableName,
@@ -120,7 +120,7 @@ export const findMany = async function find({
const findPromise = db.query[tableName].findMany(findManyArgs)
if (pagination !== false && (orderedIDs ? orderedIDs?.length <= limit : true)) {
if (pagination !== false && (orderedIDs ? orderedIDs?.length >= limit : true)) {
const selectCountMethods: ChainedMethods = []
joinAliases.forEach(({ condition, table }) => {

View File

@@ -2,11 +2,12 @@
import type { Field } from 'payload/types'
import { fieldAffectsData, tabHasName } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../types.js'
import type { Result } from './buildFindManyArgs.js'
import { getTableName } from '../schema/getTableName.js'
type TraverseFieldArgs = {
_locales: Record<string, unknown>
adapter: PostgresAdapter
@@ -78,11 +79,20 @@ export const traverseFields = ({
with: {},
}
const arrayTableName = adapter.tableNameMap.get(
`${currentTableName}_${path}${toSnakeCase(field.name)}`,
)
const arrayTableName = getTableName({
adapter,
config: field,
parentTableName: currentTableName,
prefix: `${currentTableName}_${path}`,
})
const arrayTableNameWithLocales = `${arrayTableName}${adapter.localesSuffix}`
const arrayTableNameWithLocales = getTableName({
adapter,
config: field,
locales: true,
parentTableName: currentTableName,
prefix: `${currentTableName}_${path}`,
})
if (adapter.tables[arrayTableNameWithLocales]) withArray.with._locales = _locales
currentArgs.with[`${path}${field.name}`] = withArray
@@ -132,9 +142,12 @@ export const traverseFields = ({
with: {},
}
const tableName = adapter.tableNameMap.get(
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
)
const tableName = getTableName({
adapter,
config: block,
parentTableName: topLevelTableName,
prefix: `${topLevelTableName}_blocks_`,
})
if (adapter.tables[`${tableName}${adapter.localesSuffix}`]) {
withBlock.with._locales = _locales

View File

@@ -1,18 +1,19 @@
import type { FindGlobal } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const findGlobal: FindGlobal = async function findGlobal(
this: PostgresAdapter,
{ slug, locale, req, where },
) {
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))
const tableName = getTableName({
adapter: this,
config: globalConfig,
})
const {
docs: [doc],

View File

@@ -1,12 +1,12 @@
import type { FindGlobalVersions } from 'payload/database'
import type { PayloadRequestWithData, SanitizedGlobalConfig } from 'payload/types'
import type { PayloadRequest, SanitizedGlobalConfig } from 'payload/types'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
this: PostgresAdapter,
@@ -16,7 +16,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
locale,
page,
pagination,
req = {} as PayloadRequestWithData,
req = {} as PayloadRequest,
skip,
sort: sortArg,
where,
@@ -27,10 +27,11 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
)
const sort = typeof sortArg === 'string' ? sortArg : '-createdAt'
const tableName = this.tableNameMap.get(
`_${toSnakeCase(globalConfig.slug)}${this.versionsSuffix}`,
)
const tableName = getTableName({
adapter: this,
config: globalConfig,
versions: true,
})
const fields = buildVersionGlobalFields(globalConfig)
return findMany({

View File

@@ -1,19 +1,20 @@
import type { FindOneArgs } from 'payload/database'
import type { PayloadRequestWithData, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export async function findOne<T extends TypeWithID>(
this: PostgresAdapter,
{ collection, locale, req = {} as PayloadRequestWithData, where }: FindOneArgs,
{ collection, locale, req = {} as PayloadRequest, where }: FindOneArgs,
): Promise<T> {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
const tableName = getTableName({
adapter: this,
config: collectionConfig,
})
const { docs } = await findMany({
adapter: this,

View File

@@ -1,12 +1,12 @@
import type { FindVersions } from 'payload/database'
import type { PayloadRequestWithData, SanitizedCollectionConfig } from 'payload/types'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const findVersions: FindVersions = async function findVersions(
this: PostgresAdapter,
@@ -16,7 +16,7 @@ export const findVersions: FindVersions = async function findVersions(
locale,
page,
pagination,
req = {} as PayloadRequestWithData,
req = {} as PayloadRequest,
skip,
sort: sortArg,
where,
@@ -25,10 +25,11 @@ export const findVersions: FindVersions = async function findVersions(
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort
const tableName = this.tableNameMap.get(
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
)
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
return findMany({

View File

@@ -8,7 +8,6 @@ import { createDatabaseAdapter } from 'payload/database'
import type { Args, PostgresAdapter } from './types.js'
import { connect } from './connect.js'
import { count } from './count.js'
import { create } from './create.js'
import { createGlobal } from './createGlobal.js'
import { createGlobalVersion } from './createGlobalVersion.js'
@@ -44,18 +43,19 @@ export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
export { sql } from 'drizzle-orm'
export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter> {
const postgresIDType = args.idType || 'serial'
const payloadIDType = postgresIDType === 'serial' ? 'number' : 'text'
function adapter({ payload }: { payload: Payload }) {
const migrationDir = findMigrationDir(args.migrationDir)
const idType = args.idType || 'serial'
return createDatabaseAdapter<PostgresAdapter>({
name: 'postgres',
// Postgres-specific
blockTableNames: {},
drizzle: undefined,
enums: {},
fieldConstraints: {},
idType: postgresIDType,
idType,
localesSuffix: args.localesSuffix || '_locales',
logger: args.logger,
pgSchema: undefined,
@@ -67,7 +67,6 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
schema: {},
schemaName: args.schemaName,
sessions: {},
tableNameMap: new Map<string, string>(),
tables: {},
versionsSuffix: args.versionsSuffix || '_v',
@@ -75,13 +74,15 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
beginTransaction,
commitTransaction,
connect,
count,
create,
createGlobal,
createGlobalVersion,
createMigration,
createVersion,
defaultIDType: payloadIDType,
/**
* This represents how a default ID is treated in Payload as were a field type
*/
defaultIDType: idType === 'serial' ? 'number' : 'text',
deleteMany,
deleteOne,
deleteVersions,
@@ -110,7 +111,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
}
return {
defaultIDType: payloadIDType,
defaultIDType: 'number',
init: adapter,
}
}

View File

@@ -8,7 +8,7 @@ import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/
import type { PostgresAdapter } from './types.js'
import { buildTable } from './schema/build.js'
import { createTableName } from './schema/createTableName.js'
import { getTableName } from './schema/getTableName.js'
export const init: Init = function init(this: PostgresAdapter) {
if (this.schemaName) {
@@ -25,7 +25,7 @@ export const init: Init = function init(this: PostgresAdapter) {
}
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const tableName = createTableName({
const tableName = getTableName({
adapter: this,
config: collection,
})
@@ -44,11 +44,10 @@ export const init: Init = function init(this: PostgresAdapter) {
})
if (collection.versions) {
const versionsTableName = createTableName({
const versionsTableName = getTableName({
adapter: this,
config: collection,
versions: true,
versionsCustomName: true,
})
const versionFields = buildVersionCollectionFields(collection)
@@ -68,7 +67,7 @@ export const init: Init = function init(this: PostgresAdapter) {
})
this.payload.config.globals.forEach((global) => {
const tableName = createTableName({ adapter: this, config: global })
const tableName = getTableName({ adapter: this, config: global })
buildTable({
adapter: this,
@@ -84,12 +83,7 @@ export const init: Init = function init(this: PostgresAdapter) {
})
if (global.versions) {
const versionsTableName = createTableName({
adapter: this,
config: global,
versions: true,
versionsCustomName: true,
})
const versionsTableName = getTableName({ adapter: this, config: global, versions: true })
const versionFields = buildVersionGlobalFields(global)
buildTable({

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import type { Payload } from 'payload'
import type { Migration } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import { createRequire } from 'module'
import {
@@ -88,7 +88,7 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
const { generateDrizzleJson } = require('drizzle-kit/payload')
const start = Date.now()
const req = { payload } as PayloadRequestWithData
const req = { payload } as PayloadRequest
payload.logger.info({ msg: `Migrating: ${migration.name}` })

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import {
commitTransaction,
@@ -40,7 +40,7 @@ export async function migrateDown(this: PostgresAdapter): Promise<void> {
}
const start = Date.now()
const req = { payload } as PayloadRequestWithData
const req = { payload } as PayloadRequest
try {
payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` })

View File

@@ -1,4 +1,4 @@
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import { sql } from 'drizzle-orm'
import {
@@ -56,7 +56,7 @@ export async function migrateFresh(
msg: `Found ${migrationFiles.length} migration files.`,
})
const req = { payload } as PayloadRequestWithData
const req = { payload } as PayloadRequest
// Run all migrate up
for (const migration of migrationFiles) {
payload.logger.info({ msg: `Migrating: ${migration.name}` })

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import {
commitTransaction,
@@ -34,7 +34,7 @@ export async function migrateRefresh(this: PostgresAdapter) {
msg: `Rolling back batch ${latestBatch} consisting of ${existingMigrations.length} migration(s).`,
})
const req = { payload } as PayloadRequestWithData
const req = { payload } as PayloadRequest
// Reverse order of migrations to rollback
existingMigrations.reverse()

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import {
commitTransaction,
@@ -27,7 +27,7 @@ export async function migrateReset(this: PostgresAdapter): Promise<void> {
return
}
const req = { payload } as PayloadRequestWithData
const req = { payload } as PayloadRequest
// Rollback all migrations in order
for (const migration of existingMigrations) {

View File

@@ -14,6 +14,8 @@ import { v4 as uuid } from 'uuid'
import type { GenericColumn, GenericTable, PostgresAdapter } from '../types.js'
import type { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery.js'
import { getTableName } from '../schema/getTableName.js'
type Constraint = {
columnName: string
table: GenericTable | PgTableWithColumns<any>
@@ -183,7 +185,13 @@ export const getTableColumnFromPath = ({
case 'group': {
if (locale && field.localized && adapter.payload.config.localization) {
newTableName = `${tableName}${adapter.localesSuffix}`
newTableName = getTableName({
adapter,
config: field,
locales: true,
parentTableName: tableName,
prefix: `${tableName}_`,
})
joins[tableName] = eq(
adapter.tables[tableName].id,
@@ -217,87 +225,13 @@ export const getTableColumnFromPath = ({
})
}
case 'select': {
if (field.hasMany) {
const newTableName = adapter.tableNameMap.get(
`${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`,
)
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
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,
})
}
} else {
joins[newTableName] = eq(
adapter.tables[tableName].id,
adapter.tables[newTableName].parent,
)
}
return {
columnName: 'value',
constraints,
field,
table: adapter.tables[newTableName],
}
}
break
}
case 'text':
case 'number': {
if (field.hasMany) {
let tableType = 'texts'
let columnName = 'text'
if (field.type === 'number') {
tableType = 'numbers'
columnName = 'number'
}
newTableName = `${tableName}_${tableType}`
const joinConstraints = [
eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
eq(adapter.tables[newTableName].path, `${constraintPath}${field.name}`),
]
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
...joinConstraints,
eq(adapter.tables[newTableName]._locale, locale),
)
if (locale !== 'all') {
constraints.push({
columnName: 'locale',
table: adapter.tables[newTableName],
value: locale,
})
}
} else {
joins[newTableName] = and(...joinConstraints)
}
return {
columnName,
constraints,
field,
table: adapter.tables[newTableName],
}
}
break
}
case 'array': {
newTableName = adapter.tableNameMap.get(
`${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`,
)
newTableName = getTableName({
adapter,
config: field,
parentTableName: `${tableName}_${tableNameSuffix}`,
prefix: `${tableName}_${tableNameSuffix}`,
})
constraintPath = `${constraintPath}${field.name}.%.`
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
@@ -344,11 +278,12 @@ export const getTableColumnFromPath = ({
const blockTypes = Array.isArray(value) ? value : [value]
blockTypes.forEach((blockType) => {
const block = field.blocks.find((block) => block.slug === blockType)
newTableName = adapter.tableNameMap.get(
`${tableName}_blocks_${toSnakeCase(block.slug)}`,
)
newTableName = getTableName({
adapter,
config: block,
parentTableName: tableName,
prefix: `${tableName}_blocks_`,
})
joins[newTableName] = eq(
adapter.tables[tableName].id,
adapter.tables[newTableName]._parentID,
@@ -368,9 +303,13 @@ export const getTableColumnFromPath = ({
}
const hasBlockField = field.blocks.some((block) => {
newTableName = adapter.tableNameMap.get(`${tableName}_blocks_${toSnakeCase(block.slug)}`)
newTableName = getTableName({
adapter,
config: block,
parentTableName: tableName,
prefix: `${tableName}_blocks_`,
})
constraintPath = `${constraintPath}${field.name}.%.`
let result
const blockConstraints = []
const blockSelectFields = {}
@@ -477,9 +416,10 @@ export const getTableColumnFromPath = ({
if (typeof field.relationTo === 'string') {
const relationshipConfig = adapter.payload.collections[field.relationTo].config
newTableName = adapter.tableNameMap.get(toSnakeCase(relationshipConfig.slug))
newTableName = getTableName({
adapter,
config: relationshipConfig,
})
// parent to relationship join table
relationshipFields = relationshipConfig.fields
@@ -499,13 +439,13 @@ export const getTableColumnFromPath = ({
}
}
} else if (newCollectionPath === 'value') {
const tableColumnsNames = field.relationTo.map((relationTo) => {
const relationTableName = adapter.tableNameMap.get(
toSnakeCase(adapter.payload.collections[relationTo].config.slug),
)
return `"${aliasRelationshipTableName}"."${relationTableName}_id"`
})
const tableColumnsNames = field.relationTo.map(
(relationTo) =>
`"${aliasRelationshipTableName}"."${getTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
})}_id"`,
)
return {
constraints,
field,
@@ -545,41 +485,43 @@ export const getTableColumnFromPath = ({
value,
})
}
}
if (fieldAffectsData(field)) {
if (field.localized && adapter.payload.config.localization) {
// If localized, we go to localized table and set aliasTable to undefined
// so it is not picked up below to be used as targetTable
newTableName = `${tableName}${adapter.localesSuffix}`
default: {
if (fieldAffectsData(field)) {
if (field.localized && adapter.payload.config.localization) {
// If localized, we go to localized table and set aliasTable to undefined
// so it is not picked up below to be used as targetTable
newTableName = `${tableName}${adapter.localesSuffix}`
const parentTable = aliasTable || adapter.tables[tableName]
const parentTable = aliasTable || adapter.tables[tableName]
joins[newTableName] = eq(parentTable.id, adapter.tables[newTableName]._parentID)
joins[newTableName] = eq(parentTable.id, adapter.tables[newTableName]._parentID)
aliasTable = undefined
aliasTable = undefined
if (locale !== 'all') {
constraints.push({
columnName: '_locale',
table: adapter.tables[newTableName],
value: locale,
})
if (locale !== 'all') {
constraints.push({
columnName: '_locale',
table: adapter.tables[newTableName],
value: locale,
})
}
}
const targetTable = aliasTable || adapter.tables[newTableName]
selectFields[`${newTableName}.${columnPrefix}${field.name}`] =
targetTable[`${columnPrefix}${field.name}`]
return {
columnName: `${columnPrefix}${field.name}`,
constraints,
field,
pathSegments,
table: targetTable,
}
}
}
const targetTable = aliasTable || adapter.tables[newTableName]
selectFields[`${newTableName}.${columnPrefix}${field.name}`] =
targetTable[`${columnPrefix}${field.name}`]
return {
columnName: `${columnPrefix}${field.name}`,
constraints,
field,
pathSegments,
table: targetTable,
}
}
}

View File

@@ -1,30 +1,27 @@
import type { PayloadRequestWithData, SanitizedCollectionConfig } from 'payload/types'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { type QueryDrafts, combineQueries } from 'payload/database'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const queryDrafts: QueryDrafts = async function queryDrafts(
this: PostgresAdapter,
{
collection,
limit,
locale,
page = 1,
pagination,
req = {} as PayloadRequestWithData,
sort,
where,
},
) {
export const queryDrafts: QueryDrafts = async function queryDrafts({
collection,
limit,
locale,
page = 1,
pagination,
req = {} as PayloadRequest,
sort,
where,
}) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
)
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
const combinedWhere = combineQueries({ latest: { equals: true } }, where)

View File

@@ -1,7 +1,6 @@
/* eslint-disable no-param-reassign */
import type { Relation } from 'drizzle-orm'
import type {
ForeignKeyBuilder,
IndexBuilder,
PgColumnBuilder,
PgTableWithColumns,
@@ -10,34 +9,20 @@ import type {
import type { Field } from 'payload/types'
import { relations } from 'drizzle-orm'
import {
foreignKey,
index,
integer,
numeric,
serial,
timestamp,
unique,
varchar,
} from 'drizzle-orm/pg-core'
import toSnakeCase from 'to-snake-case'
import { index, integer, numeric, serial, timestamp, unique, varchar } from 'drizzle-orm/pg-core'
import { fieldAffectsData } from 'payload/types'
import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types.js'
import { createTableName } from './createTableName.js'
import { getTableName } from './getTableName.js'
import { parentIDColumnMap } from './parentIDColumnMap.js'
import { setColumnID } from './setColumnID.js'
import { traverseFields } from './traverseFields.js'
export type BaseExtraConfig = Record<
string,
(cols: GenericColumns) => ForeignKeyBuilder | IndexBuilder | UniqueConstraintBuilder
>
type Args = {
adapter: PostgresAdapter
baseColumns?: Record<string, PgColumnBuilder>
baseExtraConfig?: BaseExtraConfig
baseExtraConfig?: Record<string, (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder>
buildNumbers?: boolean
buildRelationships?: boolean
buildTexts?: boolean
@@ -81,6 +66,13 @@ export const buildTable = ({
const columns: Record<string, PgColumnBuilder> = baseColumns
const indexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
let hasLocalizedField = false
let hasLocalizedRelationshipField = false
let hasManyTextField: 'index' | boolean = false
let hasManyNumberField: 'index' | boolean = false
let hasLocalizedManyTextField = false
let hasLocalizedManyNumberField = false
const localesColumns: Record<string, PgColumnBuilder> = {}
const localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
let localesTable: GenericTable | PgTableWithColumns<any>
@@ -97,7 +89,7 @@ export const buildTable = ({
const idColType: IDType = setColumnID({ adapter, columns, fields })
const {
;({
hasLocalizedField,
hasLocalizedManyNumberField,
hasLocalizedManyTextField,
@@ -124,7 +116,7 @@ export const buildTable = ({
rootTableIDColType: rootTableIDColType || idColType,
rootTableName,
versions,
})
}))
if (timestamps) {
columns.createdAt = timestamp('created_at', {
@@ -149,12 +141,10 @@ export const buildTable = ({
return config
}, {})
const result = Object.entries(indexes).reduce((acc, [colName, func]) => {
return Object.entries(indexes).reduce((acc, [colName, func]) => {
acc[colName] = func(cols)
return acc
}, extraConfig)
return result
})
adapter.tables[tableName] = table
@@ -163,7 +153,9 @@ export const buildTable = ({
const localeTableName = `${tableName}${adapter.localesSuffix}`
localesColumns.id = serial('id').primaryKey()
localesColumns._locale = adapter.enums.enum__locales('_locale').notNull()
localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id').notNull()
localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull()
localesTable = adapter.pgSchema.table(localeTableName, localesColumns, (cols) => {
return Object.entries(localesIndexes).reduce(
@@ -176,11 +168,6 @@ export const buildTable = ({
cols._locale,
cols._parentID,
),
_parentIdFk: foreignKey({
name: `${localeTableName}_parent_id_fk`,
columns: [cols._parentID],
foreignColumns: [table.id],
}).onDelete('cascade'),
},
)
})
@@ -202,7 +189,9 @@ export const buildTable = ({
const columns: Record<string, PgColumnBuilder> = {
id: serial('id').primaryKey(),
order: integer('order').notNull(),
parent: parentIDColumnMap[idColType]('parent_id').notNull(),
parent: parentIDColumnMap[idColType]('parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull(),
path: varchar('path').notNull(),
text: varchar('text'),
}
@@ -212,24 +201,19 @@ export const buildTable = ({
}
textsTable = adapter.pgSchema.table(textsTableName, columns, (cols) => {
const config: Record<string, ForeignKeyBuilder | IndexBuilder> = {
const indexes: Record<string, IndexBuilder> = {
orderParentIdx: index(`${textsTableName}_order_parent_idx`).on(cols.order, cols.parent),
parentFk: foreignKey({
name: `${textsTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [table.id],
}).onDelete('cascade'),
}
if (hasManyTextField === 'index') {
config.text_idx = index(`${textsTableName}_text_idx`).on(cols.text)
indexes.text_idx = index(`${textsTableName}_text_idx`).on(cols.text)
}
if (hasLocalizedManyTextField) {
config.localeParent = index(`${textsTableName}_locale_parent`).on(cols.locale, cols.parent)
indexes.localeParent = index(`${textsTableName}_locale_parent`).on(cols.locale, cols.parent)
}
return config
return indexes
})
adapter.tables[textsTableName] = textsTable
@@ -250,7 +234,9 @@ export const buildTable = ({
id: serial('id').primaryKey(),
number: numeric('number'),
order: integer('order').notNull(),
parent: parentIDColumnMap[idColType]('parent_id').notNull(),
parent: parentIDColumnMap[idColType]('parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull(),
path: varchar('path').notNull(),
}
@@ -259,27 +245,22 @@ export const buildTable = ({
}
numbersTable = adapter.pgSchema.table(numbersTableName, columns, (cols) => {
const config: Record<string, ForeignKeyBuilder | IndexBuilder> = {
const indexes: Record<string, IndexBuilder> = {
orderParentIdx: index(`${numbersTableName}_order_parent_idx`).on(cols.order, cols.parent),
parentFk: foreignKey({
name: `${numbersTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [table.id],
}).onDelete('cascade'),
}
if (hasManyNumberField === 'index') {
config.numberIdx = index(`${numbersTableName}_number_idx`).on(cols.number)
indexes.numberIdx = index(`${numbersTableName}_number_idx`).on(cols.number)
}
if (hasLocalizedManyNumberField) {
config.localeParent = index(`${numbersTableName}_locale_parent`).on(
indexes.localeParent = index(`${numbersTableName}_locale_parent`).on(
cols.locale,
cols.parent,
)
}
return config
return indexes
})
adapter.tables[numbersTableName] = numbersTable
@@ -299,7 +280,9 @@ export const buildTable = ({
const relationshipColumns: Record<string, PgColumnBuilder> = {
id: serial('id').primaryKey(),
order: integer('order'),
parent: parentIDColumnMap[idColType]('parent_id').notNull(),
parent: parentIDColumnMap[idColType]('parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull(),
path: varchar('path').notNull(),
}
@@ -307,59 +290,36 @@ export const buildTable = ({
relationshipColumns.locale = adapter.enums.enum__locales('locale')
}
const relationExtraConfig: BaseExtraConfig = {}
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
relationships.forEach((relationTo) => {
const relationshipConfig = adapter.payload.collections[relationTo].config
const formattedRelationTo = createTableName({
const formattedRelationTo = getTableName({
adapter,
config: relationshipConfig,
throwValidationError: true,
})
let colType = adapter.idType === 'uuid' ? 'uuid' : 'integer'
const relatedCollectionCustomIDType =
adapter.payload.collections[relationshipConfig.slug]?.customIDType
if (relatedCollectionCustomIDType === 'number') colType = 'numeric'
if (relatedCollectionCustomIDType === 'text') colType = 'varchar'
const relatedCollectionCustomID = relationshipConfig.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
if (relatedCollectionCustomID?.type === 'number') colType = 'numeric'
if (relatedCollectionCustomID?.type === 'text') colType = 'varchar'
relationshipColumns[`${relationTo}ID`] = parentIDColumnMap[colType](
`${formattedRelationTo}_id`,
)
relationExtraConfig[`${relationTo}IdFk`] = (cols) =>
foreignKey({
name: `${relationshipsTableName}_${toSnakeCase(relationTo)}_fk`,
columns: [cols[`${relationTo}ID`]],
foreignColumns: [adapter.tables[formattedRelationTo].id],
}).onDelete('cascade')
).references(() => adapter.tables[formattedRelationTo].id, { onDelete: 'cascade' })
})
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
relationshipsTable = adapter.pgSchema.table(
relationshipsTableName,
relationshipColumns,
(cols) => {
const result: Record<string, ForeignKeyBuilder | IndexBuilder> = Object.entries(
relationExtraConfig,
).reduce(
(config, [key, func]) => {
config[key] = func(cols)
return config
},
{
order: index(`${relationshipsTableName}_order_idx`).on(cols.order),
parentFk: foreignKey({
name: `${relationshipsTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [table.id],
}).onDelete('cascade'),
parentIdx: index(`${relationshipsTableName}_parent_idx`).on(cols.parent),
pathIdx: index(`${relationshipsTableName}_path_idx`).on(cols.path),
},
)
const result: Record<string, unknown> = {
order: index(`${relationshipsTableName}_order_idx`).on(cols.order),
parentIdx: index(`${relationshipsTableName}_parent_idx`).on(cols.parent),
pathIdx: index(`${relationshipsTableName}_path_idx`).on(cols.path),
}
if (hasLocalizedRelationshipField) {
result.localeIdx = index(`${relationshipsTableName}_locale_idx`).on(cols.locale)
@@ -381,7 +341,7 @@ export const buildTable = ({
}
relationships.forEach((relationTo) => {
const relatedTableName = createTableName({
const relatedTableName = getTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
throwValidationError: true,

View File

@@ -14,59 +14,53 @@ type Args = {
name?: string
slug?: string
}
/** Localized tables need to be given the locales suffix */
locales?: boolean
/** For nested tables passed for the user custom dbName functions to handle their own iterations */
parentTableName?: string
/** For sub tables (array for example) this needs to include the parentTableName */
prefix?: string
/** Adds the relationships suffix */
relationships?: boolean
/** For tables based on fields that could have both enumName and dbName (ie: select with hasMany), default: 'dbName' */
target?: 'dbName' | 'enumName'
throwValidationError?: boolean
/** Adds the versions suffix to the default table name - should only be used on the base collection to avoid duplicate suffixing */
/** Adds the versions suffix, should only be used on the base collection to duplicate suffixing */
versions?: boolean
/** Adds the versions suffix to custom dbName only - this is used while creating blocks / selects / arrays / etc */
versionsCustomName?: boolean
}
/**
* Used to name database enums and tables
* Returns the table or enum name for a given entity
*/
export const createTableName = ({
export const getTableName = ({
adapter,
config: { name, slug },
config,
locales = false,
parentTableName,
prefix = '',
relationships = false,
target = 'dbName',
throwValidationError = false,
versions = false,
versionsCustomName = false,
}: Args): string => {
let customNameDefinition = config[target]
let result: string
let custom = config[target]
let defaultTableName = `${prefix}${toSnakeCase(name ?? slug)}`
if (versions) defaultTableName = `_${defaultTableName}${adapter.versionsSuffix}`
let customTableNameResult: string
if (!customNameDefinition && target === 'enumName') {
customNameDefinition = config['dbName']
if (!custom && target === 'enumName') {
custom = config['dbName']
}
if (customNameDefinition) {
customTableNameResult =
typeof customNameDefinition === 'function'
? customNameDefinition({ tableName: parentTableName })
: customNameDefinition
if (versionsCustomName)
customTableNameResult = `_${customTableNameResult}${adapter.versionsSuffix}`
if (custom) {
result = typeof custom === 'function' ? custom({ tableName: parentTableName }) : custom
} else {
result = `${prefix}${toSnakeCase(name ?? slug)}`
}
const result = customTableNameResult || defaultTableName
adapter.tableNameMap.set(defaultTableName, result)
if (locales) result = `${result}${adapter.localesSuffix}`
if (versions) result = `_${result}${adapter.versionsSuffix}`
if (relationships) result = `${result}${adapter.relationshipsSuffix}`
if (!throwValidationError) {
return result
@@ -77,6 +71,5 @@ export const createTableName = ({
`Exceeded max identifier length for table or enum name of 63 characters. Invalid name: ${result}`,
)
}
return result
}

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */
import type { Relation } from 'drizzle-orm'
import type { IndexBuilder, PgColumnBuilder } from 'drizzle-orm/pg-core'
import type { IndexBuilder, PgColumnBuilder, UniqueConstraintBuilder } from 'drizzle-orm/pg-core'
import type { Field, TabAsField } from 'payload/types'
import { relations } from 'drizzle-orm'
@@ -9,7 +9,6 @@ import {
PgUUIDBuilder,
PgVarcharBuilder,
boolean,
foreignKey,
index,
integer,
jsonb,
@@ -24,12 +23,11 @@ import { fieldAffectsData, optionIsObject } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { GenericColumns, IDType, PostgresAdapter } from '../types.js'
import type { BaseExtraConfig } from './build.js'
import { hasLocalesTable } from '../utilities/hasLocalesTable.js'
import { buildTable } from './build.js'
import { createIndex } from './createIndex.js'
import { createTableName } from './createTableName.js'
import { getTableName } from './getTableName.js'
import { idToUUID } from './idToUUID.js'
import { parentIDColumnMap } from './parentIDColumnMap.js'
import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdentical.js'
@@ -223,13 +221,14 @@ export const traverseFields = ({
case 'radio':
case 'select': {
const enumName = createTableName({
const enumName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `enum_${newTableName}_`,
target: 'enumName',
throwValidationError,
versions,
})
adapter.enums[enumName] = pgEnum(
@@ -244,27 +243,27 @@ export const traverseFields = ({
)
if (field.type === 'select' && field.hasMany) {
const selectTableName = createTableName({
const selectTableName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
versions,
})
const baseColumns: Record<string, PgColumnBuilder> = {
order: integer('order').notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id').notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id')
.references(() => adapter.tables[parentTableName].id, { onDelete: 'cascade' })
.notNull(),
value: adapter.enums[enumName]('value'),
}
const baseExtraConfig: BaseExtraConfig = {
const baseExtraConfig: Record<
string,
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
> = {
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order),
parentFk: (cols) =>
foreignKey({
name: `${selectTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [adapter.tables[parentTableName].id],
}),
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
}
@@ -317,28 +316,25 @@ export const traverseFields = ({
case 'array': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const arrayTableName = createTableName({
const arrayTableName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
versionsCustomName: versions,
})
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id').notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id')
.references(() => adapter.tables[parentTableName].id, { onDelete: 'cascade' })
.notNull(),
}
const baseExtraConfig: BaseExtraConfig = {
const baseExtraConfig: Record<
string,
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
> = {
_orderIdx: (cols) => index(`${arrayTableName}_order_idx`).on(cols._order),
_parentIDFk: (cols) =>
foreignKey({
name: `${arrayTableName}_parent_id_fk`,
columns: [cols['_parentID']],
foreignColumns: [adapter.tables[parentTableName].id],
}).onDelete('cascade'),
_parentIDIdx: (cols) => index(`${arrayTableName}_parent_id_idx`).on(cols._parentID),
}
@@ -406,30 +402,28 @@ export const traverseFields = ({
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
field.blocks.forEach((block) => {
const blockTableName = createTableName({
const blockTableName = getTableName({
adapter,
config: block,
parentTableName: rootTableName,
prefix: `${rootTableName}_blocks_`,
throwValidationError,
versionsCustomName: versions,
})
if (!adapter.tables[blockTableName]) {
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
_parentID: parentIDColumnMap[rootTableIDColType]('_parent_id').notNull(),
_parentID: parentIDColumnMap[rootTableIDColType]('_parent_id')
.references(() => adapter.tables[rootTableName].id, { onDelete: 'cascade' })
.notNull(),
_path: text('_path').notNull(),
}
const baseExtraConfig: BaseExtraConfig = {
const baseExtraConfig: Record<
string,
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
> = {
_orderIdx: (cols) => index(`${blockTableName}_order_idx`).on(cols._order),
_parentIDIdx: (cols) => index(`${blockTableName}_parent_id_idx`).on(cols._parentID),
_parentIdFk: (cols) =>
foreignKey({
name: `${blockTableName}_parent_id_fk`,
columns: [cols._parentID],
foreignColumns: [adapter.tables[rootTableName].id],
}).onDelete('cascade'),
_pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path),
}
@@ -499,9 +493,9 @@ export const traverseFields = ({
localized: field.localized,
rootTableName,
table: adapter.tables[blockTableName],
tableLocales: adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
})
}
adapter.blockTableNames[`${rootTableName}.${toSnakeCase(block.slug)}`] = blockTableName
rootRelationsToBuild.set(`_blocks_${block.slug}`, blockTableName)
})
@@ -664,7 +658,7 @@ export const traverseFields = ({
indexes,
localesColumns,
localesIndexes,
newTableName,
newTableName: parentTableName,
parentTableName,
relationsToBuild,
relationships,

View File

@@ -10,13 +10,9 @@ type Args = {
localized: boolean
rootTableName: string
table: GenericTable
tableLocales?: GenericTable
}
const getFlattenedFieldNames = (
fields: Field[],
prefix: string = '',
): { localized?: boolean; name: string }[] => {
const getFlattenedFieldNames = (fields: Field[], prefix: string = ''): string[] => {
return fields.reduce((fieldsToUse, field) => {
let fieldPrefix = prefix
@@ -28,7 +24,7 @@ const getFlattenedFieldNames = (
}
if (fieldHasSubFields(field)) {
fieldPrefix = 'name' in field ? `${prefix}${field.name}_` : prefix
fieldPrefix = 'name' in field ? `${prefix}${field.name}.` : prefix
return [...fieldsToUse, ...getFlattenedFieldNames(field.fields, fieldPrefix)]
}
@@ -36,7 +32,7 @@ const getFlattenedFieldNames = (
return [
...fieldsToUse,
...field.tabs.reduce((tabFields, tab) => {
fieldPrefix = 'name' in tab ? `${prefix}_${tab.name}` : prefix
fieldPrefix = 'name' in tab ? `${prefix}.${tab.name}` : prefix
return [
...tabFields,
...(tabHasName(tab)
@@ -48,13 +44,7 @@ const getFlattenedFieldNames = (
}
if (fieldAffectsData(field)) {
return [
...fieldsToUse,
{
name: `${fieldPrefix}${field.name}`,
localized: field.localized,
},
]
return [...fieldsToUse, `${fieldPrefix?.replace('.', '_') || ''}${field.name}`]
}
return fieldsToUse
@@ -66,30 +56,22 @@ export const validateExistingBlockIsIdentical = ({
localized,
rootTableName,
table,
tableLocales,
}: Args): void => {
const fieldNames = getFlattenedFieldNames(block.fields)
const missingField =
// ensure every field from the config is in the matching table
fieldNames.find(({ name, localized }) => {
const fieldTable = localized && tableLocales ? tableLocales : table
return Object.keys(fieldTable).indexOf(name) === -1
}) ||
fieldNames.find((name) => Object.keys(table).indexOf(name) === -1) ||
// ensure every table column is matched for every field from the config
Object.keys(table).find((fieldName) => {
if (!['_locale', '_order', '_parentID', '_path', '_uuid'].includes(fieldName)) {
return fieldNames.findIndex((field) => field.name) === -1
return fieldNames.indexOf(fieldName) === -1
}
})
if (missingField) {
throw new InvalidConfiguration(
`The table ${rootTableName} has multiple blocks with slug ${
block.slug
}, but the schemas do not match. One block includes the field ${
typeof missingField === 'string' ? missingField : missingField.name
}, while the other block does not.`,
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One block includes the field ${missingField}, while the other block does not.`,
)
}

View File

@@ -45,7 +45,6 @@ export const transformArray = ({
texts,
}: Args) => {
const newRows: ArrayRowToInsert[] = []
const hasUUID = adapter.tables[arrayTableName]._uuid
if (isArrayOfRows(data)) {

View File

@@ -61,7 +61,7 @@ export const transformBlocks = ({
if (field.localized && locale) newRow.row._locale = locale
const blockTableName = adapter.tableNameMap.get(`${baseTableName}_blocks_${blockType}`)
const blockTableName = `${baseTableName}_blocks_${blockType}`
const hasUUID = adapter.tables[blockTableName]._uuid

View File

@@ -94,7 +94,7 @@ export const traverseFields = ({
}
if (field.type === 'array') {
const arrayTableName = adapter.tableNameMap.get(`${parentTableName}_${columnName}`)
const arrayTableName = `${parentTableName}_${columnName}`
if (!arrays[arrayTableName]) arrays[arrayTableName] = []
@@ -458,7 +458,7 @@ export const traverseFields = ({
}
if (field.type === 'select' && field.hasMany) {
const selectTableName = adapter.tableNameMap.get(`${parentTableName}_${columnName}`)
const selectTableName = `${parentTableName}_${columnName}`
if (!selects[selectTableName]) selects[selectTableName] = []
if (field.localized) {

View File

@@ -17,7 +17,7 @@ import type {
import type { PgTableFn } from 'drizzle-orm/pg-core/table'
import type { Payload } from 'payload'
import type { BaseDatabaseAdapter } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import type { Pool, PoolConfig } from 'pg'
export type DrizzleDB = NodePgDatabase<Record<string, unknown>>
@@ -61,6 +61,10 @@ export type DrizzleTransaction = PgTransaction<
>
export type PostgresAdapter = BaseDatabaseAdapter & {
/**
* Used internally to map the block name to the table name
*/
blockTableNames: Record<string, string>
drizzle: DrizzleDB
enums: Record<string, GenericEnum>
/**
@@ -86,7 +90,6 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
resolve: () => Promise<void>
}
}
tableNameMap: Map<string, string>
tables: Record<string, GenericTable | PgTableWithColumns<any>>
versionsSuffix?: string
}
@@ -95,8 +98,8 @@ export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar'
export type PostgresAdapterResult = (args: { payload: Payload }) => PostgresAdapter
export type MigrateUpArgs = { payload: Payload; req?: Partial<PayloadRequestWithData> }
export type MigrateDownArgs = { payload: Payload; req?: Partial<PayloadRequestWithData> }
export type MigrateUpArgs = { payload: Payload; req?: Partial<PayloadRequest> }
export type MigrateDownArgs = { payload: Payload; req?: Partial<PayloadRequest> }
declare module 'payload' {
export interface DatabaseAdapter

View File

@@ -1,11 +1,10 @@
import type { UpdateOne } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export const updateOne: UpdateOne = async function updateOne(
@@ -14,7 +13,10 @@ export const updateOne: UpdateOne = async function updateOne(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const tableName = getTableName({
adapter: this,
config: collection,
})
const whereToUse = whereArg || { id: { equals: id } }
let idToUpdate = id

View File

@@ -1,19 +1,21 @@
import type { UpdateGlobalArgs } from 'payload/database'
import type { PayloadRequestWithData, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function updateGlobal<T extends TypeWithID>(
this: PostgresAdapter,
{ slug, data, req = {} as PayloadRequestWithData }: UpdateGlobalArgs,
{ slug, data, req = {} as PayloadRequest }: UpdateGlobalArgs,
): Promise<T> {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))
const tableName = getTableName({
adapter: this,
config: globalConfig,
})
const existingGlobal = await db.query[tableName].findFirst({})

View File

@@ -1,12 +1,12 @@
import type { TypeWithVersion, UpdateGlobalVersionArgs } from 'payload/database'
import type { PayloadRequestWithData, SanitizedGlobalConfig, TypeWithID } from 'payload/types'
import type { PayloadRequest, SanitizedGlobalConfig, TypeWithID } from 'payload/types'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function updateGlobalVersion<T extends TypeWithID>(
@@ -15,7 +15,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
id,
global,
locale,
req = {} as PayloadRequestWithData,
req = {} as PayloadRequest,
versionData,
where: whereArg,
}: UpdateGlobalVersionArgs<T>,
@@ -25,11 +25,11 @@ export async function updateGlobalVersion<T extends TypeWithID>(
({ slug }) => slug === global,
)
const whereToUse = whereArg || { id: { equals: id } }
const tableName = this.tableNameMap.get(
`_${toSnakeCase(globalConfig.slug)}${this.versionsSuffix}`,
)
const tableName = getTableName({
adapter: this,
config: globalConfig,
versions: true,
})
const fields = buildVersionGlobalFields(globalConfig)
const { where } = await buildQuery({

View File

@@ -1,12 +1,12 @@
import type { TypeWithVersion, UpdateVersionArgs } from 'payload/database'
import type { PayloadRequestWithData, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function updateVersion<T extends TypeWithID>(
@@ -15,7 +15,7 @@ export async function updateVersion<T extends TypeWithID>(
id,
collection,
locale,
req = {} as PayloadRequestWithData,
req = {} as PayloadRequest,
versionData,
where: whereArg,
}: UpdateVersionArgs<T>,
@@ -23,10 +23,11 @@ export async function updateVersion<T extends TypeWithID>(
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const whereToUse = whereArg || { id: { equals: id } }
const tableName = this.tableNameMap.get(
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
)
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
const { where } = await buildQuery({

View File

@@ -137,7 +137,7 @@ export const upsertRow = async <T extends TypeWithID>({
// //////////////////////////////////
if (localesToInsert.length > 0) {
const localeTable = adapter.tables[`${tableName}${adapter.localesSuffix}`]
const localeTable = adapter.tables[`${tableName}_locales`]
if (operation === 'update') {
await db.delete(localeTable).where(eq(localeTable._parentID, insertedRow.id))
@@ -150,7 +150,7 @@ export const upsertRow = async <T extends TypeWithID>({
// INSERT RELATIONSHIPS
// //////////////////////////////////
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
const relationshipsTableName = `${tableName}_rels`
if (operation === 'update') {
await deleteExistingRowsByPath({
@@ -223,16 +223,15 @@ export const upsertRow = async <T extends TypeWithID>({
if (operation === 'update') {
for (const blockName of rowToInsert.blocksToDelete) {
const blockTableName = adapter.tableNameMap.get(`${tableName}_blocks_${blockName}`)
const blockTableName = `${tableName}_blocks_${blockName}`
const blockTable = adapter.tables[blockTableName]
await db.delete(blockTable).where(eq(blockTable._parentID, insertedRow.id))
}
}
for (const [blockName, blockRows] of Object.entries(blocksToInsert)) {
const blockTableName = adapter.tableNameMap.get(`${tableName}_blocks_${blockName}`)
insertedBlockRows[blockName] = await db
.insert(adapter.tables[blockTableName])
.insert(adapter.tables[`${tableName}_blocks_${blockName}`])
.values(blockRows.map(({ row }) => row))
.returning()
@@ -259,7 +258,7 @@ export const upsertRow = async <T extends TypeWithID>({
if (blockLocaleRowsToInsert.length > 0) {
await db
.insert(adapter.tables[`${blockTableName}${adapter.localesSuffix}`])
.insert(adapter.tables[`${tableName}_blocks_${blockName}_locales`])
.values(blockLocaleRowsToInsert)
.returning()
}

View File

@@ -1,5 +1,5 @@
import type { SQL } from 'drizzle-orm'
import type { Field, PayloadRequestWithData } from 'payload/types'
import type { Field, PayloadRequest } from 'payload/types'
import type { DrizzleDB, GenericColumn, PostgresAdapter } from '../types.js'
@@ -9,7 +9,7 @@ type BaseArgs = {
db: DrizzleDB
fields: Field[]
path?: string
req: PayloadRequestWithData
req: PayloadRequest
tableName: string
}

View File

@@ -1,10 +0,0 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

View File

@@ -1,7 +0,0 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
}

View File

@@ -1,10 +0,0 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

View File

@@ -1,15 +0,0 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true,
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
}
},
"module": {
"type": "es6"
}
}

View File

@@ -1,22 +0,0 @@
MIT License
Copyright (c) 2018-2022 Payload CMS, LLC <info@payloadcms.com>
Portions Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1 +0,0 @@
# Nodemailer Email Adapter

View File

@@ -1,59 +0,0 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.0.0-beta.18",
"description": "Payload Nodemailer Email Adapter",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/email-nodemailer"
},
"license": "MIT",
"homepage": "https://payloadcms.com",
"author": "Payload CMS, Inc.",
"main": "./src/index.ts",
"types": "./src/index.ts",
"type": "module",
"scripts": {
"build": "pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"build:clean": "find . \\( -type d \\( -name build -o -name dist -o -name .cache \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} + && pnpm build",
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"dependencies": {
"nodemailer": "6.9.10"
},
"peerDependencies": {
"payload": "workspace:*"
},
"exports": {
".": {
"import": "./src/index.ts",
"require": "./src/index.ts",
"types": "./src/index.ts"
}
},
"publishConfig": {
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
},
"engines": {
"node": ">=18.20.2"
},
"files": [
"dist"
],
"devDependencies": {
"payload": "workspace:*",
"@types/nodemailer": "6.4.14"
}
}

View File

@@ -1,123 +0,0 @@
/* eslint-disable no-console */
import type { Transporter } from 'nodemailer'
import type SMTPConnection from 'nodemailer/lib/smtp-connection'
import type { EmailAdapter } from 'payload/config'
import nodemailer from 'nodemailer'
import { InvalidConfiguration } from 'payload/errors'
export type NodemailerAdapterArgs = {
defaultFromAddress: string
defaultFromName: string
skipVerify?: boolean
transport?: Transporter
transportOptions?: SMTPConnection.Options
}
type NodemailerAdapter = EmailAdapter<unknown>
/**
* Creates an email adapter using nodemailer
*
* If no email configuration is provided, an ethereal email test account is returned
*/
export const nodemailerAdapter = async (
args?: NodemailerAdapterArgs,
): Promise<NodemailerAdapter> => {
const { defaultFromAddress, defaultFromName, transport } = await buildEmail(args)
const adapter: NodemailerAdapter = () => ({
defaultFromAddress,
defaultFromName,
sendEmail: async (message) => {
return await transport.sendMail({
from: `${defaultFromName} <${defaultFromAddress}>`,
...message,
})
},
})
return adapter
}
async function buildEmail(emailConfig?: NodemailerAdapterArgs): Promise<{
defaultFromAddress: string
defaultFromName: string
transport: Transporter
}> {
if (!emailConfig) {
const transport = await createMockAccount(emailConfig)
if (!transport) throw new InvalidConfiguration('Unable to create Nodemailer test account.')
return {
defaultFromAddress: 'info@payloadcms.com',
defaultFromName: 'Payload',
transport,
}
}
// Create or extract transport
let transport: Transporter
if ('transport' in emailConfig && emailConfig.transport) {
;({ transport } = emailConfig)
} else if ('transportOptions' in emailConfig && emailConfig.transportOptions) {
transport = nodemailer.createTransport(emailConfig.transportOptions)
} else {
transport = await createMockAccount(emailConfig)
}
if (emailConfig.skipVerify !== false) {
await verifyTransport(transport)
}
return {
defaultFromAddress: emailConfig.defaultFromAddress,
defaultFromName: emailConfig.defaultFromName,
transport,
}
}
async function verifyTransport(transport: Transporter) {
try {
await transport.verify()
} catch (err: unknown) {
console.error({ err, msg: 'Error verifying Nodemailer transport.' })
}
}
/**
* Use ethereal.email to create a mock email account
*/
async function createMockAccount(emailConfig?: NodemailerAdapterArgs) {
try {
const etherealAccount = await nodemailer.createTestAccount()
const smtpOptions = {
...(emailConfig || {}),
auth: {
pass: etherealAccount.pass,
user: etherealAccount.user,
},
fromAddress: emailConfig?.defaultFromAddress,
fromName: emailConfig?.defaultFromName,
host: 'smtp.ethereal.email',
port: 587,
secure: false,
}
const transport = nodemailer.createTransport(smtpOptions)
const { pass, user, web } = etherealAccount
console.info('E-mail configured with ethereal.email test account. ')
console.info(`Log into mock email provider at ${web}`)
console.info(`Mock email account username: ${user}`)
console.info(`Mock email account password: ${pass}`)
return transport
} catch (err: unknown) {
if (err instanceof Error) {
console.error({ err, msg: 'There was a problem setting up the mock email handler' })
throw new InvalidConfiguration(
`Unable to create Nodemailer test account. Error: ${err.message}`,
)
}
throw new InvalidConfiguration('Unable to create Nodemailer test account.')
}
}

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