Compare commits
2 Commits
chore/file
...
chore/jest
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdf42d78be | ||
|
|
2fd0a21f4e |
13
.github/workflows/main.yml
vendored
13
.github/workflows/main.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: ci
|
||||
name: build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -187,8 +187,7 @@ jobs:
|
||||
|
||||
services:
|
||||
postgres:
|
||||
# Custom postgres 17 docker image that supports both pg-vector and postgis: https://github.com/payloadcms/postgis-vector
|
||||
image: ${{ (startsWith(matrix.database, 'postgres') ) && 'ghcr.io/payloadcms/postgis-vector:latest' || '' }}
|
||||
image: ${{ (startsWith(matrix.database, 'postgres') ) && 'postgis/postgis:16-3.4' || '' }}
|
||||
env:
|
||||
# must specify password for PG Docker container image, see: https://registry.hub.docker.com/_/postgres?tab=description&page=1&name=10
|
||||
POSTGRES_USER: ${{ env.POSTGRES_USER }}
|
||||
@@ -316,7 +315,6 @@ jobs:
|
||||
- plugin-cloud-storage
|
||||
- plugin-form-builder
|
||||
- plugin-import-export
|
||||
- plugin-multi-tenant
|
||||
- plugin-nested-docs
|
||||
- plugin-seo
|
||||
- sort
|
||||
@@ -453,7 +451,6 @@ jobs:
|
||||
- plugin-cloud-storage
|
||||
- plugin-form-builder
|
||||
- plugin-import-export
|
||||
- plugin-multi-tenant
|
||||
- plugin-nested-docs
|
||||
- plugin-seo
|
||||
- sort
|
||||
@@ -529,9 +526,7 @@ jobs:
|
||||
# Build listed templates with packed local packages and then runs their int and e2e tests
|
||||
build-and-test-templates:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.needs_build == 'true' }}
|
||||
name: build-template-${{ matrix.template }}-${{ matrix.database }}
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -561,6 +556,8 @@ jobs:
|
||||
# - template: with-vercel-website
|
||||
# database: postgres
|
||||
|
||||
name: ${{ matrix.template }}-${{ matrix.database }}
|
||||
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
|
||||
@@ -32,18 +32,18 @@ The Admin Panel serves as the entire HTTP layer for Payload, providing a full CR
|
||||
Once you [install Payload](../getting-started/installation), the following files and directories will be created in your app:
|
||||
|
||||
```plaintext
|
||||
app
|
||||
├─ (payload)
|
||||
├── admin
|
||||
├─── [[...segments]]
|
||||
app/
|
||||
├─ (payload)/
|
||||
├── admin/
|
||||
├─── [[...segments]]/
|
||||
├──── page.tsx
|
||||
├──── not-found.tsx
|
||||
├── api
|
||||
├─── [...slug]
|
||||
├── api/
|
||||
├─── [...slug]/
|
||||
├──── route.ts
|
||||
├── graphql
|
||||
├── graphql/
|
||||
├──── route.ts
|
||||
├── graphql-playground
|
||||
├── graphql-playground/
|
||||
├──── route.ts
|
||||
├── custom.scss
|
||||
├── layout.tsx
|
||||
@@ -84,30 +84,29 @@ import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
// highlight-start
|
||||
admin: {
|
||||
// highlight-line
|
||||
// ...
|
||||
},
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
|
||||
| `autoLogin` | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). |
|
||||
| `components` | Component overrides that affect the entirety of the Admin Panel. [More details](../custom-components/overview). |
|
||||
| `custom` | Any custom properties you wish to pass to the Admin Panel. |
|
||||
| `dateFormat` | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| `meta` | Base metadata to use for the Admin Panel. [More details](./metadata). |
|
||||
| `routes` | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
|
||||
| `suppressHydrationWarning` | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `<html>` tag. Defaults to `false`. |
|
||||
| `theme` | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. |
|
||||
| `timezones` | Configure the timezone settings for the admin panel. [More details](#timezones) |
|
||||
| `user` | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
|
||||
| Option | Description |
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`avatar`** | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
|
||||
| **`autoLogin`** | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). |
|
||||
| **`components`** | Component overrides that affect the entirety of the Admin Panel. [More details](../custom-components/overview). |
|
||||
| **`custom`** | Any custom properties you wish to pass to the Admin Panel. |
|
||||
| **`dateFormat`** | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
|
||||
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| **`meta`** | Base metadata to use for the Admin Panel. [More details](./metadata). |
|
||||
| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
|
||||
| **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `<html>` tag. Defaults to `false`. |
|
||||
| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. |
|
||||
| **`timezones`** | Configure the timezone settings for the admin panel. [More details](#timezones) |
|
||||
| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
|
||||
|
||||
<Banner type="success">
|
||||
**Reminder:** These are the _root-level_ options for the Admin Panel. You can
|
||||
@@ -187,12 +186,6 @@ The following options are available:
|
||||
| `graphQL` | `/graphql` | The [GraphQL API](../graphql/overview) base path. |
|
||||
| `graphQLPlayground` | `/graphql-playground` | The GraphQL Playground. |
|
||||
|
||||
<Banner type="warning">
|
||||
**Important:** Changing Root-level Routes also requires a change to [Project
|
||||
Structure](#project-structure) to match the new route. [More
|
||||
details](#customizing-root-level-routes).
|
||||
</Banner>
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:** You can easily add _new_ routes to the Admin Panel through [Custom
|
||||
Endpoints](../rest-api/overview#custom-endpoints) and [Custom
|
||||
@@ -203,29 +196,13 @@ The following options are available:
|
||||
|
||||
You can change the Root-level Routes as needed, such as to mount the Admin Panel at the root of your application.
|
||||
|
||||
This change, however, also requires a change to your [Project Structure](#project-structure) to match the new route.
|
||||
|
||||
For example, if you set `routes.admin` to `/`:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
routes: {
|
||||
admin: '/', // highlight-line
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Then you would need to completely remove the `admin` directory from the project structure:
|
||||
Changing Root-level Routes also requires a change to [Project Structure](#project-structure) to match the new route. For example, if you set `routes.admin` to `/`, you would need to completely remove the `admin` directory from the project structure:
|
||||
|
||||
```plaintext
|
||||
app
|
||||
├─ (payload)
|
||||
├── [[...segments]]
|
||||
app/
|
||||
├─ (payload)/
|
||||
├── [[...segments]]/
|
||||
├──── ...
|
||||
├── layout.tsx
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
|
||||
@@ -51,7 +51,7 @@ For more granular control, pass a configuration object instead. Payload exposes
|
||||
| Property | Description |
|
||||
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Component` \* | Pass in the component path that should be rendered when a user navigates to this route. |
|
||||
| `path` \* | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. Must begin with a forward slash (`/`). |
|
||||
| `path` \* | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. |
|
||||
| `exact` | Boolean. When true, will only match if the path matches the `usePathname()` exactly. |
|
||||
| `strict` | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. |
|
||||
| `sensitive` | When true, will match if the path is case sensitive. |
|
||||
|
||||
@@ -315,8 +315,7 @@ import type { Field } from 'payload'
|
||||
export const MyField: Field = {
|
||||
type: 'text',
|
||||
name: 'myField',
|
||||
validate: (value, { req: { t } }) =>
|
||||
Boolean(value) || t('validation:required'), // highlight-line
|
||||
validate: (value, {req: { t }}) => Boolean(value) || t('validation:required'), // highlight-line
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -16,15 +16,14 @@ The labels you provide for your Collections and Globals are used to name the Gra
|
||||
|
||||
At the top of your Payload Config you can define all the options to manage GraphQL.
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `mutations` | Any custom Mutations to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
|
||||
| `queries` | Any custom Queries to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
|
||||
| `maxComplexity` | A number used to set the maximum allowed complexity allowed by requests [More](/docs/graphql/overview#query-complexity-limits) |
|
||||
| `disablePlaygroundInProduction` | A boolean that if false will enable the GraphQL playground in production environments, defaults to true. [More](/docs/graphql/overview#graphql-playground) |
|
||||
| `disableIntrospectionInProduction` | A boolean that if false will enable the GraphQL introspection in production environments, defaults to true. |
|
||||
| `disable` | A boolean that if true will disable the GraphQL entirely, defaults to false. |
|
||||
| `validationRules` | A function that takes the ExecutionArgs and returns an array of ValidationRules. |
|
||||
| Option | Description |
|
||||
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `mutations` | Any custom Mutations to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
|
||||
| `queries` | Any custom Queries to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
|
||||
| `maxComplexity` | A number used to set the maximum allowed complexity allowed by requests [More](/docs/graphql/overview#query-complexity-limits) |
|
||||
| `disablePlaygroundInProduction` | A boolean that if false will enable the GraphQL playground, defaults to true. [More](/docs/graphql/overview#graphql-playground) |
|
||||
| `disable` | A boolean that if true will disable the GraphQL entirely, defaults to false. |
|
||||
| `validationRules` | A function that takes the ExecutionArgs and returns an array of ValidationRules. |
|
||||
|
||||
## Collections
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { cn } from '@/utilities/ui'
|
||||
import useClickableCard from '@/utilities/useClickableCard'
|
||||
import Link from 'next/link'
|
||||
import { useLocale } from 'next-intl'
|
||||
import { useLocale } from 'next-intl';
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { Post } from '@/payload-types'
|
||||
@@ -17,7 +17,7 @@ export const Card: React.FC<{
|
||||
showCategories?: boolean
|
||||
title?: string
|
||||
}> = (props) => {
|
||||
const locale = useLocale()
|
||||
const locale = useLocale();
|
||||
const { card, link } = useClickableCard({})
|
||||
const { className, doc, relationTo, showCategories, title: titleFromProps } = props
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ export default buildConfig({
|
||||
admin: {
|
||||
components: {
|
||||
// The `BeforeLogin` component renders a message that you see while logging into your admin panel.
|
||||
// Feel free to delete this at any time. Simply remove the line below.
|
||||
// Feel free to delete this at any time. Simply remove the line below and the import `BeforeLogin` statement on line 15.
|
||||
beforeLogin: ['@/components/BeforeLogin'],
|
||||
afterDashboard: ['@/components/AfterDashboard'],
|
||||
},
|
||||
|
||||
@@ -14,12 +14,9 @@ export const superAdminOrTenantAdminAccess: Access = ({ req }) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const adminTenantAccessIDs = getUserTenantIDs(req.user, 'tenant-admin')
|
||||
const requestedTenant = req?.data?.tenant
|
||||
|
||||
if (requestedTenant && adminTenantAccessIDs.includes(requestedTenant)) {
|
||||
return true
|
||||
return {
|
||||
tenant: {
|
||||
in: getUserTenantIDs(req.user, 'tenant-admin'),
|
||||
},
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Access } from 'payload'
|
||||
|
||||
import type { Tenant, User } from '../../../payload-types'
|
||||
import type { User } from '../../../payload-types'
|
||||
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||
import { getUserTenantIDs } from '../../../utilities/getUserTenantIDs'
|
||||
@@ -14,20 +14,9 @@ export const createAccess: Access<User> = ({ req }) => {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!isSuperAdmin(req.user) && req.data?.roles?.includes('super-admin')) {
|
||||
return false
|
||||
}
|
||||
|
||||
const adminTenantAccessIDs = getUserTenantIDs(req.user, 'tenant-admin')
|
||||
|
||||
const requestedTenants: Tenant['id'][] =
|
||||
req.data?.tenants?.map((t: { tenant: Tenant['id'] }) => t.tenant) ?? []
|
||||
|
||||
const hasAccessToAllRequestedTenants = requestedTenants.every((tenantID) =>
|
||||
adminTenantAccessIDs.includes(tenantID),
|
||||
)
|
||||
|
||||
if (hasAccessToAllRequestedTenants) {
|
||||
if (adminTenantAccessIDs.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
@@ -125,7 +125,7 @@
|
||||
"tsconfig.base.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@jest/globals": "30.0.3",
|
||||
"@libsql/client": "0.14.0",
|
||||
"@next/bundle-analyzer": "15.3.2",
|
||||
"@payloadcms/db-postgres": "workspace:*",
|
||||
@@ -139,7 +139,7 @@
|
||||
"@swc/cli": "0.7.7",
|
||||
"@swc/jest": "0.2.38",
|
||||
"@types/fs-extra": "^11.0.2",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/minimist": "1.2.5",
|
||||
"@types/node": "22.15.30",
|
||||
"@types/react": "19.1.0",
|
||||
@@ -151,22 +151,22 @@
|
||||
"create-payload-app": "workspace:*",
|
||||
"cross-env": "7.0.3",
|
||||
"dotenv": "16.4.7",
|
||||
"drizzle-kit": "0.31.4",
|
||||
"drizzle-orm": "0.44.2",
|
||||
"drizzle-kit": "0.31.0",
|
||||
"drizzle-orm": "0.43.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"execa": "5.1.1",
|
||||
"form-data": "3.0.1",
|
||||
"fs-extra": "10.1.0",
|
||||
"globby": "11.1.0",
|
||||
"husky": "9.0.11",
|
||||
"jest": "29.7.0",
|
||||
"jest": "30.0.3",
|
||||
"lint-staged": "15.2.7",
|
||||
"minimist": "1.2.8",
|
||||
"mongodb-memory-server": "10.1.4",
|
||||
"next": "15.3.2",
|
||||
"open": "^10.1.0",
|
||||
"p-limit": "^5.0.0",
|
||||
"pg": "8.16.3",
|
||||
"pg": "8.11.3",
|
||||
"playwright": "1.50.0",
|
||||
"playwright-core": "1.50.0",
|
||||
"prettier": "3.5.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/admin-bar",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "An admin bar for React apps using Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -76,7 +76,7 @@
|
||||
"devDependencies": {
|
||||
"@types/esprima": "^4.0.6",
|
||||
"@types/fs-extra": "^9.0.12",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/node": "22.15.30"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -77,9 +77,6 @@ const relationshipSort = ({
|
||||
) {
|
||||
const relationshipPath = segments.slice(0, i + 1).join('.')
|
||||
let sortFieldPath = segments.slice(i + 1, segments.length).join('.')
|
||||
if (sortFieldPath.endsWith('.id')) {
|
||||
sortFieldPath = sortFieldPath.split('.').slice(0, -1).join('.')
|
||||
}
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
throw new APIError('Not supported')
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
try {
|
||||
if (returning === false) {
|
||||
await Model.updateOne(query, data, options)
|
||||
transform({ adapter: this, data, fields, operation: 'read' })
|
||||
return null
|
||||
} else {
|
||||
result = await Model.findOneAndUpdate(query, data, options)
|
||||
|
||||
@@ -417,7 +417,7 @@ export const transform = ({
|
||||
|
||||
if (operation === 'read') {
|
||||
delete data['__v']
|
||||
data.id = data._id || data.id
|
||||
data.id = data._id
|
||||
delete data['_id']
|
||||
|
||||
if (data.id instanceof Types.ObjectId) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -78,9 +78,9 @@
|
||||
"@payloadcms/drizzle": "workspace:*",
|
||||
"@types/pg": "8.10.2",
|
||||
"console-table-printer": "2.12.1",
|
||||
"drizzle-kit": "0.31.4",
|
||||
"drizzle-kit": "0.31.1",
|
||||
"drizzle-orm": "0.44.2",
|
||||
"pg": "8.16.3",
|
||||
"pg": "8.11.3",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "10.0.0"
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert,
|
||||
} from '@payloadcms/drizzle'
|
||||
import {
|
||||
columnToCodeConverter,
|
||||
@@ -208,7 +207,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert,
|
||||
upsert: updateOne,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -76,7 +76,7 @@
|
||||
"@libsql/client": "0.14.0",
|
||||
"@payloadcms/drizzle": "workspace:*",
|
||||
"console-table-printer": "2.12.1",
|
||||
"drizzle-kit": "0.31.4",
|
||||
"drizzle-kit": "0.31.1",
|
||||
"drizzle-orm": "0.44.2",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert,
|
||||
} from '@payloadcms/drizzle'
|
||||
import { like, notLike } from 'drizzle-orm'
|
||||
import { createDatabaseAdapter, defaultBeginTransaction } from 'payload'
|
||||
@@ -190,7 +189,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
updateGlobalVersion,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert,
|
||||
upsert: updateOne,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -78,9 +78,9 @@
|
||||
"@payloadcms/drizzle": "workspace:*",
|
||||
"@vercel/postgres": "^0.9.0",
|
||||
"console-table-printer": "2.12.1",
|
||||
"drizzle-kit": "0.31.4",
|
||||
"drizzle-kit": "0.31.1",
|
||||
"drizzle-orm": "0.44.2",
|
||||
"pg": "8.16.3",
|
||||
"pg": "8.11.3",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "10.0.0"
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert,
|
||||
} from '@payloadcms/drizzle'
|
||||
import {
|
||||
columnToCodeConverter,
|
||||
@@ -203,7 +202,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert,
|
||||
upsert: updateOne,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -78,7 +78,6 @@ export { updateJobs } from './updateJobs.js'
|
||||
export { updateMany } from './updateMany.js'
|
||||
export { updateOne } from './updateOne.js'
|
||||
export { updateVersion } from './updateVersion.js'
|
||||
export { upsert } from './upsert.js'
|
||||
export { upsertRow } from './upsertRow/index.js'
|
||||
export { buildCreateMigration } from './utilities/buildCreateMigration.js'
|
||||
export { buildIndexName } from './utilities/buildIndexName.js'
|
||||
|
||||
@@ -24,26 +24,20 @@ export const columnToCodeConverter: ColumnToCodeConverter = ({
|
||||
|
||||
const columnBuilderArgsArray: string[] = []
|
||||
|
||||
switch (column.type) {
|
||||
case 'bit':
|
||||
case 'halfvec':
|
||||
case 'sparsevec':
|
||||
case 'vector': {
|
||||
if (column.dimensions) {
|
||||
columnBuilderArgsArray.push(`dimensions: ${column.dimensions}`)
|
||||
}
|
||||
break
|
||||
if (column.type === 'timestamp') {
|
||||
columnBuilderArgsArray.push(`mode: '${column.mode}'`)
|
||||
if (column.withTimezone) {
|
||||
columnBuilderArgsArray.push('withTimezone: true')
|
||||
}
|
||||
case 'timestamp': {
|
||||
columnBuilderArgsArray.push(`mode: '${column.mode}'`)
|
||||
if (column.withTimezone) {
|
||||
columnBuilderArgsArray.push('withTimezone: true')
|
||||
}
|
||||
|
||||
if (typeof column.precision === 'number') {
|
||||
columnBuilderArgsArray.push(`precision: ${column.precision}`)
|
||||
}
|
||||
break
|
||||
if (typeof column.precision === 'number') {
|
||||
columnBuilderArgsArray.push(`precision: ${column.precision}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (column.type === 'vector') {
|
||||
if (column.dimensions) {
|
||||
columnBuilderArgsArray.push(`dimensions: ${column.dimensions}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import type { ForeignKeyBuilder, IndexBuilder } from 'drizzle-orm/pg-core'
|
||||
|
||||
import {
|
||||
bit,
|
||||
boolean,
|
||||
foreignKey,
|
||||
halfvec,
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
numeric,
|
||||
serial,
|
||||
sparsevec,
|
||||
text,
|
||||
timestamp,
|
||||
uniqueIndex,
|
||||
@@ -47,14 +44,6 @@ export const buildDrizzleTable = ({
|
||||
|
||||
for (const [key, column] of Object.entries(rawTable.columns)) {
|
||||
switch (column.type) {
|
||||
case 'bit': {
|
||||
const builder = bit(column.name, { dimensions: column.dimensions })
|
||||
|
||||
columns[key] = builder
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'enum':
|
||||
if ('locale' in column) {
|
||||
columns[key] = adapter.enums.enum__locales(column.name)
|
||||
@@ -67,21 +56,6 @@ export const buildDrizzleTable = ({
|
||||
}
|
||||
break
|
||||
|
||||
case 'halfvec': {
|
||||
const builder = halfvec(column.name, { dimensions: column.dimensions })
|
||||
|
||||
columns[key] = builder
|
||||
break
|
||||
}
|
||||
|
||||
case 'sparsevec': {
|
||||
const builder = sparsevec(column.name, { dimensions: column.dimensions })
|
||||
|
||||
columns[key] = builder
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'timestamp': {
|
||||
let builder = timestamp(column.name, {
|
||||
mode: column.mode,
|
||||
|
||||
@@ -53,7 +53,6 @@ type Args = {
|
||||
fields: FlattenedField[]
|
||||
joins: BuildQueryJoinAliases
|
||||
locale?: string
|
||||
parentAliasTable?: PgTableWithColumns<any> | SQLiteTableWithColumns<any>
|
||||
parentIsLocalized: boolean
|
||||
pathSegments: string[]
|
||||
rootTableName?: string
|
||||
@@ -84,7 +83,6 @@ export const getTableColumnFromPath = ({
|
||||
fields,
|
||||
joins,
|
||||
locale: incomingLocale,
|
||||
parentAliasTable,
|
||||
parentIsLocalized,
|
||||
pathSegments: incomingSegments,
|
||||
rootTableName: incomingRootTableName,
|
||||
@@ -164,7 +162,6 @@ export const getTableColumnFromPath = ({
|
||||
table: adapter.tables[newTableName],
|
||||
})
|
||||
}
|
||||
|
||||
return getTableColumnFromPath({
|
||||
adapter,
|
||||
collectionPath,
|
||||
@@ -173,7 +170,6 @@ export const getTableColumnFromPath = ({
|
||||
fields: field.flattenedFields,
|
||||
joins,
|
||||
locale,
|
||||
parentAliasTable: aliasTable,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName,
|
||||
@@ -552,10 +548,7 @@ export const getTableColumnFromPath = ({
|
||||
// Join in the relationships table
|
||||
if (locale && isFieldLocalized && adapter.payload.config.localization) {
|
||||
const conditions = [
|
||||
eq(
|
||||
(parentAliasTable || aliasTable || adapter.tables[rootTableName]).id,
|
||||
aliasRelationshipTable.parent,
|
||||
),
|
||||
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
|
||||
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
|
||||
]
|
||||
|
||||
@@ -573,10 +566,7 @@ export const getTableColumnFromPath = ({
|
||||
// Join in the relationships table
|
||||
addJoinTable({
|
||||
condition: and(
|
||||
eq(
|
||||
(parentAliasTable || aliasTable || adapter.tables[rootTableName]).id,
|
||||
aliasRelationshipTable.parent,
|
||||
),
|
||||
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
|
||||
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
|
||||
),
|
||||
joins,
|
||||
|
||||
@@ -281,30 +281,12 @@ export type VectorRawColumn = {
|
||||
type: 'vector'
|
||||
} & BaseRawColumn
|
||||
|
||||
export type HalfVecRawColumn = {
|
||||
dimensions?: number
|
||||
type: 'halfvec'
|
||||
} & BaseRawColumn
|
||||
|
||||
export type SparseVecRawColumn = {
|
||||
dimensions?: number
|
||||
type: 'sparsevec'
|
||||
} & BaseRawColumn
|
||||
|
||||
export type BinaryVecRawColumn = {
|
||||
dimensions?: number
|
||||
type: 'bit'
|
||||
} & BaseRawColumn
|
||||
|
||||
export type RawColumn =
|
||||
| ({
|
||||
type: 'boolean' | 'geometry' | 'jsonb' | 'numeric' | 'serial' | 'text' | 'varchar'
|
||||
} & BaseRawColumn)
|
||||
| BinaryVecRawColumn
|
||||
| EnumRawColumn
|
||||
| HalfVecRawColumn
|
||||
| IntegerRawColumn
|
||||
| SparseVecRawColumn
|
||||
| TimestampRawColumn
|
||||
| UUIDRawColumn
|
||||
| VectorRawColumn
|
||||
|
||||
@@ -18,7 +18,6 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
data,
|
||||
joins: joinQuery,
|
||||
locale,
|
||||
options = { upsert: false },
|
||||
req,
|
||||
returning,
|
||||
select,
|
||||
@@ -67,13 +66,6 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
}
|
||||
}
|
||||
|
||||
if (!idToUpdate && !options.upsert) {
|
||||
// TODO: In 4.0, if returning === false, we should differentiate between:
|
||||
// - No document found to update
|
||||
// - Document found, but returning === false
|
||||
return null
|
||||
}
|
||||
|
||||
const result = await upsertRow({
|
||||
id: idToUpdate,
|
||||
adapter: this,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { Upsert } from 'payload'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
export const upsert: Upsert = async function upsert(
|
||||
this: DrizzleAdapter,
|
||||
{ collection, data, joins, locale, req, returning, select, where },
|
||||
) {
|
||||
return this.updateOne({
|
||||
collection,
|
||||
data,
|
||||
joins,
|
||||
locale,
|
||||
options: { upsert: true },
|
||||
req,
|
||||
returning,
|
||||
select,
|
||||
where,
|
||||
})
|
||||
}
|
||||
@@ -13,13 +13,6 @@ import { deleteExistingArrayRows } from './deleteExistingArrayRows.js'
|
||||
import { deleteExistingRowsByPath } from './deleteExistingRowsByPath.js'
|
||||
import { insertArrays } from './insertArrays.js'
|
||||
|
||||
/**
|
||||
* If `id` is provided, it will update the row with that ID.
|
||||
* If `where` is provided, it will update the row that matches the `where`
|
||||
* If neither `id` nor `where` is provided, it will create a new row.
|
||||
*
|
||||
* This function replaces the entire row and does not support partial updates.
|
||||
*/
|
||||
export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>({
|
||||
id,
|
||||
adapter,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -42,8 +42,8 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.5.12",
|
||||
"jest": "^29.7.0",
|
||||
"@types/jest": "30.0.0",
|
||||
"jest": "30.0.3",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"eslint": "9.22.0",
|
||||
"eslint-config-prettier": "10.1.1",
|
||||
"eslint-plugin-import-x": "4.6.1",
|
||||
"eslint-plugin-jest": "28.11.0",
|
||||
"eslint-plugin-jest": "29.0.1",
|
||||
"eslint-plugin-jest-dom": "5.5.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-perfectionist": "3.9.1",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"eslint": "9.22.0",
|
||||
"eslint-config-prettier": "10.1.1",
|
||||
"eslint-plugin-import-x": "4.6.1",
|
||||
"eslint-plugin-jest": "28.11.0",
|
||||
"eslint-plugin-jest": "29.0.1",
|
||||
"eslint-plugin-jest-dom": "5.5.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-perfectionist": "3.9.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -113,7 +113,6 @@ export function configToSchema(config: SanitizedConfig): {
|
||||
variables: args.variableValues,
|
||||
// onComplete: (complexity) => { console.log('Query Complexity:', complexity); },
|
||||
}),
|
||||
...(config.graphQL.disableIntrospectionInProduction ? [NoProductionIntrospection] : []),
|
||||
...(typeof config?.graphQL?.validationRules === 'function'
|
||||
? config.graphQL.validationRules(args)
|
||||
: []),
|
||||
@@ -124,18 +123,3 @@ export function configToSchema(config: SanitizedConfig): {
|
||||
validationRules,
|
||||
}
|
||||
}
|
||||
|
||||
const NoProductionIntrospection: GraphQL.ValidationRule = (context) => ({
|
||||
Field(node) {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (node.name.value === '__schema' || node.name.value === '__type') {
|
||||
context.reportError(
|
||||
new GraphQL.GraphQLError(
|
||||
'GraphQL introspection is not allowed, but the query contained __schema or __type',
|
||||
{ nodes: [node] },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -146,7 +146,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
|
||||
|
||||
return (
|
||||
<EntityVisibilityProvider visibleEntities={visibleEntities}>
|
||||
<BulkUploadProvider drawerSlugPrefix={collectionSlug}>
|
||||
<BulkUploadProvider>
|
||||
<ActionsProvider Actions={Actions}>
|
||||
{RenderServerComponent({
|
||||
clientProps,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TypedUser } from 'payload'
|
||||
import type { User } from 'payload'
|
||||
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
import * as qs from 'qs-esm'
|
||||
@@ -7,7 +7,7 @@ type Args = {
|
||||
config
|
||||
route: string
|
||||
searchParams: { [key: string]: string | string[] }
|
||||
user?: TypedUser
|
||||
user?: User
|
||||
}
|
||||
|
||||
export const handleAuthRedirect = ({ config, route, searchParams, user }: Args): string => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
PayloadRequest,
|
||||
SanitizedConfig,
|
||||
SanitizedPermissions,
|
||||
TypedUser,
|
||||
User,
|
||||
} from 'payload'
|
||||
|
||||
import { initI18n } from '@payloadcms/translations'
|
||||
@@ -37,7 +37,7 @@ type PartialResult = {
|
||||
languageCode: AcceptedLanguages
|
||||
payload: Payload
|
||||
responseHeaders: Headers
|
||||
user: null | TypedUser
|
||||
user: null | User
|
||||
}
|
||||
|
||||
// Create cache instances for different parts of our application
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { TypedUser } from 'payload'
|
||||
import type { User } from 'payload'
|
||||
|
||||
import { Button, ConfirmationModal, toast, useModal, useTranslation } from '@payloadcms/ui'
|
||||
import * as qs from 'qs-esm'
|
||||
@@ -9,7 +9,7 @@ const confirmResetModalSlug = 'confirm-reset-modal'
|
||||
|
||||
export const ResetPreferences: React.FC<{
|
||||
readonly apiRoute: string
|
||||
readonly user?: TypedUser
|
||||
readonly user?: User
|
||||
}> = ({ apiRoute, user }) => {
|
||||
const { openModal } = useModal()
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { BasePayload, Config, LanguageOptions, TypedUser } from 'payload'
|
||||
import type { BasePayload, Config, LanguageOptions, User } from 'payload'
|
||||
|
||||
import { FieldLabel } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
@@ -17,7 +17,7 @@ export const Settings: React.FC<{
|
||||
readonly languageOptions: LanguageOptions
|
||||
readonly payload: BasePayload
|
||||
readonly theme: Config['admin']['theme']
|
||||
readonly user?: TypedUser
|
||||
readonly user?: User
|
||||
}> = (props) => {
|
||||
const { className, i18n, languageOptions, payload, theme, user } = props
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
DocumentViewServerProps,
|
||||
DocumentViewServerPropsOnly,
|
||||
EditViewComponent,
|
||||
LivePreviewConfig,
|
||||
PayloadComponent,
|
||||
RenderDocumentVersionsProperties,
|
||||
} from 'payload'
|
||||
@@ -92,6 +91,7 @@ export const renderDocument = async ({
|
||||
payload: {
|
||||
config,
|
||||
config: {
|
||||
admin: { livePreview: livePreviewConfig },
|
||||
routes: { admin: adminRoute, api: apiRoute },
|
||||
serverURL,
|
||||
},
|
||||
@@ -329,12 +329,6 @@ export const renderDocument = async ({
|
||||
viewType,
|
||||
}
|
||||
|
||||
const livePreviewConfig: LivePreviewConfig = {
|
||||
...(config.admin.livePreview || {}),
|
||||
...(collectionConfig?.admin?.livePreview || {}),
|
||||
...(globalConfig?.admin?.livePreview || {}),
|
||||
}
|
||||
|
||||
const livePreviewURL =
|
||||
typeof livePreviewConfig?.url === 'function'
|
||||
? await livePreviewConfig.url({
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
type PayloadRequest,
|
||||
type SelectType,
|
||||
type Sort,
|
||||
type TypedUser,
|
||||
type TypeWithVersion,
|
||||
type User,
|
||||
type Where,
|
||||
} from 'payload'
|
||||
|
||||
@@ -28,7 +28,7 @@ export const fetchVersion = async <TVersionData extends object = object>({
|
||||
overrideAccess?: boolean
|
||||
req: PayloadRequest
|
||||
select?: SelectType
|
||||
user?: TypedUser
|
||||
user?: User
|
||||
}): Promise<null | TypeWithVersion<TVersionData>> => {
|
||||
try {
|
||||
if (collectionSlug) {
|
||||
@@ -88,7 +88,7 @@ export const fetchVersions = async <TVersionData extends object = object>({
|
||||
req: PayloadRequest
|
||||
select?: SelectType
|
||||
sort?: Sort
|
||||
user?: TypedUser
|
||||
user?: User
|
||||
where?: Where
|
||||
}): Promise<null | PaginatedDocs<TypeWithVersion<TVersionData>>> => {
|
||||
const where: Where = { and: [...(whereFromArgs ? [whereFromArgs] : [])] }
|
||||
@@ -160,7 +160,7 @@ export const fetchLatestVersion = async <TVersionData extends object = object>({
|
||||
req: PayloadRequest
|
||||
select?: SelectType
|
||||
status: 'draft' | 'published'
|
||||
user?: TypedUser
|
||||
user?: User
|
||||
where?: Where
|
||||
}): Promise<null | TypeWithVersion<TVersionData>> => {
|
||||
const and: Where[] = [
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedConfig,
|
||||
SanitizedGlobalConfig,
|
||||
TypedUser,
|
||||
User,
|
||||
} from 'payload'
|
||||
|
||||
export type DefaultVersionsViewProps = {
|
||||
@@ -18,6 +18,6 @@ export type DefaultVersionsViewProps = {
|
||||
i18n: I18n
|
||||
id: number | string
|
||||
limit: number
|
||||
user: TypedUser
|
||||
user: User
|
||||
versionsData: PaginatedDocs<Document>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/payload-cloud",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -51,7 +51,7 @@
|
||||
"nodemailer": "6.9.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/nodemailer": "6.4.17",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -7,11 +7,11 @@ const dirname = path.dirname(filename)
|
||||
|
||||
async function build() {
|
||||
const resultIndex = await esbuild.build({
|
||||
entryPoints: ['src/index.ts'],
|
||||
entryPoints: ['src/exports/index.ts'],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
outfile: 'dist/index.js',
|
||||
outfile: 'dist/exports/index.js',
|
||||
splitting: false,
|
||||
external: [
|
||||
'lodash',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { MarkOptional } from 'ts-essentials'
|
||||
|
||||
import type { SanitizedFieldPermissions } from '../../auth/types.js'
|
||||
import type { SanitizedFieldPermissions, User } from '../../auth/types.js'
|
||||
import type { ClientBlock, ClientField, Field } from '../../fields/config/types.js'
|
||||
import type { TypedUser } from '../../index.js'
|
||||
import type { DocumentPreferences } from '../../preferences/types.js'
|
||||
import type { Operation, Payload, PayloadRequest } from '../../types/index.js'
|
||||
import type {
|
||||
@@ -91,7 +90,7 @@ export type ServerComponentProps = {
|
||||
preferences: DocumentPreferences
|
||||
req: PayloadRequest
|
||||
siblingData: Data
|
||||
user: TypedUser
|
||||
user: User
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
|
||||
@@ -23,10 +23,7 @@ export type AdminViewConfig = {
|
||||
/** Whether the path should be matched exactly or as a prefix */
|
||||
exact?: boolean
|
||||
meta?: MetaConfig
|
||||
/**
|
||||
* Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. Must begin with a forward slash (`/`).
|
||||
*/
|
||||
path?: `/${string}`
|
||||
path?: string
|
||||
sensitive?: boolean
|
||||
strict?: boolean
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import type {
|
||||
AuthOperationsFromCollectionSlug,
|
||||
Collection,
|
||||
DataFromCollectionSlug,
|
||||
SanitizedCollectionConfig,
|
||||
} from '../../collections/config/types.js'
|
||||
import type { CollectionSlug, TypedUser } from '../../index.js'
|
||||
import type { CollectionSlug } from '../../index.js'
|
||||
import type { PayloadRequest, Where } from '../../types/index.js'
|
||||
import type { User } from '../types.js'
|
||||
|
||||
import { buildAfterOperation } from '../../collections/operations/utils.js'
|
||||
import {
|
||||
@@ -31,7 +33,7 @@ import { resetLoginAttempts } from '../strategies/local/resetLoginAttempts.js'
|
||||
export type Result = {
|
||||
exp?: number
|
||||
token?: string
|
||||
user?: TypedUser
|
||||
user?: User
|
||||
}
|
||||
|
||||
export type Arguments<TSlug extends CollectionSlug> = {
|
||||
@@ -44,12 +46,14 @@ export type Arguments<TSlug extends CollectionSlug> = {
|
||||
}
|
||||
|
||||
type CheckLoginPermissionArgs = {
|
||||
collection: SanitizedCollectionConfig
|
||||
loggingInWithUsername?: boolean
|
||||
req: PayloadRequest
|
||||
user: any
|
||||
}
|
||||
|
||||
export const checkLoginPermission = ({
|
||||
collection,
|
||||
loggingInWithUsername,
|
||||
req,
|
||||
user,
|
||||
@@ -58,6 +62,10 @@ export const checkLoginPermission = ({
|
||||
throw new AuthenticationError(req.t, Boolean(loggingInWithUsername))
|
||||
}
|
||||
|
||||
if (collection.auth.verify && user._verified === false) {
|
||||
throw new UnverifiedEmail({ t: req.t })
|
||||
}
|
||||
|
||||
if (isUserLocked(new Date(user.lockUntil).getTime())) {
|
||||
throw new LockedAuth(req.t)
|
||||
}
|
||||
@@ -205,6 +213,7 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
|
||||
})
|
||||
|
||||
checkLoginPermission({
|
||||
collection: collectionConfig,
|
||||
loggingInWithUsername: Boolean(canLoginWithUsername && sanitizedUsername),
|
||||
req,
|
||||
user,
|
||||
@@ -232,10 +241,6 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
|
||||
throw new AuthenticationError(req.t)
|
||||
}
|
||||
|
||||
if (collectionConfig.auth.verify && user._verified === false) {
|
||||
throw new UnverifiedEmail({ t: req.t })
|
||||
}
|
||||
|
||||
const fieldsToSignArgs: Parameters<typeof getFieldsToSign>[0] = {
|
||||
collectionConfig,
|
||||
email: sanitizedEmail!,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { decodeJwt } from 'jose'
|
||||
|
||||
import type { Collection } from '../../collections/config/types.js'
|
||||
import type { TypedUser } from '../../index.js'
|
||||
import type { PayloadRequest } from '../../types/index.js'
|
||||
import type { ClientUser } from '../types.js'
|
||||
import type { ClientUser, User } from '../types.js'
|
||||
|
||||
export type MeOperationResult = {
|
||||
collection?: string
|
||||
@@ -43,7 +42,7 @@ export const meOperation = async (args: Arguments): Promise<MeOperationResult> =
|
||||
overrideAccess: false,
|
||||
req,
|
||||
showHiddenFields: false,
|
||||
})) as TypedUser
|
||||
})) as User
|
||||
|
||||
if (user) {
|
||||
user.collection = collection.config.slug
|
||||
|
||||
@@ -3,9 +3,8 @@ import { URL } from 'url'
|
||||
import type { Collection } from '../collections/config/types.js'
|
||||
import type { SanitizedConfig } from '../config/types.js'
|
||||
import type { InitializedEmailAdapter } from '../email/types.js'
|
||||
import type { TypedUser } from '../index.js'
|
||||
import type { PayloadRequest } from '../types/index.js'
|
||||
import type { VerifyConfig } from './types.js'
|
||||
import type { User, VerifyConfig } from './types.js'
|
||||
|
||||
type Args = {
|
||||
collection: Collection
|
||||
@@ -14,7 +13,7 @@ type Args = {
|
||||
email: InitializedEmailAdapter
|
||||
req: PayloadRequest
|
||||
token: string
|
||||
user: TypedUser
|
||||
user: User
|
||||
}
|
||||
|
||||
export async function sendVerificationEmail(args: Args): Promise<void> {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
|
||||
import type { TypedUser } from '../../index.js'
|
||||
import type { Where } from '../../types/index.js'
|
||||
import type { AuthStrategyFunction } from '../index.js'
|
||||
import type { AuthStrategyFunction, User } from '../index.js'
|
||||
|
||||
export const APIKeyAuthentication =
|
||||
(collectionConfig: SanitizedCollectionConfig): AuthStrategyFunction =>
|
||||
@@ -50,7 +49,7 @@ export const APIKeyAuthentication =
|
||||
user!._strategy = 'api-key'
|
||||
|
||||
return {
|
||||
user: user as TypedUser,
|
||||
user: user as User,
|
||||
}
|
||||
}
|
||||
} catch (ignore) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DeepRequired } from 'ts-essentials'
|
||||
|
||||
import type { CollectionSlug, GlobalSlug, Payload, TypedUser } from '../index.js'
|
||||
import type { CollectionSlug, GlobalSlug, Payload } from '../index.js'
|
||||
import type { PayloadRequest, Where } from '../types/index.js'
|
||||
|
||||
/**
|
||||
@@ -122,10 +122,7 @@ type BaseUser = {
|
||||
username?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `TypedUser` instead. This will be removed in 4.0.
|
||||
*/
|
||||
export type UntypedUser = {
|
||||
export type User = {
|
||||
[key: string]: any
|
||||
} & BaseUser
|
||||
|
||||
@@ -182,7 +179,7 @@ export type AuthStrategyResult = {
|
||||
| ({
|
||||
_strategy?: string
|
||||
collection?: string
|
||||
} & TypedUser)
|
||||
} & User)
|
||||
| null
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,6 @@ export const addDefaultsToConfig = (config: Config): Config => {
|
||||
config.endpoints = config.endpoints ?? []
|
||||
config.globals = config.globals ?? []
|
||||
config.graphQL = {
|
||||
disableIntrospectionInProduction: true,
|
||||
disablePlaygroundInProduction: true,
|
||||
maxComplexity: 1000,
|
||||
schemaOutputFile: `${typeof process?.cwd === 'function' ? process.cwd() : ''}/schema.graphql`,
|
||||
|
||||
@@ -375,7 +375,7 @@ type BaseDocumentViewConfig = {
|
||||
export type CustomDocumentViewConfig =
|
||||
| ({
|
||||
Component: DocumentViewComponent
|
||||
path: `/${string}`
|
||||
path: string
|
||||
} & BaseDocumentViewConfig)
|
||||
| ({
|
||||
Component?: DocumentViewComponent
|
||||
@@ -1029,17 +1029,6 @@ export type Config = {
|
||||
*/
|
||||
graphQL?: {
|
||||
disable?: boolean
|
||||
/**
|
||||
* Disable introspection queries in production.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
disableIntrospectionInProduction?: boolean
|
||||
/**
|
||||
* Disable the GraphQL Playground in production.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
disablePlaygroundInProduction?: boolean
|
||||
maxComplexity?: number
|
||||
/**
|
||||
@@ -1173,35 +1162,18 @@ export type Config = {
|
||||
filterConstraints?: SelectField['filterOptions']
|
||||
labels?: CollectionConfig['labels']
|
||||
}
|
||||
/**
|
||||
* Control the routing structure that Payload binds itself to.
|
||||
* @link https://payloadcms.com/docs/admin/overview#root-level-routes
|
||||
*/
|
||||
/** Control the routing structure that Payload binds itself to. */
|
||||
routes?: {
|
||||
/**
|
||||
* The route for the admin panel.
|
||||
* @example "/my-admin" or "/"
|
||||
/** The route for the admin panel.
|
||||
* @example "/my-admin"
|
||||
* @default "/admin"
|
||||
* @link https://payloadcms.com/docs/admin/overview#root-level-routes
|
||||
*/
|
||||
admin?: string
|
||||
/**
|
||||
* The base route for all REST API endpoints.
|
||||
* @default "/api"
|
||||
* @link https://payloadcms.com/docs/admin/overview#root-level-routes
|
||||
*/
|
||||
/** @default "/api" */
|
||||
api?: string
|
||||
/**
|
||||
* The base route for all GraphQL endpoints.
|
||||
* @default "/graphql"
|
||||
* @link https://payloadcms.com/docs/admin/overview#root-level-routes
|
||||
*/
|
||||
/** @default "/graphql" */
|
||||
graphQL?: string
|
||||
/**
|
||||
* The route for the GraphQL Playground.
|
||||
* @default "/graphql-playground"
|
||||
* @link https://payloadcms.com/docs/admin/overview#root-level-routes
|
||||
*/
|
||||
/** @default "/graphql-playground" */
|
||||
graphQLPlayground?: string
|
||||
}
|
||||
/** Secure string that Payload will use for any encryption workflows */
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { AuthArgs } from './auth/operations/auth.js'
|
||||
import type { Result as ForgotPasswordResult } from './auth/operations/forgotPassword.js'
|
||||
import type { Result as LoginResult } from './auth/operations/login.js'
|
||||
import type { Result as ResetPasswordResult } from './auth/operations/resetPassword.js'
|
||||
import type { AuthStrategy, UntypedUser } from './auth/types.js'
|
||||
import type { AuthStrategy, User } from './auth/types.js'
|
||||
import type {
|
||||
BulkOperationResult,
|
||||
Collection,
|
||||
@@ -216,7 +216,7 @@ export interface GeneratedTypes {
|
||||
}
|
||||
}
|
||||
localeUntyped: null | string
|
||||
userUntyped: UntypedUser
|
||||
userUntyped: User
|
||||
}
|
||||
|
||||
// Helper type to resolve the correct type using conditional types
|
||||
@@ -299,10 +299,6 @@ type ResolveLocaleType<T> = 'locale' extends keyof T ? T['locale'] : T['localeUn
|
||||
type ResolveUserType<T> = 'user' extends keyof T ? T['user'] : T['userUntyped']
|
||||
|
||||
export type TypedLocale = ResolveLocaleType<GeneratedTypes>
|
||||
|
||||
/**
|
||||
* @todo rename to `User` in 4.0
|
||||
*/
|
||||
export type TypedUser = ResolveUserType<GeneratedTypes>
|
||||
|
||||
// @ts-expect-error
|
||||
@@ -1103,7 +1099,7 @@ export type {
|
||||
SanitizedFieldPermissions,
|
||||
SanitizedGlobalPermission,
|
||||
SanitizedPermissions,
|
||||
UntypedUser as User,
|
||||
User,
|
||||
VerifyConfig,
|
||||
} from './auth/types.js'
|
||||
export { generateImportMap } from './bin/generateImportMap/index.js'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Dispatcher } from 'undici'
|
||||
|
||||
import { lookup } from 'dns/promises'
|
||||
import ipaddr from 'ipaddr.js'
|
||||
import { Agent, fetch as undiciFetch } from 'undici'
|
||||
|
||||
@@ -25,26 +24,12 @@ const isSafeIp = (ip: string) => {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a hostname or IP address is safe to fetch from.
|
||||
* @param hostname a hostname or IP address
|
||||
* @returns
|
||||
*/
|
||||
const isSafe = async (hostname: string) => {
|
||||
try {
|
||||
if (ipaddr.isValid(hostname)) {
|
||||
return isSafeIp(hostname)
|
||||
}
|
||||
|
||||
const { address } = await lookup(hostname)
|
||||
return isSafeIp(address)
|
||||
} catch (_ignore) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const ssrfFilterInterceptor: Dispatcher.DispatcherComposeInterceptor = (dispatch) => {
|
||||
return (opts, handler) => {
|
||||
const url = new URL(opts.origin?.toString() + opts.path)
|
||||
if (!isSafeIp(url.hostname)) {
|
||||
throw new Error(`Blocked unsafe attempt to ${url}`)
|
||||
}
|
||||
return dispatch(opts, handler)
|
||||
}
|
||||
}
|
||||
@@ -55,20 +40,11 @@ const safeDispatcher = new Agent().compose(ssrfFilterInterceptor)
|
||||
* A "safe" version of undici's fetch that prevents SSRF attacks.
|
||||
*
|
||||
* - Utilizes a custom dispatcher that filters out requests to unsafe IP addresses.
|
||||
* - Validates domain names by resolving them to IP addresses and checking if they're safe.
|
||||
* - Undici was used because it supported interceptors as well as "credentials: include". Native fetch
|
||||
*/
|
||||
export const safeFetch = async (...args: Parameters<typeof undiciFetch>) => {
|
||||
const [unverifiedUrl, options] = args
|
||||
|
||||
const [url, options] = args
|
||||
try {
|
||||
const url = new URL(unverifiedUrl)
|
||||
|
||||
const isHostnameSafe = await isSafe(url.hostname)
|
||||
if (!isHostnameSafe) {
|
||||
throw new Error(`Blocked unsafe attempt to ${url.toString()}`)
|
||||
}
|
||||
|
||||
return await undiciFetch(url, {
|
||||
...options,
|
||||
dispatcher: safeDispatcher,
|
||||
@@ -80,13 +56,11 @@ export const safeFetch = async (...args: Parameters<typeof undiciFetch>) => {
|
||||
// The desired message we want to bubble up is in the cause
|
||||
throw new Error(error.cause.message)
|
||||
} else {
|
||||
let stringifiedUrl: string | undefined = undefined
|
||||
if (typeof unverifiedUrl === 'string') {
|
||||
stringifiedUrl = unverifiedUrl
|
||||
} else if (unverifiedUrl instanceof URL) {
|
||||
stringifiedUrl = unverifiedUrl.toString()
|
||||
} else if (unverifiedUrl instanceof Request) {
|
||||
stringifiedUrl = unverifiedUrl.url
|
||||
let stringifiedUrl: string | undefined | URL = undefined
|
||||
if (typeof url === 'string' || url instanceof URL) {
|
||||
stringifiedUrl = url
|
||||
} else if (url instanceof Request) {
|
||||
stringifiedUrl = url.url
|
||||
}
|
||||
|
||||
throw new Error(`Failed to fetch from ${stringifiedUrl}, ${error.message}`)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Payload, RequestContext, TypedLocale, TypedUser } from '../index.js'
|
||||
import type { User } from '../auth/types.js'
|
||||
import type { Payload, RequestContext, TypedLocale } from '../index.js'
|
||||
import type { PayloadRequest } from '../types/index.js'
|
||||
|
||||
import { getDataLoader } from '../collections/dataloader.js'
|
||||
@@ -90,7 +91,7 @@ export type CreateLocalReqOptions = {
|
||||
locale?: string
|
||||
req?: Partial<PayloadRequest>
|
||||
urlSuffix?: string
|
||||
user?: TypedUser
|
||||
user?: User
|
||||
}
|
||||
|
||||
type CreateLocalReq = (options: CreateLocalReqOptions, payload: Payload) => Promise<PayloadRequest>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { User } from '../../auth/types.js'
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
import type { TypedUser } from '../../index.js'
|
||||
import type { TaskConfig } from '../../queues/config/types/taskTypes.js'
|
||||
import type { SchedulePublishTaskInput } from './types.js'
|
||||
|
||||
@@ -21,14 +21,14 @@ export const getSchedulePublishTask = ({
|
||||
|
||||
const userID = input.user
|
||||
|
||||
let user: null | TypedUser = null
|
||||
let user: null | User = null
|
||||
|
||||
if (userID) {
|
||||
user = (await req.payload.findByID({
|
||||
id: userID,
|
||||
collection: adminUserSlug,
|
||||
depth: 0,
|
||||
})) as TypedUser
|
||||
})) as User
|
||||
|
||||
user.collection = adminUserSlug
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-import-export",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Import-Export plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -74,12 +74,11 @@ export const Preview = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const { docs, totalDocs }: { docs: Record<string, unknown>[]; totalDocs: number } =
|
||||
await res.json()
|
||||
const { docs, totalDocs } = await res.json()
|
||||
|
||||
setResultCount(limit && limit < totalDocs ? limit : totalDocs)
|
||||
|
||||
const allKeys = Array.from(new Set(docs.flatMap((doc) => Object.keys(doc))))
|
||||
const allKeys = Object.keys(docs[0] || {})
|
||||
const defaultMetaFields = ['createdAt', 'updatedAt', '_status', 'id']
|
||||
|
||||
// Match CSV column ordering by building keys based on fields and regex
|
||||
@@ -97,10 +96,13 @@ export const Preview = () => {
|
||||
})
|
||||
: allKeys.filter((key) => !defaultMetaFields.includes(key))
|
||||
|
||||
const fieldKeys =
|
||||
Array.isArray(fields) && fields.length > 0
|
||||
? selectedKeys // strictly only what was selected
|
||||
: [...selectedKeys, ...defaultMetaFields.filter((key) => allKeys.includes(key))]
|
||||
const includedMeta = new Set(selectedKeys)
|
||||
const missingMetaFields = defaultMetaFields.flatMap((field) => {
|
||||
const regex = fieldToRegex(field)
|
||||
return allKeys.filter((key) => regex.test(key) && !includedMeta.has(key))
|
||||
})
|
||||
|
||||
const fieldKeys = [...selectedKeys, ...missingMetaFields]
|
||||
|
||||
// Build columns based on flattened keys
|
||||
const newColumns: Column[] = fieldKeys.map((key) => ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable perfectionist/sort-objects */
|
||||
import type { PayloadRequest, Sort, TypedUser, Where } from 'payload'
|
||||
import type { PayloadRequest, Sort, User, Where } from 'payload'
|
||||
|
||||
import { stringify } from 'csv-stringify/sync'
|
||||
import { APIError } from 'payload'
|
||||
@@ -38,7 +38,7 @@ export type CreateExportArgs = {
|
||||
download?: boolean
|
||||
input: Export
|
||||
req: PayloadRequest
|
||||
user?: TypedUser
|
||||
user?: User
|
||||
}
|
||||
|
||||
export const createExport = async (args: CreateExportArgs) => {
|
||||
@@ -111,45 +111,23 @@ export const createExport = async (args: CreateExportArgs) => {
|
||||
|
||||
if (download) {
|
||||
if (debug) {
|
||||
req.payload.logger.info('Pre-scanning all columns before streaming')
|
||||
}
|
||||
|
||||
const allColumnsSet = new Set<string>()
|
||||
const allColumns: string[] = []
|
||||
let scanPage = 1
|
||||
let hasMore = true
|
||||
|
||||
while (hasMore) {
|
||||
const result = await payload.find({ ...findArgs, page: scanPage })
|
||||
|
||||
result.docs.forEach((doc) => {
|
||||
const flat = flattenObject({ doc, fields, toCSVFunctions })
|
||||
Object.keys(flat).forEach((key) => {
|
||||
if (!allColumnsSet.has(key)) {
|
||||
allColumnsSet.add(key)
|
||||
allColumns.push(key)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
hasMore = result.hasNextPage
|
||||
scanPage += 1
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
req.payload.logger.info(`Discovered ${allColumns.length} columns`)
|
||||
req.payload.logger.info('Starting download stream')
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
let isFirstBatch = true
|
||||
let streamPage = 1
|
||||
let columns: string[] | undefined
|
||||
let page = 1
|
||||
|
||||
const stream = new Readable({
|
||||
async read() {
|
||||
const result = await payload.find({ ...findArgs, page: streamPage })
|
||||
const result = await payload.find({
|
||||
...findArgs,
|
||||
page,
|
||||
})
|
||||
|
||||
if (debug) {
|
||||
req.payload.logger.info(`Streaming batch ${streamPage} with ${result.docs.length} docs`)
|
||||
req.payload.logger.info(`Processing batch ${page} with ${result.docs.length} documents`)
|
||||
}
|
||||
|
||||
if (result.docs.length === 0) {
|
||||
@@ -157,24 +135,19 @@ export const createExport = async (args: CreateExportArgs) => {
|
||||
return
|
||||
}
|
||||
|
||||
const batchRows = result.docs.map((doc) => flattenObject({ doc, fields, toCSVFunctions }))
|
||||
const csvInput = result.docs.map((doc) => flattenObject({ doc, fields, toCSVFunctions }))
|
||||
|
||||
const paddedRows = batchRows.map((row) => {
|
||||
const fullRow: Record<string, unknown> = {}
|
||||
for (const col of allColumns) {
|
||||
fullRow[col] = row[col] ?? ''
|
||||
}
|
||||
return fullRow
|
||||
})
|
||||
if (isFirstBatch) {
|
||||
columns = Object.keys(csvInput[0] ?? {})
|
||||
}
|
||||
|
||||
const csvString = stringify(paddedRows, {
|
||||
const csvString = stringify(csvInput, {
|
||||
header: isFirstBatch,
|
||||
columns: allColumns,
|
||||
columns,
|
||||
})
|
||||
|
||||
this.push(encoder.encode(csvString))
|
||||
isFirstBatch = false
|
||||
streamPage += 1
|
||||
|
||||
if (!result.hasNextPage) {
|
||||
if (debug) {
|
||||
@@ -182,6 +155,8 @@ export const createExport = async (args: CreateExportArgs) => {
|
||||
}
|
||||
this.push(null) // End the stream
|
||||
}
|
||||
|
||||
page += 1
|
||||
},
|
||||
})
|
||||
|
||||
@@ -193,15 +168,11 @@ export const createExport = async (args: CreateExportArgs) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Non-download path (buffered export)
|
||||
if (debug) {
|
||||
req.payload.logger.info('Starting file generation')
|
||||
}
|
||||
|
||||
const outputData: string[] = []
|
||||
const rows: Record<string, unknown>[] = []
|
||||
const columnsSet = new Set<string>()
|
||||
const columns: string[] = []
|
||||
let isFirstBatch = true
|
||||
let page = 1
|
||||
let hasNextPage = true
|
||||
|
||||
@@ -218,19 +189,9 @@ export const createExport = async (args: CreateExportArgs) => {
|
||||
}
|
||||
|
||||
if (isCSV) {
|
||||
const batchRows = result.docs.map((doc) => flattenObject({ doc, fields, toCSVFunctions }))
|
||||
|
||||
// Track discovered column keys
|
||||
batchRows.forEach((row) => {
|
||||
Object.keys(row).forEach((key) => {
|
||||
if (!columnsSet.has(key)) {
|
||||
columnsSet.add(key)
|
||||
columns.push(key)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
rows.push(...batchRows)
|
||||
const csvInput = result.docs.map((doc) => flattenObject({ doc, fields, toCSVFunctions }))
|
||||
outputData.push(stringify(csvInput, { header: isFirstBatch }))
|
||||
isFirstBatch = false
|
||||
} else {
|
||||
const jsonInput = result.docs.map((doc) => JSON.stringify(doc))
|
||||
outputData.push(jsonInput.join(',\n'))
|
||||
@@ -240,23 +201,6 @@ export const createExport = async (args: CreateExportArgs) => {
|
||||
page += 1
|
||||
}
|
||||
|
||||
if (isCSV) {
|
||||
const paddedRows = rows.map((row) => {
|
||||
const fullRow: Record<string, unknown> = {}
|
||||
for (const col of columns) {
|
||||
fullRow[col] = row[col] ?? ''
|
||||
}
|
||||
return fullRow
|
||||
})
|
||||
|
||||
outputData.push(
|
||||
stringify(paddedRows, {
|
||||
header: true,
|
||||
columns,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(format === 'json' ? `[${outputData.join(',')}]` : outputData.join(''))
|
||||
if (debug) {
|
||||
req.payload.logger.info(`${format} file generation complete`)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Config, TaskConfig, TypedUser } from 'payload'
|
||||
import type { Config, TaskConfig, User } from 'payload'
|
||||
|
||||
import type { CreateExportArgs, Export } from './createExport.js'
|
||||
|
||||
@@ -29,13 +29,13 @@ export const getCreateCollectionExportTask = (
|
||||
return {
|
||||
slug: 'createCollectionExport',
|
||||
handler: async ({ input, req }: CreateExportArgs) => {
|
||||
let user: TypedUser | undefined
|
||||
let user: undefined | User
|
||||
|
||||
if (input.userCollection && input.user) {
|
||||
user = (await req.payload.findByID({
|
||||
id: input.user,
|
||||
collection: input.userCollection,
|
||||
})) as TypedUser
|
||||
})) as User
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
|
||||
@@ -11,7 +11,6 @@ import { getCustomFieldFunctions } from './export/getCustomFieldFunctions.js'
|
||||
import { getSelect } from './export/getSelect.js'
|
||||
import { getExportCollection } from './getExportCollection.js'
|
||||
import { translations } from './translations/index.js'
|
||||
import { getFlattenedFieldKeys } from './utilities/getFlattenedFieldKeys.js'
|
||||
|
||||
export const importExportPlugin =
|
||||
(pluginConfig: ImportExportPluginConfig) =>
|
||||
@@ -113,23 +112,13 @@ export const importExportPlugin =
|
||||
select,
|
||||
})
|
||||
|
||||
const possibleKeys = getFlattenedFieldKeys(collection.config.fields as FlattenedField[])
|
||||
|
||||
const transformed = docs.map((doc) => {
|
||||
const row = flattenObject({
|
||||
const transformed = docs.map((doc) =>
|
||||
flattenObject({
|
||||
doc,
|
||||
fields,
|
||||
toCSVFunctions,
|
||||
})
|
||||
|
||||
for (const key of possibleKeys) {
|
||||
if (!(key in row)) {
|
||||
row[key] = null
|
||||
}
|
||||
}
|
||||
|
||||
return row
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
return Response.json({
|
||||
docs: transformed,
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { type FlattenedField } from 'payload'
|
||||
|
||||
type FieldWithPresentational =
|
||||
| {
|
||||
fields?: FlattenedField[]
|
||||
name?: string
|
||||
tabs?: {
|
||||
fields: FlattenedField[]
|
||||
name?: string
|
||||
}[]
|
||||
type: 'collapsible' | 'row' | 'tabs'
|
||||
}
|
||||
| FlattenedField
|
||||
|
||||
export const getFlattenedFieldKeys = (fields: FieldWithPresentational[], prefix = ''): string[] => {
|
||||
const keys: string[] = []
|
||||
|
||||
fields.forEach((field) => {
|
||||
if (!('name' in field) || typeof field.name !== 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
const name = prefix ? `${prefix}_${field.name}` : field.name
|
||||
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const subKeys = getFlattenedFieldKeys(field.fields as FlattenedField[], `${name}_0`)
|
||||
keys.push(...subKeys)
|
||||
break
|
||||
}
|
||||
case 'blocks':
|
||||
field.blocks.forEach((block) => {
|
||||
const blockKeys = getFlattenedFieldKeys(block.fields as FlattenedField[], `${name}_0`)
|
||||
keys.push(...blockKeys)
|
||||
})
|
||||
break
|
||||
case 'collapsible':
|
||||
case 'group':
|
||||
case 'row':
|
||||
keys.push(...getFlattenedFieldKeys(field.fields as FlattenedField[], name))
|
||||
break
|
||||
case 'relationship':
|
||||
if (field.hasMany) {
|
||||
// e.g. hasManyPolymorphic_0_value_id
|
||||
keys.push(`${name}_0_relationTo`, `${name}_0_value_id`)
|
||||
} else {
|
||||
// e.g. hasOnePolymorphic_id
|
||||
keys.push(`${name}_id`, `${name}_relationTo`)
|
||||
}
|
||||
break
|
||||
case 'tabs':
|
||||
if (field.tabs) {
|
||||
field.tabs.forEach((tab) => {
|
||||
if (tab.name) {
|
||||
const tabPrefix = prefix ? `${prefix}_${tab.name}` : tab.name
|
||||
keys.push(...getFlattenedFieldKeys(tab.fields, tabPrefix))
|
||||
} else {
|
||||
keys.push(...getFlattenedFieldKeys(tab.fields, prefix))
|
||||
}
|
||||
})
|
||||
}
|
||||
break
|
||||
default:
|
||||
if ('hasMany' in field && field.hasMany) {
|
||||
// Push placeholder for first index
|
||||
keys.push(`${name}_0`)
|
||||
} else {
|
||||
keys.push(name)
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return keys
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-multi-tenant",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Multi Tenant plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { RelationshipFieldClientProps } from 'payload'
|
||||
|
||||
import { RelationshipField, useField, useFormModified } from '@payloadcms/ui'
|
||||
import { RelationshipField, useField } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
|
||||
@@ -18,14 +18,7 @@ type Props = {
|
||||
export const TenantField = (args: Props) => {
|
||||
const { debug, unique } = args
|
||||
const { setValue, value } = useField<number | string>()
|
||||
const modified = useFormModified()
|
||||
const {
|
||||
options,
|
||||
selectedTenantID,
|
||||
setEntityType: setEntityType,
|
||||
setModified,
|
||||
setTenant,
|
||||
} = useTenantSelection()
|
||||
const { options, selectedTenantID, setPreventRefreshOnChange, setTenant } = useTenantSelection()
|
||||
|
||||
const hasSetValueRef = React.useRef(false)
|
||||
|
||||
@@ -42,25 +35,18 @@ export const TenantField = (args: Props) => {
|
||||
hasSetValueRef.current = true
|
||||
} else if (!value || value !== selectedTenantID) {
|
||||
// Update the field on the document value when the tenant is changed
|
||||
setValue(selectedTenantID, !value || value === selectedTenantID)
|
||||
setValue(selectedTenantID)
|
||||
}
|
||||
}, [value, selectedTenantID, setTenant, setValue, options, unique])
|
||||
|
||||
React.useEffect(() => {
|
||||
setEntityType(unique ? 'global' : 'document')
|
||||
return () => {
|
||||
setEntityType(undefined)
|
||||
if (!unique) {
|
||||
setPreventRefreshOnChange(true)
|
||||
}
|
||||
}, [unique, setEntityType])
|
||||
|
||||
React.useEffect(() => {
|
||||
// sync form modified state with the tenant selection provider context
|
||||
setModified(modified)
|
||||
|
||||
return () => {
|
||||
setModified(false)
|
||||
setPreventRefreshOnChange(false)
|
||||
}
|
||||
}, [modified, setModified])
|
||||
}, [unique, setPreventRefreshOnChange])
|
||||
|
||||
if (debug) {
|
||||
return (
|
||||
|
||||
@@ -20,12 +20,11 @@ import type {
|
||||
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
|
||||
import './index.scss'
|
||||
|
||||
const confirmSwitchTenantSlug = 'confirm-switch-tenant'
|
||||
const confirmLeaveWithoutSavingSlug = 'confirm-leave-without-saving'
|
||||
const confirmSwitchTenantSlug = 'confirmSwitchTenant'
|
||||
|
||||
export const TenantSelector = ({ label, viewType }: { label: string; viewType?: ViewTypes }) => {
|
||||
const { entityType, modified, options, selectedTenantID, setTenant } = useTenantSelection()
|
||||
const { closeModal, openModal } = useModal()
|
||||
const { options, preventRefreshOnChange, selectedTenantID, setTenant } = useTenantSelection()
|
||||
const { openModal } = useModal()
|
||||
const { i18n, t } = useTranslation<
|
||||
PluginMultiTenantTranslations,
|
||||
PluginMultiTenantTranslationKeys
|
||||
@@ -61,27 +60,15 @@ export const TenantSelector = ({ label, viewType }: { label: string; viewType?:
|
||||
|
||||
const onChange = React.useCallback(
|
||||
(option: ReactSelectOption | ReactSelectOption[]) => {
|
||||
if (option && 'value' in option && option.value === selectedTenantID) {
|
||||
// If the selected option is the same as the current tenant, do nothing
|
||||
if (!preventRefreshOnChange) {
|
||||
switchTenant(option)
|
||||
return
|
||||
}
|
||||
|
||||
if (entityType !== 'document') {
|
||||
if (entityType === 'global' && modified) {
|
||||
// If the entityType is 'global' and there are unsaved changes, prompt for confirmation
|
||||
setTenantSelection(option)
|
||||
openModal(confirmLeaveWithoutSavingSlug)
|
||||
} else {
|
||||
// If the entityType is not 'document', switch tenant without confirmation
|
||||
switchTenant(option)
|
||||
}
|
||||
} else {
|
||||
// non-unique documents should always prompt for confirmation
|
||||
setTenantSelection(option)
|
||||
openModal(confirmSwitchTenantSlug)
|
||||
}
|
||||
},
|
||||
[selectedTenantID, entityType, modified, switchTenant, openModal],
|
||||
[openModal, preventRefreshOnChange, switchTenant],
|
||||
)
|
||||
|
||||
if (options.length <= 1) {
|
||||
@@ -118,28 +105,22 @@ export const TenantSelector = ({ label, viewType }: { label: string; viewType?:
|
||||
}}
|
||||
/>
|
||||
}
|
||||
heading={t('plugin-multi-tenant:confirm-tenant-switch--heading', {
|
||||
tenantLabel: getTranslation(label, i18n),
|
||||
})}
|
||||
heading={
|
||||
<Translation
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
i18nKey="plugin-multi-tenant:confirm-tenant-switch--heading"
|
||||
t={t}
|
||||
variables={{
|
||||
tenantLabel: getTranslation(label, i18n),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
modalSlug={confirmSwitchTenantSlug}
|
||||
onConfirm={() => {
|
||||
switchTenant(tenantSelection)
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
body={t('general:changesNotSaved')}
|
||||
cancelLabel={t('general:stayOnThisPage')}
|
||||
confirmLabel={t('general:leaveAnyway')}
|
||||
heading={t('general:leaveWithoutSaving')}
|
||||
modalSlug={confirmLeaveWithoutSavingSlug}
|
||||
onCancel={() => {
|
||||
closeModal(confirmLeaveWithoutSavingSlug)
|
||||
}}
|
||||
onConfirm={() => {
|
||||
switchTenant(tenantSelection)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
useDocumentTitle,
|
||||
useEffectEvent,
|
||||
useFormFields,
|
||||
useFormSubmitted,
|
||||
useOperation,
|
||||
} from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
@@ -17,9 +15,8 @@ import { useTenantSelection } from '../../providers/TenantSelectionProvider/inde
|
||||
|
||||
export const WatchTenantCollection = () => {
|
||||
const { id, collectionSlug } = useDocumentInfo()
|
||||
const operation = useOperation()
|
||||
const submitted = useFormSubmitted()
|
||||
const { title } = useDocumentTitle()
|
||||
const addedNewTenant = React.useRef(false)
|
||||
|
||||
const { getEntityConfig } = useConfig()
|
||||
const [useAsTitleName] = React.useState(
|
||||
@@ -27,7 +24,7 @@ export const WatchTenantCollection = () => {
|
||||
)
|
||||
const titleField = useFormFields(([fields]) => (useAsTitleName ? fields[useAsTitleName] : {}))
|
||||
|
||||
const { syncTenants, updateTenants } = useTenantSelection()
|
||||
const { options, updateTenants } = useTenantSelection()
|
||||
|
||||
const syncTenantTitle = useEffectEvent(() => {
|
||||
if (id) {
|
||||
@@ -35,19 +32,27 @@ export const WatchTenantCollection = () => {
|
||||
}
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!id || !title || addedNewTenant.current) {
|
||||
return
|
||||
}
|
||||
// Track tenant creation and add it to the tenant selector
|
||||
const exists = options.some((opt) => opt.value === id)
|
||||
if (!exists) {
|
||||
addedNewTenant.current = true
|
||||
updateTenants({ id, label: title })
|
||||
}
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id])
|
||||
|
||||
React.useEffect(() => {
|
||||
// only update the tenant selector when the document saves
|
||||
// → aka when initial value changes
|
||||
if (id && titleField?.initialValue) {
|
||||
void syncTenantTitle()
|
||||
syncTenantTitle()
|
||||
}
|
||||
}, [id, titleField?.initialValue, syncTenants])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (operation === 'create' && submitted) {
|
||||
void syncTenants()
|
||||
}
|
||||
}, [operation, submitted, syncTenants, id])
|
||||
}, [id, titleField?.initialValue])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -2,39 +2,26 @@
|
||||
|
||||
import type { OptionObject } from 'payload'
|
||||
|
||||
import { toast, useAuth, useConfig } from '@payloadcms/ui'
|
||||
import { useAuth } from '@payloadcms/ui'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React, { createContext } from 'react'
|
||||
|
||||
type ContextType = {
|
||||
/**
|
||||
* What is the context of the selector? It is either 'document' | 'global' | undefined.
|
||||
*
|
||||
* - 'document' means you are viewing a document in the context of a tenant
|
||||
* - 'global' means you are viewing a "global" (globals are collection documents but prevent you from viewing the list view) document in the context of a tenant
|
||||
* - undefined means you are not viewing a document at all
|
||||
*/
|
||||
entityType?: 'document' | 'global'
|
||||
/**
|
||||
* Hoists the forms modified state
|
||||
*/
|
||||
modified?: boolean
|
||||
/**
|
||||
* Array of options to select from
|
||||
*/
|
||||
options: OptionObject[]
|
||||
preventRefreshOnChange: boolean
|
||||
/**
|
||||
* The currently selected tenant ID
|
||||
*/
|
||||
selectedTenantID: number | string | undefined
|
||||
/**
|
||||
* Sets the entityType when a document is loaded and sets it to undefined when the document unmounts.
|
||||
* Prevents a refresh when the tenant is changed
|
||||
*
|
||||
* If not switching tenants while viewing a "global", set to true
|
||||
*/
|
||||
setEntityType: React.Dispatch<React.SetStateAction<'document' | 'global' | undefined>>
|
||||
/**
|
||||
* Sets the modified state
|
||||
*/
|
||||
setModified: React.Dispatch<React.SetStateAction<boolean>>
|
||||
setPreventRefreshOnChange: React.Dispatch<React.SetStateAction<boolean>>
|
||||
/**
|
||||
* Sets the selected tenant ID
|
||||
*
|
||||
@@ -42,10 +29,6 @@ type ContextType = {
|
||||
* @param args.refresh - Whether to refresh the page after changing the tenant
|
||||
*/
|
||||
setTenant: (args: { id: number | string | undefined; refresh?: boolean }) => void
|
||||
/**
|
||||
* Used to sync tenants displayed in the tenant selector when updates are made to the tenants collection.
|
||||
*/
|
||||
syncTenants: () => Promise<void>
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@@ -53,13 +36,11 @@ type ContextType = {
|
||||
}
|
||||
|
||||
const Context = createContext<ContextType>({
|
||||
entityType: undefined,
|
||||
options: [],
|
||||
preventRefreshOnChange: false,
|
||||
selectedTenantID: undefined,
|
||||
setEntityType: () => undefined,
|
||||
setModified: () => undefined,
|
||||
setPreventRefreshOnChange: () => null,
|
||||
setTenant: () => null,
|
||||
syncTenants: () => Promise.resolve(),
|
||||
updateTenants: () => null,
|
||||
})
|
||||
|
||||
@@ -68,23 +49,17 @@ export const TenantSelectionProviderClient = ({
|
||||
initialValue,
|
||||
tenantCookie,
|
||||
tenantOptions: tenantOptionsFromProps,
|
||||
tenantsCollectionSlug,
|
||||
useAsTitle,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
initialValue?: number | string
|
||||
tenantCookie?: string
|
||||
tenantOptions: OptionObject[]
|
||||
tenantsCollectionSlug: string
|
||||
useAsTitle: string
|
||||
}) => {
|
||||
const [selectedTenantID, setSelectedTenantID] = React.useState<number | string | undefined>(
|
||||
initialValue,
|
||||
)
|
||||
const [modified, setModified] = React.useState<boolean>(false)
|
||||
const [entityType, setEntityType] = React.useState<'document' | 'global' | undefined>(undefined)
|
||||
const [preventRefreshOnChange, setPreventRefreshOnChange] = React.useState(false)
|
||||
const { user } = useAuth()
|
||||
const { config } = useConfig()
|
||||
const userID = React.useMemo(() => user?.id, [user?.id])
|
||||
const [tenantOptions, setTenantOptions] = React.useState<OptionObject[]>(
|
||||
() => tenantOptionsFromProps,
|
||||
@@ -110,105 +85,73 @@ export const TenantSelectionProviderClient = ({
|
||||
({ id, refresh }) => {
|
||||
if (id === undefined) {
|
||||
if (tenantOptions.length > 1) {
|
||||
// users with multiple tenants can clear the tenant selection
|
||||
setSelectedTenantID(undefined)
|
||||
deleteCookie()
|
||||
} else {
|
||||
// if there is only one tenant, force the selection of that tenant
|
||||
setSelectedTenantID(tenantOptions[0]?.value)
|
||||
setCookie(String(tenantOptions[0]?.value))
|
||||
}
|
||||
} else if (!tenantOptions.find((option) => option.value === id)) {
|
||||
// if the tenant is not valid, set the first tenant as selected
|
||||
if (tenantOptions?.[0]?.value) {
|
||||
setTenant({ id: tenantOptions[0].value, refresh: true })
|
||||
} else {
|
||||
setTenant({ id: undefined, refresh: true })
|
||||
}
|
||||
} else {
|
||||
// if the tenant is in the options, set it as selected
|
||||
setSelectedTenantID(id)
|
||||
setCookie(String(id))
|
||||
}
|
||||
if (entityType !== 'document' && refresh) {
|
||||
if (!preventRefreshOnChange && refresh) {
|
||||
router.refresh()
|
||||
}
|
||||
},
|
||||
[deleteCookie, entityType, router, setCookie, tenantOptions],
|
||||
[deleteCookie, preventRefreshOnChange, router, setCookie, setSelectedTenantID, tenantOptions],
|
||||
)
|
||||
|
||||
const syncTenants = React.useCallback(async () => {
|
||||
try {
|
||||
const req = await fetch(
|
||||
`${config.serverURL}${config.routes.api}/${tenantsCollectionSlug}?select[${useAsTitle}]=true&limit=0&depth=0`,
|
||||
{
|
||||
credentials: 'include',
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
|
||||
const result = await req.json()
|
||||
|
||||
if (result.docs) {
|
||||
setTenantOptions(
|
||||
result.docs.map((doc: Record<string, number | string>) => ({
|
||||
label: doc[useAsTitle],
|
||||
value: doc.id,
|
||||
})),
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(`Error fetching tenants`)
|
||||
}
|
||||
}, [config.serverURL, config.routes.api, tenantsCollectionSlug, useAsTitle])
|
||||
|
||||
const updateTenants = React.useCallback<ContextType['updateTenants']>(
|
||||
({ id, label }) => {
|
||||
setTenantOptions((prev) => {
|
||||
return prev.map((currentTenant) => {
|
||||
if (id === currentTenant.value) {
|
||||
return {
|
||||
label,
|
||||
value: id,
|
||||
}
|
||||
const updateTenants = React.useCallback<ContextType['updateTenants']>(({ id, label }) => {
|
||||
setTenantOptions((prev) => {
|
||||
const stringID = String(id)
|
||||
let exists = false
|
||||
const updated = prev.map((currentTenant) => {
|
||||
if (stringID === String(currentTenant.value)) {
|
||||
exists = true
|
||||
return {
|
||||
label,
|
||||
value: stringID,
|
||||
}
|
||||
return currentTenant
|
||||
})
|
||||
}
|
||||
return currentTenant
|
||||
})
|
||||
|
||||
void syncTenants()
|
||||
},
|
||||
[syncTenants],
|
||||
)
|
||||
if (!exists) {
|
||||
updated.push({ label, value: stringID })
|
||||
}
|
||||
|
||||
// Sort alphabetically by label (or value as fallback)
|
||||
return updated.sort((a, b) => {
|
||||
const aKey = typeof a.label === 'string' ? a.label : String(a.value)
|
||||
const bKey = typeof b.label === 'string' ? b.label : String(b.value)
|
||||
return aKey.localeCompare(bKey)
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selectedTenantID && !tenantOptions.find((option) => option.value === selectedTenantID)) {
|
||||
if (tenantOptions?.[0]?.value) {
|
||||
setTenant({ id: tenantOptions[0].value, refresh: true })
|
||||
} else {
|
||||
setTenant({ id: undefined, refresh: true })
|
||||
}
|
||||
}
|
||||
}, [tenantCookie, setTenant, selectedTenantID, tenantOptions, initialValue, setCookie])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (userID && !tenantCookie) {
|
||||
if (tenantOptionsFromProps.length === 1) {
|
||||
// Users with no cookie set and only 1 tenant should set that tenant automatically
|
||||
setTenant({ id: tenantOptionsFromProps[0]?.value, refresh: true })
|
||||
setTenantOptions(tenantOptionsFromProps)
|
||||
} else if (
|
||||
(!tenantOptions || tenantOptions.length === 0) &&
|
||||
tenantOptionsFromProps.length > 0
|
||||
) {
|
||||
// If there are no tenant options, set them from the props
|
||||
setTenantOptions(tenantOptionsFromProps)
|
||||
}
|
||||
} else if (userID && tenantCookie) {
|
||||
if ((!tenantOptions || tenantOptions.length === 0) && tenantOptionsFromProps.length > 0) {
|
||||
// If there are no tenant options, set them from the props
|
||||
setTenantOptions(tenantOptionsFromProps)
|
||||
// User is logged in, but does not have a tenant cookie, set it
|
||||
setSelectedTenantID(initialValue)
|
||||
setTenantOptions(tenantOptionsFromProps)
|
||||
if (initialValue) {
|
||||
setCookie(String(initialValue))
|
||||
} else {
|
||||
deleteCookie()
|
||||
}
|
||||
}
|
||||
}, [
|
||||
initialValue,
|
||||
selectedTenantID,
|
||||
tenantCookie,
|
||||
userID,
|
||||
setTenant,
|
||||
tenantOptionsFromProps,
|
||||
tenantOptions,
|
||||
])
|
||||
}, [userID, tenantCookie, initialValue, setCookie, deleteCookie, router, tenantOptionsFromProps])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!userID && tenantCookie) {
|
||||
@@ -228,14 +171,11 @@ export const TenantSelectionProviderClient = ({
|
||||
>
|
||||
<Context
|
||||
value={{
|
||||
entityType,
|
||||
modified,
|
||||
options: tenantOptions,
|
||||
preventRefreshOnChange,
|
||||
selectedTenantID,
|
||||
setEntityType,
|
||||
setModified,
|
||||
setPreventRefreshOnChange,
|
||||
setTenant,
|
||||
syncTenants,
|
||||
updateTenants,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OptionObject, Payload, TypedUser } from 'payload'
|
||||
import type { OptionObject, Payload, User } from 'payload'
|
||||
|
||||
import { cookies as getCookies } from 'next/headers.js'
|
||||
|
||||
@@ -10,7 +10,7 @@ type Args = {
|
||||
payload: Payload
|
||||
tenantsCollectionSlug: string
|
||||
useAsTitle: string
|
||||
user: TypedUser
|
||||
user: User
|
||||
}
|
||||
|
||||
export const TenantSelectionProvider = async ({
|
||||
@@ -65,8 +65,6 @@ export const TenantSelectionProvider = async ({
|
||||
initialValue={initialValue}
|
||||
tenantCookie={tenantCookie}
|
||||
tenantOptions={tenantOptions}
|
||||
tenantsCollectionSlug={tenantsCollectionSlug}
|
||||
useAsTitle={useAsTitle}
|
||||
>
|
||||
{children}
|
||||
</TenantSelectionProviderClient>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { PaginatedDocs, Payload, TypedUser } from 'payload'
|
||||
import type { PaginatedDocs, Payload, User } from 'payload'
|
||||
|
||||
type Args = {
|
||||
limit: number
|
||||
payload: Payload
|
||||
tenantsCollectionSlug: string
|
||||
useAsTitle: string
|
||||
user?: TypedUser
|
||||
user?: User
|
||||
}
|
||||
export const findTenantOptions = async ({
|
||||
limit,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AcceptedLanguages } from '@payloadcms/translations'
|
||||
import type { ArrayField, CollectionSlug, Field, RelationshipField, TypedUser } from 'payload'
|
||||
import type { ArrayField, CollectionSlug, Field, RelationshipField, User } from 'payload'
|
||||
|
||||
export type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
/**
|
||||
@@ -138,7 +138,7 @@ export type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
* Useful for super-admin type users
|
||||
*/
|
||||
userHasAccessToAllTenants?: (
|
||||
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : TypedUser,
|
||||
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
|
||||
) => boolean
|
||||
/**
|
||||
* Opt out of adding access constraints to the tenants collection
|
||||
@@ -165,4 +165,4 @@ export type UserWithTenantsField = {
|
||||
tenant: number | string | Tenant
|
||||
}[]
|
||||
| null
|
||||
} & TypedUser
|
||||
} & User
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Payload, TypedUser, ViewTypes } from 'payload'
|
||||
import type { Payload, User, ViewTypes } from 'payload'
|
||||
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
|
||||
@@ -15,7 +15,7 @@ type Args = {
|
||||
tenantFieldName: string
|
||||
tenantsCollectionSlug: string
|
||||
useAsTitle: string
|
||||
user?: TypedUser
|
||||
user?: User
|
||||
view: ViewTypes
|
||||
}
|
||||
export async function getGlobalViewRedirect({
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
AccessResult,
|
||||
AllOperations,
|
||||
CollectionConfig,
|
||||
TypedUser,
|
||||
User,
|
||||
Where,
|
||||
} from 'payload'
|
||||
|
||||
@@ -53,7 +53,7 @@ export const withTenantAccess =
|
||||
args.req.user &&
|
||||
args.req.user.collection === adminUsersSlug &&
|
||||
!userHasAccessToAllTenants(
|
||||
args.req.user as ConfigType extends { user: unknown } ? ConfigType['user'] : TypedUser,
|
||||
args.req.user as ConfigType extends { user: unknown } ? ConfigType['user'] : User,
|
||||
)
|
||||
) {
|
||||
const tenantConstraint = getTenantAccess({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-nested-docs",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "The official Nested Docs plugin for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-redirects",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Redirects plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -28,7 +28,6 @@ export const redirectsPlugin =
|
||||
index: true,
|
||||
label: 'From URL',
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
{
|
||||
name: 'to',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-search",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Search plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-sentry",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Sentry plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-seo",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "SEO plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-stripe",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Stripe plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
SanitizedConfig,
|
||||
TextField,
|
||||
TextFieldSingleValidation,
|
||||
TypedUser,
|
||||
User,
|
||||
} from 'payload'
|
||||
|
||||
import type { LinkFields } from '../nodes/types.js'
|
||||
@@ -120,7 +120,7 @@ export const getBaseFields = (
|
||||
? ({ relationTo, user }) => {
|
||||
const hidden = config.collections.find(({ slug }) => slug === relationTo)?.admin
|
||||
.hidden
|
||||
if (typeof hidden === 'function' && hidden({ user } as { user: TypedUser })) {
|
||||
if (typeof hidden === 'function' && hidden({ user } as { user: User })) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -67,16 +67,6 @@ $lexical-contenteditable-bottom-padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
.rich-text-lexical {
|
||||
&.error {
|
||||
> .rich-text-lexical__wrap {
|
||||
@include lightInputError;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='dark'] {
|
||||
@@ -91,15 +81,5 @@ $lexical-contenteditable-bottom-padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
.rich-text-lexical {
|
||||
&.error {
|
||||
> .rich-text-lexical__wrap {
|
||||
@include darkInputError;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-slate",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "The officially supported Slate richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Field, SanitizedConfig, TypedUser } from 'payload'
|
||||
import type { Field, SanitizedConfig, User } from 'payload'
|
||||
|
||||
export const getBaseFields = (config: SanitizedConfig): Field[] => [
|
||||
{
|
||||
@@ -47,7 +47,7 @@ export const getBaseFields = (config: SanitizedConfig): Field[] => [
|
||||
type: 'relationship',
|
||||
filterOptions: ({ relationTo, user }) => {
|
||||
const hidden = config.collections.find(({ slug }) => slug === relationTo).admin.hidden
|
||||
if (typeof hidden === 'function' && hidden({ user } as { user: TypedUser })) {
|
||||
if (typeof hidden === 'function' && hidden({ user } as { user: User })) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-azure",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Payload storage adapter for Azure Blob Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-gcs",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Payload storage adapter for Google Cloud Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-s3",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Payload storage adapter for Amazon S3",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-uploadthing",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Payload storage adapter for uploadthing",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-vercel-blob",
|
||||
"version": "3.45.0",
|
||||
"version": "3.44.0",
|
||||
"description": "Payload storage adapter for Vercel Blob Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user