Compare commits

...

17 Commits

Author SHA1 Message Date
Elliot DeNolf
87a1d698b2 chore(release): payload/2.14.0 [skip ci] 2024-04-24 15:40:04 -04:00
Dan Ribbens
c11600aac3 fix: bulk publish (#6006) 2024-04-24 15:04:50 -04:00
Elliot DeNolf
ad01c6784d fix: header filters (#5997) 2024-04-24 11:01:24 -04:00
Elliot DeNolf
62601c54a7 chore: update .vscode/settings.json 2024-04-24 08:59:45 -04:00
Elliot DeNolf
4a144ddc44 ci: register pr-title workflow 2024-04-23 23:31:13 -04:00
Patrik
9152a238d2 fix(db-postgres): row table names were not being built properly - v2 (#5961) 2024-04-22 16:56:03 -04:00
Mike Keefe
fc8b835264 docs: fix typo in admin custom components docs (#5944) 2024-04-21 20:39:32 -04:00
Elliot DeNolf
28ee5e34c3 chore(readme): add 3.0 beta announcement [skip ci] 2024-04-21 16:54:26 -04:00
Ricardo Domingues
e25886649f fix(db-postgres): Fixes nested groups inside nested blocks (#5882)
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2024-04-21 00:18:25 -04:00
Rafał Nawojczyk
985796be54 fix: min/max attributes missing from number input (#5779) 2024-04-20 23:36:32 -04:00
Dan Ribbens
bd8b5123b0 fix(db-postgres): extra version suffix added to table names (#5939) 2024-04-20 23:23:31 -04:00
Ritsu
c380deee4a feat: add count operation to collections (#5936) 2024-04-20 23:05:37 -04:00
Elliot DeNolf
0b12aac895 ci: bump actions node version (#5103) 2024-04-20 08:54:12 -04:00
Christian Gil
90d3f178ab feat(live-preview-vue): Vue Hook for Live Preview (#5925) 2024-04-20 06:45:18 -04:00
Patrik
a8c9625cde fix: removes equals & not_equals operators from fields with hasMany (#5885) 2024-04-19 11:41:39 -04:00
Elliot DeNolf
938d069523 chore(release): plugin-seo/2.3.1 [skip ci] 2024-04-19 11:38:41 -04:00
Elliot DeNolf
1a337ec223 chore(release): richtext-lexical/0.9.0 [skip ci] 2024-04-19 11:38:09 -04:00
60 changed files with 2080 additions and 1195 deletions

View File

@@ -52,14 +52,14 @@ jobs:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 18
uses: actions/setup-node@v3
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- name: Install pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false
@@ -69,7 +69,7 @@ jobs:
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
@@ -82,7 +82,7 @@ jobs:
- run: pnpm run build
- name: Cache build
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -108,19 +108,19 @@ jobs:
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 18
uses: actions/setup-node@v3
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- name: Install pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -193,19 +193,19 @@ jobs:
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 18
uses: actions/setup-node@v3
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- name: Install pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -234,19 +234,19 @@ jobs:
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 18
uses: actions/setup-node@v3
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- name: Install pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -278,19 +278,19 @@ jobs:
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 18
uses: actions/setup-node@v3
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- name: Install pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -319,19 +319,19 @@ jobs:
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 18
uses: actions/setup-node@v3
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- name: Install pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -361,10 +361,10 @@ jobs:
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 18
uses: actions/setup-node@v3
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.10.0

11
.github/workflows/pr-title.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
name: pr-title
on:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Echo
run: echo "Register pr-title workflow"

View File

@@ -5,21 +5,21 @@
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
}
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
}
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
}
},
"[json]": {

View File

@@ -1,3 +1,20 @@
## [2.14.0](https://github.com/payloadcms/payload/compare/v2.13.0...v2.14.0) (2024-04-24)
### Features
* add count operation to collections ([#5936](https://github.com/payloadcms/payload/issues/5936)) ([c380dee](https://github.com/payloadcms/payload/commit/c380deee4a1db82bce9fea264060000957a53eee))
### Bug Fixes
* bulk publish ([#6006](https://github.com/payloadcms/payload/issues/6006)) ([c11600a](https://github.com/payloadcms/payload/commit/c11600aac38cd67019765faf2a41e62df13e50cc))
* **db-postgres:** extra version suffix added to table names ([#5939](https://github.com/payloadcms/payload/issues/5939)) ([bd8b512](https://github.com/payloadcms/payload/commit/bd8b5123b0991e53eb209315897dbca10d14d45e))
* **db-postgres:** Fixes nested groups inside nested blocks ([#5882](https://github.com/payloadcms/payload/issues/5882)) ([e258866](https://github.com/payloadcms/payload/commit/e25886649fce414d5d47918f35ba2d4d2ba59174))
* **db-postgres:** row table names were not being built properly - v2 ([#5961](https://github.com/payloadcms/payload/issues/5961)) ([9152a23](https://github.com/payloadcms/payload/commit/9152a238d2982503e7f509350651b0ba3f83b1ec))
* header filters ([#5997](https://github.com/payloadcms/payload/issues/5997)) ([ad01c67](https://github.com/payloadcms/payload/commit/ad01c6784d283386dc819dfcd47455cad5accfaa))
* min/max attributes missing from number input ([#5779](https://github.com/payloadcms/payload/issues/5779)) ([985796b](https://github.com/payloadcms/payload/commit/985796be54b593af0a4934685ab8621b9badda10))
* removes `equals` & `not_equals` operators from fields with `hasMany` ([#5885](https://github.com/payloadcms/payload/issues/5885)) ([a8c9625](https://github.com/payloadcms/payload/commit/a8c9625cdec33476a5da87bcd9f010f9d7fb9a94))
## [2.13.0](https://github.com/payloadcms/payload/compare/v2.12.1...v2.13.0) (2024-04-19)

View File

@@ -17,7 +17,7 @@
<hr/>
> [!IMPORTANT]
> 🎉 <strong>Payload 2.0 is now available!</strong> Read more in the <a target="_blank" href="https://payloadcms.com/blog/payload-2-0" rel="dofollow"><strong>announcement post</strong></a>.
> 🎉 <strong>Payload 3.0 beta announced!</strong> Read more in the <a target="_blank" href="https://payloadcms.com/blog/30-beta-install-payload-into-any-nextjs-app-with-one-line" rel="dofollow"><strong>announcement post</strong></a>.
<h3>Benefits over a regular CMS</h3>
<ul>

View File

@@ -657,7 +657,7 @@ As your admin customizations gets more complex you may want to share state betwe
### Styling Custom Components
Payload exports its SCSS variables and mixins for reuse in your own custom components. This is helpful in cases where you might want to style a custom input similarly to Payload's built-ini styling, so it blends more thoroughly into the existing admin UI.
Payload exports its SCSS variables and mixins for reuse in your own custom components. This is helpful in cases where you might want to style a custom input similarly to Payload's built-in styling, so it blends more thoroughly into the existing admin UI.
To make use of Payload SCSS variables / mixins to use directly in your own components, you can import them as follows:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import { createDatabaseAdapter } from 'payload/database'
import type { Args, PostgresAdapter, PostgresAdapterResult } from './types'
import { connect } from './connect'
import { count } from './count'
import { create } from './create'
import { createGlobal } from './createGlobal'
import { createGlobalVersion } from './createGlobalVersion'
@@ -70,6 +71,7 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
beginTransaction,
commitTransaction,
connect,
count,
create,
createGlobal,
createGlobalVersion,

View File

@@ -228,7 +228,6 @@ export const traverseFields = ({
prefix: `enum_${newTableName}_`,
target: 'enumName',
throwValidationError,
versions,
})
adapter.enums[enumName] = pgEnum(
@@ -249,7 +248,6 @@ export const traverseFields = ({
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
versions,
})
const baseColumns: Record<string, PgColumnBuilder> = {
order: integer('order').notNull(),
@@ -659,7 +657,7 @@ export const traverseFields = ({
indexes,
localesColumns,
localesIndexes,
newTableName: parentTableName,
newTableName,
parentTableName,
relationsToBuild,
relationships,

View File

@@ -28,7 +28,7 @@ const getFlattenedFieldNames = (
}
if (fieldHasSubFields(field)) {
fieldPrefix = 'name' in field ? `${prefix}${field.name}.` : prefix
fieldPrefix = 'name' in field ? `${prefix}${field.name}_` : prefix
return [...fieldsToUse, ...getFlattenedFieldNames(field.fields, fieldPrefix)]
}
@@ -36,7 +36,7 @@ const getFlattenedFieldNames = (
return [
...fieldsToUse,
...field.tabs.reduce((tabFields, tab) => {
fieldPrefix = 'name' in tab ? `${prefix}.${tab.name}` : prefix
fieldPrefix = 'name' in tab ? `${prefix}_${tab.name}` : prefix
return [
...tabFields,
...(tabHasName(tab)
@@ -51,7 +51,7 @@ const getFlattenedFieldNames = (
return [
...fieldsToUse,
{
name: `${fieldPrefix?.replace('.', '_') || ''}${field.name}`,
name: `${fieldPrefix}${field.name}`,
localized: field.localized,
},
]
@@ -84,7 +84,11 @@ export const validateExistingBlockIsIdentical = ({
if (missingField) {
throw new InvalidConfiguration(
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One block includes the field ${typeof missingField === 'string' ? missingField : missingField.name}, while the other block does not.`,
`The table ${rootTableName} has multiple blocks with slug ${
block.slug
}, but the schemas do not match. One block includes the field ${
typeof missingField === 'string' ? missingField : missingField.name
}, while the other block does not.`,
)
}

View File

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

View File

@@ -0,0 +1,37 @@
/** @type {import('prettier').Config} */
module.exports = {
extends: ['@payloadcms'],
overrides: [
{
extends: ['plugin:@typescript-eslint/disable-type-checked'],
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
},
{
files: ['package.json', 'tsconfig.json'],
rules: {
'perfectionist/sort-array-includes': 'off',
'perfectionist/sort-astro-attributes': 'off',
'perfectionist/sort-classes': 'off',
'perfectionist/sort-enums': 'off',
'perfectionist/sort-exports': 'off',
'perfectionist/sort-imports': 'off',
'perfectionist/sort-interfaces': 'off',
'perfectionist/sort-jsx-props': 'off',
'perfectionist/sort-keys': 'off',
'perfectionist/sort-maps': 'off',
'perfectionist/sort-named-exports': 'off',
'perfectionist/sort-named-imports': 'off',
'perfectionist/sort-object-types': 'off',
'perfectionist/sort-objects': 'off',
'perfectionist/sort-svelte-attributes': 'off',
'perfectionist/sort-union-types': 'off',
'perfectionist/sort-vue-attributes': 'off',
},
},
],
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
root: true,
}

View File

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

View File

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

View File

@@ -0,0 +1,49 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "0.1.0",
"description": "The official live preview Vue SDK for Payload",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/live-preview-vue"
},
"license": "MIT",
"homepage": "https://payloadcms.com",
"author": "Payload CMS, Inc.",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf {dist,*.tsbuildinfo}",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"prepublishOnly": "pnpm clean && pnpm build"
},
"dependencies": {
"@payloadcms/live-preview": "workspace:^0.x"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"vue": "^3.0.0",
"payload": "workspace:*"
},
"peerDependencies": {
"vue": "^3.0.0"
},
"exports": {
".": {
"default": "./src/index.ts",
"types": "./src/index.ts"
}
},
"publishConfig": {
"exports": null,
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
},
"files": [
"dist"
]
}

View File

@@ -0,0 +1,58 @@
import type { Ref } from 'vue'
import { ready, subscribe, unsubscribe } from '@payloadcms/live-preview'
import { onMounted, onUnmounted, ref } from 'vue'
/**
* Vue composable to implement Payload CMS Live Preview.
*
* {@link https://payloadcms.com/docs/live-preview/frontend View the documentation}
*/
export const useLivePreview = <T>(props: {
apiRoute?: string
depth?: number
initialData: T
serverURL: string
}): {
data: Ref<T>
isLoading: Ref<boolean>
} => {
const { apiRoute, depth, initialData, serverURL } = props
const data = ref(initialData) as Ref<T>
const isLoading = ref(true)
const hasSentReadyMessage = ref(false)
const onChange = (mergedData: T) => {
data.value = mergedData
isLoading.value = false
}
let subscription: (event: MessageEvent) => void
onMounted(() => {
subscription = subscribe({
apiRoute,
callback: onChange,
depth,
initialData,
serverURL,
})
if (!hasSentReadyMessage.value) {
hasSentReadyMessage.value = true
ready({
serverURL,
})
}
})
onUnmounted(() => {
unsubscribe(subscription)
})
return {
data,
isLoading,
}
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.13.0",
"version": "2.14.0",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",

View File

@@ -77,6 +77,15 @@ const contains = {
value: 'contains',
}
const filterOperators = (operators, hasMany = false) => {
if (hasMany) {
return operators.filter(
(operator) => operator.value !== 'equals' && operator.value !== 'not_equals',
)
}
return operators
}
const fieldTypeConditions = {
checkbox: {
component: 'Text',
@@ -100,7 +109,7 @@ const fieldTypeConditions = {
},
number: {
component: 'Number',
operators: [...base, ...numeric],
operators: (hasMany) => filterOperators([...base, ...numeric], hasMany),
},
point: {
component: 'Point',
@@ -120,11 +129,11 @@ const fieldTypeConditions = {
},
select: {
component: 'Select',
operators: [...base],
operators: (hasMany) => filterOperators([...base], hasMany),
},
text: {
component: 'Text',
operators: [...base, like, contains],
operators: (hasMany) => filterOperators([...base, like, contains], hasMany),
},
textarea: {
component: 'Text',

View File

@@ -22,36 +22,44 @@ const baseClass = 'where-builder'
const reduceFields = (fields, i18n) =>
flattenTopLevelFields(fields).reduce((reduced, field) => {
let operators = []
if (typeof fieldTypes[field.type] === 'object') {
const operatorKeys = new Set()
const operators = fieldTypes[field.type].operators.reduce((acc, operator) => {
if (!operatorKeys.has(operator.value)) {
operatorKeys.add(operator.value)
return [
...acc,
{
...operator,
label: i18n.t(`operators:${operator.label}`),
},
]
}
return acc
}, [])
const formattedField = {
label: getTranslation(field.label || field.name, i18n),
value: field.name,
...fieldTypes[field.type],
operators,
props: {
...field,
},
if (typeof fieldTypes[field.type].operators === 'function') {
operators = fieldTypes[field.type].operators(
'hasMany' in field && field.hasMany ? true : false,
)
} else {
operators = fieldTypes[field.type].operators
}
return [...reduced, formattedField]
}
return reduced
const operatorKeys = new Set()
const filteredOperators = operators.reduce((acc, operator) => {
if (!operatorKeys.has(operator.value)) {
operatorKeys.add(operator.value)
return [
...acc,
{
...operator,
label: i18n.t(`operators:${operator.label}`),
},
]
}
return acc
}, [])
const formattedField = {
label: getTranslation(field.label || field.name, i18n),
value: field.name,
...fieldTypes[field.type],
operators: filteredOperators,
props: {
...field,
},
}
return [...reduced, formattedField]
}, [])
/**
@@ -185,7 +193,7 @@ const WhereBuilder: React.FC<Props> = (props) => {
iconStyle="with-border"
onClick={() => {
if (reducedFields.length > 0)
dispatchConditions({ field: reducedFields[0].value, type: 'add' })
dispatchConditions({ type: 'add', field: reducedFields[0].value })
}}
>
{t('or')}
@@ -203,7 +211,7 @@ const WhereBuilder: React.FC<Props> = (props) => {
iconStyle="with-border"
onClick={() => {
if (reducedFields.length > 0)
dispatchConditions({ field: reducedFields[0].value, type: 'add' })
dispatchConditions({ type: 'add', field: reducedFields[0].value })
}}
>
{t('addFilter')}

View File

@@ -166,6 +166,8 @@ const NumberField: React.FC<Props> = (props) => {
<input
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
max={max}
min={min}
name={path}
onChange={handleChange}
onWheel={(e) => {

View File

@@ -11,6 +11,7 @@ import registerFirstUserHandler from '../auth/requestHandlers/registerFirstUser'
import resetPassword from '../auth/requestHandlers/resetPassword'
import unlock from '../auth/requestHandlers/unlock'
import verifyEmail from '../auth/requestHandlers/verifyEmail'
import count from './requestHandlers/count'
import create from './requestHandlers/create'
import deleteHandler from './requestHandlers/delete'
import deleteByID from './requestHandlers/deleteByID'
@@ -124,6 +125,11 @@ const buildEndpoints = (collection: SanitizedCollectionConfig): Endpoint[] => {
method: 'post',
path: '/',
},
{
handler: count,
method: 'get',
path: '/count',
},
{
handler: docAccessRequestHandler,
method: 'get',

View File

@@ -30,6 +30,7 @@ import type { AfterOperationArg, AfterOperationMap } from '../operations/utils'
export type HookOperationType =
| 'autosave'
| 'count'
| 'create'
| 'delete'
| 'forgotPassword'
@@ -465,6 +466,7 @@ export type Collection = {
config: SanitizedCollectionConfig
graphQL?: {
JWT: GraphQLObjectType
countType: GraphQLObjectType
mutationInputType: GraphQLNonNull<any>
paginatedType: GraphQLObjectType
type: GraphQLObjectType

View File

@@ -30,8 +30,10 @@ import buildPaginatedListType from '../../graphql/schema/buildPaginatedListType'
import { buildPolicyType } from '../../graphql/schema/buildPoliciesType'
import buildWhereInputType from '../../graphql/schema/buildWhereInputType'
import formatName from '../../graphql/utilities/formatName'
import flattenFields from '../../utilities/flattenTopLevelFields'
import { formatNames, toWords } from '../../utilities/formatLabels'
import { buildVersionCollectionFields } from '../../versions/buildCollectionFields'
import countResolver from './resolvers/count'
import createResolver from './resolvers/create'
import getDeleteResolver from './resolvers/delete'
import { docAccessResolver } from './resolvers/docAccess'
@@ -41,7 +43,6 @@ import findVersionByIDResolver from './resolvers/findVersionByID'
import findVersionsResolver from './resolvers/findVersions'
import restoreVersionResolver from './resolvers/restoreVersion'
import updateResolver from './resolvers/update'
import flattenFields from '../../utilities/flattenTopLevelFields'
function initCollectionsGraphQL(payload: Payload): void {
Object.keys(payload.collections).forEach((slug) => {
@@ -118,9 +119,9 @@ function initCollectionsGraphQL(payload: Payload): void {
if (config.auth && !config.auth.disableLocalStrategy) {
fields.push({
name: 'password',
type: 'text',
label: 'Password',
required: true,
type: 'text',
})
}
@@ -146,6 +147,7 @@ function initCollectionsGraphQL(payload: Payload): void {
}
payload.Query.fields[singularName] = {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(idType) },
draft: { type: GraphQLBoolean },
@@ -157,10 +159,10 @@ function initCollectionsGraphQL(payload: Payload): void {
: {}),
},
resolve: findByIDResolver(collection),
type: collection.graphQL.type,
}
payload.Query.fields[pluralName] = {
type: buildPaginatedListType(pluralName, collection.graphQL.type),
args: {
draft: { type: GraphQLBoolean },
where: { type: collection.graphQL.whereInputType },
@@ -175,23 +177,42 @@ function initCollectionsGraphQL(payload: Payload): void {
sort: { type: GraphQLString },
},
resolve: findResolver(collection),
type: buildPaginatedListType(pluralName, collection.graphQL.type),
}
payload.Query.fields[`count${pluralName}`] = {
type: new GraphQLObjectType({
name: `count${pluralName}`,
fields: {
totalDocs: { type: GraphQLInt },
},
}),
args: {
draft: { type: GraphQLBoolean },
where: { type: collection.graphQL.whereInputType },
...(payload.config.localization
? {
locale: { type: payload.types.localeInputType },
}
: {}),
},
resolve: countResolver(collection),
}
payload.Query.fields[`docAccess${singularName}`] = {
type: buildPolicyType({
type: 'collection',
entity: config,
scope: 'docAccess',
typeSuffix: 'DocAccess',
}),
args: {
id: { type: new GraphQLNonNull(idType) },
},
resolve: docAccessResolver(),
type: buildPolicyType({
entity: config,
scope: 'docAccess',
type: 'collection',
typeSuffix: 'DocAccess',
}),
}
payload.Mutation.fields[`create${singularName}`] = {
type: collection.graphQL.type,
args: {
...(createMutationInputType
? { data: { type: collection.graphQL.mutationInputType } }
@@ -204,10 +225,10 @@ function initCollectionsGraphQL(payload: Payload): void {
: {}),
},
resolve: createResolver(collection),
type: collection.graphQL.type,
}
payload.Mutation.fields[`update${singularName}`] = {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(idType) },
autosave: { type: GraphQLBoolean },
@@ -222,15 +243,14 @@ function initCollectionsGraphQL(payload: Payload): void {
: {}),
},
resolve: updateResolver(collection),
type: collection.graphQL.type,
}
payload.Mutation.fields[`delete${singularName}`] = {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(idType) },
},
resolve: getDeleteResolver(collection),
type: collection.graphQL.type,
}
if (config.versions) {
@@ -243,13 +263,13 @@ function initCollectionsGraphQL(payload: Payload): void {
},
{
name: 'createdAt',
label: 'Created At',
type: 'date',
label: 'Created At',
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'date',
label: 'Updated At',
},
]
@@ -262,6 +282,7 @@ function initCollectionsGraphQL(payload: Payload): void {
})
payload.Query.fields[`version${formatName(singularName)}`] = {
type: collection.graphQL.versionType,
args: {
id: { type: versionIDType },
...(payload.config.localization
@@ -272,9 +293,12 @@ function initCollectionsGraphQL(payload: Payload): void {
: {}),
},
resolve: findVersionByIDResolver(collection),
type: collection.graphQL.versionType,
}
payload.Query.fields[`versions${pluralName}`] = {
type: buildPaginatedListType(
`versions${formatName(pluralName)}`,
collection.graphQL.versionType,
),
args: {
where: {
type: buildWhereInputType({
@@ -295,17 +319,13 @@ function initCollectionsGraphQL(payload: Payload): void {
sort: { type: GraphQLString },
},
resolve: findVersionsResolver(collection),
type: buildPaginatedListType(
`versions${formatName(pluralName)}`,
collection.graphQL.versionType,
),
}
payload.Mutation.fields[`restoreVersion${formatName(singularName)}`] = {
type: collection.graphQL.type,
args: {
id: { type: versionIDType },
},
resolve: restoreVersionResolver(collection),
type: collection.graphQL.type,
}
}
@@ -315,8 +335,8 @@ function initCollectionsGraphQL(payload: Payload): void {
: [
{
name: 'email',
required: true,
type: 'email',
required: true,
},
]
collection.graphQL.JWT = buildObjectType({
@@ -326,8 +346,8 @@ function initCollectionsGraphQL(payload: Payload): void {
...authFields,
{
name: 'collection',
required: true,
type: 'text',
required: true,
},
],
parentName: formatName(`${slug}JWT`),
@@ -335,7 +355,6 @@ function initCollectionsGraphQL(payload: Payload): void {
})
payload.Query.fields[`me${singularName}`] = {
resolve: me(collection),
type: new GraphQLObjectType({
name: formatName(`${slug}Me`),
fields: {
@@ -353,18 +372,15 @@ function initCollectionsGraphQL(payload: Payload): void {
},
},
}),
resolve: me(collection),
}
payload.Query.fields[`initialized${singularName}`] = {
resolve: init(collection.config.slug),
type: GraphQLBoolean,
resolve: init(collection.config.slug),
}
payload.Mutation.fields[`refreshToken${singularName}`] = {
args: {
token: { type: GraphQLString },
},
resolve: refresh(collection),
type: new GraphQLObjectType({
name: formatName(`${slug}Refreshed${singularName}`),
fields: {
@@ -379,30 +395,29 @@ function initCollectionsGraphQL(payload: Payload): void {
},
},
}),
args: {
token: { type: GraphQLString },
},
resolve: refresh(collection),
}
payload.Mutation.fields[`logout${singularName}`] = {
resolve: logout(collection),
type: GraphQLString,
resolve: logout(collection),
}
if (!config.auth.disableLocalStrategy) {
if (config.auth.maxLoginAttempts > 0) {
payload.Mutation.fields[`unlock${singularName}`] = {
type: new GraphQLNonNull(GraphQLBoolean),
args: {
email: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: unlock(collection),
type: new GraphQLNonNull(GraphQLBoolean),
}
}
payload.Mutation.fields[`login${singularName}`] = {
args: {
email: { type: GraphQLString },
password: { type: GraphQLString },
},
resolve: login(collection),
type: new GraphQLObjectType({
name: formatName(`${slug}LoginResult`),
fields: {
@@ -417,24 +432,24 @@ function initCollectionsGraphQL(payload: Payload): void {
},
},
}),
args: {
email: { type: GraphQLString },
password: { type: GraphQLString },
},
resolve: login(collection),
}
payload.Mutation.fields[`forgotPassword${singularName}`] = {
type: new GraphQLNonNull(GraphQLBoolean),
args: {
disableEmail: { type: GraphQLBoolean },
email: { type: new GraphQLNonNull(GraphQLString) },
expiration: { type: GraphQLInt },
},
resolve: forgotPassword(collection),
type: new GraphQLNonNull(GraphQLBoolean),
}
payload.Mutation.fields[`resetPassword${singularName}`] = {
args: {
password: { type: GraphQLString },
token: { type: GraphQLString },
},
resolve: resetPassword(collection),
type: new GraphQLObjectType({
name: formatName(`${slug}ResetPassword`),
fields: {
@@ -442,14 +457,19 @@ function initCollectionsGraphQL(payload: Payload): void {
user: { type: collection.graphQL.type },
},
}),
args: {
password: { type: GraphQLString },
token: { type: GraphQLString },
},
resolve: resetPassword(collection),
}
payload.Mutation.fields[`verifyEmail${singularName}`] = {
type: GraphQLBoolean,
args: {
token: { type: GraphQLString },
},
resolve: verifyEmail(collection),
type: GraphQLBoolean,
}
}
}

View File

@@ -0,0 +1,40 @@
import type { PayloadRequest } from '../../../express/types'
import type { Where } from '../../../types'
import type { Collection } from '../../config/types'
import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import count from '../../operations/count'
export type Resolver = (
_: unknown,
args: {
data: Record<string, unknown>
locale?: string
where?: Where
},
context: {
req: PayloadRequest
res: Response
},
) => Promise<{ totalDocs: number }>
export default function findResolver(collection: Collection): Resolver {
return async function resolver(_, args, context) {
let { req } = context
const locale = req.locale
const fallbackLocale = req.fallbackLocale
req = isolateObjectProperty(req, 'locale')
req = isolateObjectProperty(req, 'fallbackLocale')
req.locale = args.locale || locale
req.fallbackLocale = fallbackLocale
const options = {
collection,
req: isolateObjectProperty<PayloadRequest>(req, 'transactionID'),
where: args.where,
}
const results = await count(options)
return results
}
}

View File

@@ -0,0 +1,113 @@
import type { AccessResult } from '../../config/types'
import type { PayloadRequest, Where } from '../../types/index'
import type { Collection, TypeWithID } from '../config/types'
import executeAccess from '../../auth/executeAccess'
import { combineQueries } from '../../database/combineQueries'
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths'
import { commitTransaction } from '../../utilities/commitTransaction'
import { initTransaction } from '../../utilities/initTransaction'
import { killTransaction } from '../../utilities/killTransaction'
import { buildAfterOperation } from './utils'
export type Arguments = {
collection: Collection
disableErrors?: boolean
overrideAccess?: boolean
req?: PayloadRequest
where?: Where
}
async function count<T extends TypeWithID & Record<string, unknown>>(
incomingArgs: Arguments,
): Promise<{ totalDocs: number }> {
let args = incomingArgs
try {
const shouldCommit = await initTransaction(args.req)
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'count',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },
disableErrors,
overrideAccess,
req: { payload },
req,
where,
} = args
// /////////////////////////////////////
// Access
// /////////////////////////////////////
let accessResult: AccessResult
if (!overrideAccess) {
accessResult = await executeAccess({ disableErrors, req }, collectionConfig.access.read)
// If errors are disabled, and access returns false, return empty results
if (accessResult === false) {
return {
totalDocs: 0,
}
}
}
let result: { totalDocs: number }
const fullWhere = combineQueries(where, accessResult)
await validateQueryPaths({
collectionConfig,
overrideAccess,
req,
where,
})
result = await payload.db.count({
collection: collectionConfig.slug,
req,
where: fullWhere,
})
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation<T>({
args,
collection: collectionConfig,
operation: 'count',
result,
})
// /////////////////////////////////////
// Return results
// /////////////////////////////////////
if (shouldCommit) await commitTransaction(req)
return result
} catch (error: unknown) {
await killTransaction(args.req)
throw error
}
}
export default count

View File

@@ -0,0 +1,47 @@
import type { GeneratedTypes } from '../../../'
import type { PayloadRequest, RequestContext } from '../../../express/types'
import type { Payload } from '../../../payload'
import type { Document, Where } from '../../../types'
import { APIError } from '../../../errors'
import { createLocalReq } from '../../../utilities/createLocalReq'
import count from '../count'
export type Options<T extends keyof GeneratedTypes['collections']> = {
collection: T
/**
* context, which will then be passed to req.context, which can be read by hooks
*/
context?: RequestContext
disableErrors?: boolean
locale?: string
overrideAccess?: boolean
req?: PayloadRequest
user?: Document
where?: Where
}
export default async function countLocal<T extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<T>,
): Promise<{ totalDocs: number }> {
const { collection: collectionSlug, disableErrors, overrideAccess = true, where } = options
const collection = payload.collections[collectionSlug]
if (!collection) {
throw new APIError(
`The collection with slug ${String(collectionSlug)} can't be found. Find Operation.`,
)
}
const req = createLocalReq(options, payload)
return count<GeneratedTypes['collections'][T]>({
collection,
disableErrors,
overrideAccess,
req,
where,
})
}

View File

@@ -1,4 +1,5 @@
import auth from '../../../auth/operations/local'
import count from './count'
import create from './create'
import deleteLocal from './delete'
import find from './find'
@@ -10,6 +11,7 @@ import update from './update'
export default {
auth,
count,
create,
deleteLocal,
find,

View File

@@ -276,7 +276,7 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
// Update
// /////////////////////////////////////
if (!shouldSaveDraft) {
if (!shouldSaveDraft || data._status === 'published') {
result = await req.payload.db.updateOne({
id,
collection: collectionConfig.slug,

View File

@@ -262,7 +262,7 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
// Update
// /////////////////////////////////////
if (!shouldSaveDraft) {
if (!shouldSaveDraft || data._status === 'published') {
result = await req.payload.db.updateOne({
id,
collection: collectionConfig.slug,

View File

@@ -3,6 +3,7 @@ import type login from '../../auth/operations/login'
import type refresh from '../../auth/operations/refresh'
import type { PayloadRequest } from '../../express/types'
import type { AfterOperationHook, SanitizedCollectionConfig, TypeWithID } from '../config/types'
import type countOperation from './count'
import type create from './create'
import type deleteOperation from './delete'
import type deleteByID from './deleteByID'
@@ -12,6 +13,7 @@ import type update from './update'
import type updateByID from './updateByID'
export type AfterOperationMap<T extends TypeWithID> = {
count: typeof countOperation
create: typeof create // todo: pass correct generic
delete: typeof deleteOperation // todo: pass correct generic
deleteByID: typeof deleteByID // todo: pass correct generic
@@ -28,6 +30,11 @@ export type AfterOperationArg<T extends TypeWithID> = {
collection: SanitizedCollectionConfig
req: PayloadRequest
} & (
| {
args: Parameters<AfterOperationMap<T>['count']>[0]
operation: 'count'
result: Awaited<ReturnType<AfterOperationMap<T>['count']>>
}
| {
args: Parameters<AfterOperationMap<T>['create']>[0]
operation: 'create'

View File

@@ -0,0 +1,26 @@
import type { NextFunction, Response } from 'express'
import httpStatus from 'http-status'
import type { PayloadRequest } from '../../express/types'
import type { Where } from '../../types'
import count from '../operations/count'
export default async function countHandler(
req: PayloadRequest,
res: Response,
next: NextFunction,
): Promise<Response<{ totalDocs: number }> | void> {
try {
const result = await count({
collection: req.collection,
req,
where: req.query.where as Where, // This is a little shady
})
return res.status(httpStatus.OK).json(result)
} catch (error) {
return next(error)
}
}

View File

@@ -22,6 +22,8 @@ export interface BaseDatabaseAdapter {
*/
connect?: Connect
count: Count
create: Create
createGlobal: CreateGlobal
@@ -197,6 +199,15 @@ export type FindArgs = {
export type Find = <T = TypeWithID>(args: FindArgs) => Promise<PaginatedDocs<T>>
export type CountArgs = {
collection: string
locale?: string
req: PayloadRequest
where?: Where
}
export type Count = (args: CountArgs) => Promise<{ totalDocs: number }>
type BaseVersionArgs = {
limit?: number
locale?: string

View File

@@ -0,0 +1,18 @@
import type { TFunction } from 'i18next'
import httpStatus from 'http-status'
import APIError from './APIError'
class FileRetrievalError extends APIError {
constructor(t?: TFunction, message?: string) {
let msg = t ? t('error:problemUploadingFile') : 'There was a problem while retrieving the file.'
if (message) {
msg += ` ${message}`
}
super(msg, httpStatus.BAD_REQUEST)
}
}
export default FileRetrievalError

View File

@@ -3,6 +3,8 @@ export {
BeginTransaction,
CommitTransaction,
Connect,
Count,
CountArgs,
Create,
CreateArgs,
CreateGlobal,

View File

@@ -18,6 +18,7 @@ import type { Options as VerifyEmailOptions } from './auth/operations/local/veri
import type { Result as LoginResult } from './auth/operations/login'
import type { Result as ResetPasswordResult } from './auth/operations/resetPassword'
import type { BulkOperationResult, Collection } from './collections/config/types'
import type { Options as CountOptions } from './collections/operations/local/count'
import type { Options as CreateOptions } from './collections/operations/local/create'
import type {
ByIDOptions as DeleteByIDOptions,
@@ -74,6 +75,18 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
config: SanitizedConfig
/**
* @description Performs count operation
* @param options
* @returns count of documents satisfying query
*/
count = async <T extends keyof TGeneratedTypes['collections']>(
options: CountOptions<T>,
): Promise<{ totalDocs: number }> => {
const { count } = localOperations
return count(this, options)
}
/**
* @description Performs create operation
* @param options

View File

@@ -14,6 +14,7 @@ import type { PayloadRequest } from '../express/types'
import type { FileData, FileToSave, ProbedImageSize } from './types'
import { FileUploadError, MissingFile } from '../errors'
import FileRetrievalError from '../errors/FileRetrievalError'
import canResizeImage from './canResizeImage'
import cropImage from './cropImage'
import { getExternalFile } from './getExternalFile'
@@ -80,8 +81,10 @@ export const generateFileData = async <T>({
})) as UploadedFile
overwriteExistingFiles = true
}
} catch (err) {
throw new FileUploadError(req.t)
} catch (err: unknown) {
if (err instanceof Error) {
throw new FileRetrievalError(req.t, err.message)
}
}
}

View File

@@ -1,4 +1,5 @@
import type { Request } from 'express'
import type { IncomingHttpHeaders } from 'http'
import type { File, FileData, IncomingUploadType } from './types'
@@ -21,20 +22,15 @@ export const getExternalFile = async ({ data, req, uploadConfig }: Args): Promis
const { default: fetch } = (await import('node-fetch')) as any
// Convert headers
const convertedHeaders: Record<string, string> = headersToObject(req.headers)
const headers = uploadConfig.externalFileHeaderFilter
? uploadConfig.externalFileHeaderFilter(convertedHeaders)
? uploadConfig.externalFileHeaderFilter(headersToObject(req.headers))
: {
cookie: req.headers['cookie'],
}
const res = await fetch(fileURL, {
credentials: 'include',
headers: {
headers,
},
headers,
method: 'GET',
})
@@ -53,15 +49,17 @@ export const getExternalFile = async ({ data, req, uploadConfig }: Args): Promis
throw new APIError('Invalid file url', 400)
}
function headersToObject(headers) {
const headersObj = {}
headers.forEach((value, key) => {
// If the header value is an array, join its elements into a single string
if (Array.isArray(value)) {
headersObj[key] = value.join(', ')
} else {
headersObj[key] = value
}
})
return headersObj
function headersToObject(headers: IncomingHttpHeaders) {
return Object.entries(headers).reduce(
(acc, [key, value]) => {
if (Array.isArray(value)) {
acc[key] = value.join(',')
} else {
acc[key] = value
}
return acc
},
{} as Record<string, string>,
)
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-seo",
"version": "2.3.0",
"version": "2.3.1",
"homepage:": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "0.8.0",
"version": "0.9.0",
"description": "The officially supported Lexical richtext adapter for Payload",
"repository": {
"type": "git",

1221
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ import type { FieldAccess } from '../../packages/payload/src/fields/config/types
import { buildConfigWithDefaults } from '../buildConfigWithDefaults'
import { devUser } from '../credentials'
import { firstArrayText, secondArrayText } from './shared'
import { firstArrayText, hiddenAccessCountSlug, secondArrayText } from './shared'
import {
docLevelAccessSlug,
hiddenAccessSlug,
@@ -390,6 +390,32 @@ export default buildConfigWithDefaults({
},
],
},
{
slug: hiddenAccessCountSlug,
access: {
read: ({ req: { user } }) => {
if (user) return true
return {
hidden: {
not_equals: true,
},
}
},
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'hidden',
type: 'checkbox',
hidden: true,
},
],
},
],
onInit: async (payload) => {
await payload.create({

View File

@@ -7,6 +7,7 @@ import { initPayloadTest } from '../helpers/configHelpers'
import { requestHeaders } from './config'
import {
firstArrayText,
hiddenAccessCountSlug,
hiddenAccessSlug,
hiddenFieldsSlug,
relyOnRequestHeadersSlug,
@@ -417,6 +418,30 @@ describe('Access Control', () => {
expect(docs).toHaveLength(1)
})
it('should respect query constraint using hidden field on count', async () => {
await payload.create({
collection: hiddenAccessCountSlug,
data: {
title: 'hello',
},
})
await payload.create({
collection: hiddenAccessCountSlug,
data: {
title: 'hello',
hidden: true,
},
})
const { totalDocs } = await payload.count({
collection: hiddenAccessCountSlug,
overrideAccess: false,
})
expect(totalDocs).toBe(1)
})
it('should respect query constraint using hidden field on versions', async () => {
await payload.create({
collection: restrictedVersionsSlug,

View File

@@ -20,11 +20,16 @@ export interface Config {
'doc-level-access': DocLevelAccess
'hidden-fields': HiddenField
'hidden-access': HiddenAccess
'hidden-access-count': HiddenAccessCount
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
globals: {}
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: string
roles?: ('admin' | 'user')[] | null
@@ -39,6 +44,10 @@ export interface User {
lockUntil?: string | null
password: string | null
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts".
*/
export interface Post {
id: string
restrictedField?: string | null
@@ -50,6 +59,10 @@ export interface Post {
updatedAt: string
createdAt: string
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "unrestricted".
*/
export interface Unrestricted {
id: string
name?: string | null
@@ -57,24 +70,40 @@ export interface Unrestricted {
updatedAt: string
createdAt: string
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "user-restricted".
*/
export interface UserRestricted {
id: string
name?: string | null
updatedAt: string
createdAt: string
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "restricted".
*/
export interface Restricted {
id: string
name?: string | null
updatedAt: string
createdAt: string
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "read-only-collection".
*/
export interface ReadOnlyCollection {
id: string
name?: string | null
updatedAt: string
createdAt: string
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "restricted-versions".
*/
export interface RestrictedVersion {
id: string
name?: string | null
@@ -82,6 +111,10 @@ export interface RestrictedVersion {
updatedAt: string
createdAt: string
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "sibling-data".
*/
export interface SiblingDatum {
id: string
array?:
@@ -94,12 +127,20 @@ export interface SiblingDatum {
updatedAt: string
createdAt: string
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "rely-on-request-headers".
*/
export interface RelyOnRequestHeader {
id: string
name?: string | null
updatedAt: string
createdAt: string
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "doc-level-access".
*/
export interface DocLevelAccess {
id: string
approvedForRemoval?: boolean | null
@@ -108,6 +149,10 @@ export interface DocLevelAccess {
updatedAt: string
createdAt: string
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "hidden-fields".
*/
export interface HiddenField {
id: string
title?: string | null
@@ -126,6 +171,10 @@ export interface HiddenField {
updatedAt: string
createdAt: string
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "hidden-access".
*/
export interface HiddenAccess {
id: string
title: string
@@ -133,6 +182,21 @@ export interface HiddenAccess {
updatedAt: string
createdAt: string
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "hidden-access-count".
*/
export interface HiddenAccessCount {
id: string
title: string
hidden?: boolean | null
updatedAt: string
createdAt: string
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string
user: {
@@ -152,6 +216,10 @@ export interface PayloadPreference {
updatedAt: string
createdAt: string
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string
name?: string | null

View File

@@ -12,5 +12,5 @@ export const siblingDataSlug = 'sibling-data'
export const relyOnRequestHeadersSlug = 'rely-on-request-headers'
export const docLevelAccessSlug = 'doc-level-access'
export const hiddenFieldsSlug = 'hidden-fields'
export const hiddenAccessCountSlug = 'hidden-access-count'
export const hiddenAccessSlug = 'hidden-access'

View File

@@ -102,6 +102,18 @@ describe('collections-graphql', () => {
expect(docs).toContainEqual(expect.objectContaining({ id: existingDoc.id }))
})
it('should count', async () => {
const query = `query {
countPosts {
totalDocs
}
}`
const response = await client.request<{ countPosts: { totalDocs: number } }>(query)
const { totalDocs } = response.countPosts
expect(typeof totalDocs).toBe('number')
})
it('should read using multiple queries', async () => {
const query = `query {
postIDs: Posts {

View File

@@ -1,32 +1,49 @@
type Query {
User(id: String!, draft: Boolean): User
Users(draft: Boolean, where: User_where, limit: Int, page: Int, sort: String): Users
countUsers(draft: Boolean, where: User_where): countUsers
docAccessUser(id: String!): usersDocAccess
meUser: usersMe
initializedUser: Boolean
Point(id: String!, draft: Boolean): Point
Points(draft: Boolean, where: Point_where, limit: Int, page: Int, sort: String): Points
countPoints(draft: Boolean, where: Point_where): countPoints
docAccessPoint(id: String!): pointDocAccess
Post(id: String!, draft: Boolean): Post
Posts(draft: Boolean, where: Post_where, limit: Int, page: Int, sort: String): Posts
countPosts(draft: Boolean, where: Post_where): countPosts
docAccessPost(id: String!): postsDocAccess
CustomId(id: Int!, draft: Boolean): CustomId
CustomIds(draft: Boolean, where: CustomId_where, limit: Int, page: Int, sort: String): CustomIds
countCustomIds(draft: Boolean, where: CustomId_where): countCustomIds
docAccessCustomId(id: Int!): custom_idsDocAccess
Relation(id: String!, draft: Boolean): Relation
Relations(draft: Boolean, where: Relation_where, limit: Int, page: Int, sort: String): Relations
countRelations(draft: Boolean, where: Relation_where): countRelations
docAccessRelation(id: String!): relationDocAccess
Dummy(id: String!, draft: Boolean): Dummy
Dummies(draft: Boolean, where: Dummy_where, limit: Int, page: Int, sort: String): Dummies
countDummies(draft: Boolean, where: Dummy_where): countDummies
docAccessDummy(id: String!): dummyDocAccess
ErrorOnHook(id: String!, draft: Boolean): ErrorOnHook
ErrorOnHooks(draft: Boolean, where: ErrorOnHook_where, limit: Int, page: Int, sort: String): ErrorOnHooks
countErrorOnHooks(draft: Boolean, where: ErrorOnHook_where): countErrorOnHooks
docAccessErrorOnHook(id: String!): error_on_hooksDocAccess
PayloadApiTestOne(id: String!, draft: Boolean): PayloadApiTestOne
PayloadApiTestOnes(draft: Boolean, where: PayloadApiTestOne_where, limit: Int, page: Int, sort: String): PayloadApiTestOnes
countPayloadApiTestOnes(draft: Boolean, where: PayloadApiTestOne_where): countPayloadApiTestOnes
docAccessPayloadApiTestOne(id: String!): payload_api_test_onesDocAccess
PayloadApiTestTwo(id: String!, draft: Boolean): PayloadApiTestTwo
PayloadApiTestTwos(draft: Boolean, where: PayloadApiTestTwo_where, limit: Int, page: Int, sort: String): PayloadApiTestTwos
countPayloadApiTestTwos(draft: Boolean, where: PayloadApiTestTwo_where): countPayloadApiTestTwos
docAccessPayloadApiTestTwo(id: String!): payload_api_test_twosDocAccess
ContentType(id: String!, draft: Boolean): ContentType
ContentTypes(draft: Boolean, where: ContentType_where, limit: Int, page: Int, sort: String): ContentTypes
countContentTypes(draft: Boolean, where: ContentType_where): countContentTypes
docAccessContentType(id: String!): content_typeDocAccess
PayloadPreference(id: String!, draft: Boolean): PayloadPreference
PayloadPreferences(draft: Boolean, where: PayloadPreference_where, limit: Int, page: Int, sort: String): PayloadPreferences
countPayloadPreferences(draft: Boolean, where: PayloadPreference_where): countPayloadPreferences
docAccessPayloadPreference(id: String!): payload_preferencesDocAccess
Access: Access
QueryWithInternalError: QueryWithInternalError
@@ -140,6 +157,10 @@ input User_where_or {
OR: [User_where_or]
}
type countUsers {
totalDocs: Int
}
type usersDocAccess {
fields: UsersDocAccessFields
create: UsersCreateDocAccess
@@ -389,6 +410,10 @@ input Point_where_or {
OR: [Point_where_or]
}
type countPoints {
totalDocs: Int
}
type pointDocAccess {
fields: PointDocAccessFields
create: PointCreateDocAccess
@@ -842,6 +867,10 @@ input Post_where_or {
OR: [Post_where_or]
}
type countPosts {
totalDocs: Int
}
type postsDocAccess {
fields: PostsDocAccessFields
create: PostsCreateDocAccess
@@ -1509,6 +1538,10 @@ input CustomId_where_or {
OR: [CustomId_where_or]
}
type countCustomIds {
totalDocs: Int
}
type custom_idsDocAccess {
fields: CustomIdsDocAccessFields
create: CustomIdsCreateDocAccess
@@ -1721,6 +1754,10 @@ input Relation_where_or {
OR: [Relation_where_or]
}
type countRelations {
totalDocs: Int
}
type relationDocAccess {
fields: RelationDocAccessFields
create: RelationCreateDocAccess
@@ -1909,6 +1946,10 @@ input Dummy_where_or {
OR: [Dummy_where_or]
}
type countDummies {
totalDocs: Int
}
type dummyDocAccess {
fields: DummyDocAccessFields
create: DummyCreateDocAccess
@@ -2012,6 +2053,239 @@ type DummyDeleteDocAccess {
where: JSONObject
}
type ErrorOnHook {
id: String
title: String
errorBeforeChange: Boolean
updatedAt: DateTime
createdAt: DateTime
}
type ErrorOnHooks {
docs: [ErrorOnHook]
hasNextPage: Boolean
hasPrevPage: Boolean
limit: Int
nextPage: Int
offset: Int
page: Int
pagingCounter: Int
prevPage: Int
totalDocs: Int
totalPages: Int
}
input ErrorOnHook_where {
title: ErrorOnHook_title_operator
errorBeforeChange: ErrorOnHook_errorBeforeChange_operator
updatedAt: ErrorOnHook_updatedAt_operator
createdAt: ErrorOnHook_createdAt_operator
id: ErrorOnHook_id_operator
AND: [ErrorOnHook_where_and]
OR: [ErrorOnHook_where_or]
}
input ErrorOnHook_title_operator {
equals: String
not_equals: String
like: String
contains: String
in: [String]
not_in: [String]
all: [String]
exists: Boolean
}
input ErrorOnHook_errorBeforeChange_operator {
equals: Boolean
not_equals: Boolean
exists: Boolean
}
input ErrorOnHook_updatedAt_operator {
equals: DateTime
not_equals: DateTime
greater_than_equal: DateTime
greater_than: DateTime
less_than_equal: DateTime
less_than: DateTime
like: DateTime
exists: Boolean
}
input ErrorOnHook_createdAt_operator {
equals: DateTime
not_equals: DateTime
greater_than_equal: DateTime
greater_than: DateTime
less_than_equal: DateTime
less_than: DateTime
like: DateTime
exists: Boolean
}
input ErrorOnHook_id_operator {
equals: String
not_equals: String
like: String
contains: String
in: [String]
not_in: [String]
all: [String]
exists: Boolean
}
input ErrorOnHook_where_and {
title: ErrorOnHook_title_operator
errorBeforeChange: ErrorOnHook_errorBeforeChange_operator
updatedAt: ErrorOnHook_updatedAt_operator
createdAt: ErrorOnHook_createdAt_operator
id: ErrorOnHook_id_operator
AND: [ErrorOnHook_where_and]
OR: [ErrorOnHook_where_or]
}
input ErrorOnHook_where_or {
title: ErrorOnHook_title_operator
errorBeforeChange: ErrorOnHook_errorBeforeChange_operator
updatedAt: ErrorOnHook_updatedAt_operator
createdAt: ErrorOnHook_createdAt_operator
id: ErrorOnHook_id_operator
AND: [ErrorOnHook_where_and]
OR: [ErrorOnHook_where_or]
}
type countErrorOnHooks {
totalDocs: Int
}
type error_on_hooksDocAccess {
fields: ErrorOnHooksDocAccessFields
create: ErrorOnHooksCreateDocAccess
read: ErrorOnHooksReadDocAccess
update: ErrorOnHooksUpdateDocAccess
delete: ErrorOnHooksDeleteDocAccess
}
type ErrorOnHooksDocAccessFields {
title: ErrorOnHooksDocAccessFields_title
errorBeforeChange: ErrorOnHooksDocAccessFields_errorBeforeChange
updatedAt: ErrorOnHooksDocAccessFields_updatedAt
createdAt: ErrorOnHooksDocAccessFields_createdAt
}
type ErrorOnHooksDocAccessFields_title {
create: ErrorOnHooksDocAccessFields_title_Create
read: ErrorOnHooksDocAccessFields_title_Read
update: ErrorOnHooksDocAccessFields_title_Update
delete: ErrorOnHooksDocAccessFields_title_Delete
}
type ErrorOnHooksDocAccessFields_title_Create {
permission: Boolean!
}
type ErrorOnHooksDocAccessFields_title_Read {
permission: Boolean!
}
type ErrorOnHooksDocAccessFields_title_Update {
permission: Boolean!
}
type ErrorOnHooksDocAccessFields_title_Delete {
permission: Boolean!
}
type ErrorOnHooksDocAccessFields_errorBeforeChange {
create: ErrorOnHooksDocAccessFields_errorBeforeChange_Create
read: ErrorOnHooksDocAccessFields_errorBeforeChange_Read
update: ErrorOnHooksDocAccessFields_errorBeforeChange_Update
delete: ErrorOnHooksDocAccessFields_errorBeforeChange_Delete
}
type ErrorOnHooksDocAccessFields_errorBeforeChange_Create {
permission: Boolean!
}
type ErrorOnHooksDocAccessFields_errorBeforeChange_Read {
permission: Boolean!
}
type ErrorOnHooksDocAccessFields_errorBeforeChange_Update {
permission: Boolean!
}
type ErrorOnHooksDocAccessFields_errorBeforeChange_Delete {
permission: Boolean!
}
type ErrorOnHooksDocAccessFields_updatedAt {
create: ErrorOnHooksDocAccessFields_updatedAt_Create
read: ErrorOnHooksDocAccessFields_updatedAt_Read
update: ErrorOnHooksDocAccessFields_updatedAt_Update
delete: ErrorOnHooksDocAccessFields_updatedAt_Delete
}
type ErrorOnHooksDocAccessFields_updatedAt_Create {
permission: Boolean!
}
type ErrorOnHooksDocAccessFields_updatedAt_Read {
permission: Boolean!
}
type ErrorOnHooksDocAccessFields_updatedAt_Update {
permission: Boolean!
}
type ErrorOnHooksDocAccessFields_updatedAt_Delete {
permission: Boolean!
}
type ErrorOnHooksDocAccessFields_createdAt {
create: ErrorOnHooksDocAccessFields_createdAt_Create
read: ErrorOnHooksDocAccessFields_createdAt_Read
update: ErrorOnHooksDocAccessFields_createdAt_Update
delete: ErrorOnHooksDocAccessFields_createdAt_Delete
}
type ErrorOnHooksDocAccessFields_createdAt_Create {
permission: Boolean!
}
type ErrorOnHooksDocAccessFields_createdAt_Read {
permission: Boolean!
}
type ErrorOnHooksDocAccessFields_createdAt_Update {
permission: Boolean!
}
type ErrorOnHooksDocAccessFields_createdAt_Delete {
permission: Boolean!
}
type ErrorOnHooksCreateDocAccess {
permission: Boolean!
where: JSONObject
}
type ErrorOnHooksReadDocAccess {
permission: Boolean!
where: JSONObject
}
type ErrorOnHooksUpdateDocAccess {
permission: Boolean!
where: JSONObject
}
type ErrorOnHooksDeleteDocAccess {
permission: Boolean!
where: JSONObject
}
type PayloadApiTestOne {
id: String
payloadAPI: String
@@ -2104,6 +2378,10 @@ input PayloadApiTestOne_where_or {
OR: [PayloadApiTestOne_where_or]
}
type countPayloadApiTestOnes {
totalDocs: Int
}
type payload_api_test_onesDocAccess {
fields: PayloadApiTestOnesDocAccessFields
create: PayloadApiTestOnesCreateDocAccess
@@ -2312,6 +2590,10 @@ input PayloadApiTestTwo_where_or {
OR: [PayloadApiTestTwo_where_or]
}
type countPayloadApiTestTwos {
totalDocs: Int
}
type payload_api_test_twosDocAccess {
fields: PayloadApiTestTwosDocAccessFields
create: PayloadApiTestTwosCreateDocAccess
@@ -2439,6 +2721,205 @@ type PayloadApiTestTwosDeleteDocAccess {
where: JSONObject
}
type ContentType {
id: String
contentType: String
updatedAt: DateTime
createdAt: DateTime
}
type ContentTypes {
docs: [ContentType]
hasNextPage: Boolean
hasPrevPage: Boolean
limit: Int
nextPage: Int
offset: Int
page: Int
pagingCounter: Int
prevPage: Int
totalDocs: Int
totalPages: Int
}
input ContentType_where {
contentType: ContentType_contentType_operator
updatedAt: ContentType_updatedAt_operator
createdAt: ContentType_createdAt_operator
id: ContentType_id_operator
AND: [ContentType_where_and]
OR: [ContentType_where_or]
}
input ContentType_contentType_operator {
equals: String
not_equals: String
like: String
contains: String
in: [String]
not_in: [String]
all: [String]
exists: Boolean
}
input ContentType_updatedAt_operator {
equals: DateTime
not_equals: DateTime
greater_than_equal: DateTime
greater_than: DateTime
less_than_equal: DateTime
less_than: DateTime
like: DateTime
exists: Boolean
}
input ContentType_createdAt_operator {
equals: DateTime
not_equals: DateTime
greater_than_equal: DateTime
greater_than: DateTime
less_than_equal: DateTime
less_than: DateTime
like: DateTime
exists: Boolean
}
input ContentType_id_operator {
equals: String
not_equals: String
like: String
contains: String
in: [String]
not_in: [String]
all: [String]
exists: Boolean
}
input ContentType_where_and {
contentType: ContentType_contentType_operator
updatedAt: ContentType_updatedAt_operator
createdAt: ContentType_createdAt_operator
id: ContentType_id_operator
AND: [ContentType_where_and]
OR: [ContentType_where_or]
}
input ContentType_where_or {
contentType: ContentType_contentType_operator
updatedAt: ContentType_updatedAt_operator
createdAt: ContentType_createdAt_operator
id: ContentType_id_operator
AND: [ContentType_where_and]
OR: [ContentType_where_or]
}
type countContentTypes {
totalDocs: Int
}
type content_typeDocAccess {
fields: ContentTypeDocAccessFields
create: ContentTypeCreateDocAccess
read: ContentTypeReadDocAccess
update: ContentTypeUpdateDocAccess
delete: ContentTypeDeleteDocAccess
}
type ContentTypeDocAccessFields {
contentType: ContentTypeDocAccessFields_contentType
updatedAt: ContentTypeDocAccessFields_updatedAt
createdAt: ContentTypeDocAccessFields_createdAt
}
type ContentTypeDocAccessFields_contentType {
create: ContentTypeDocAccessFields_contentType_Create
read: ContentTypeDocAccessFields_contentType_Read
update: ContentTypeDocAccessFields_contentType_Update
delete: ContentTypeDocAccessFields_contentType_Delete
}
type ContentTypeDocAccessFields_contentType_Create {
permission: Boolean!
}
type ContentTypeDocAccessFields_contentType_Read {
permission: Boolean!
}
type ContentTypeDocAccessFields_contentType_Update {
permission: Boolean!
}
type ContentTypeDocAccessFields_contentType_Delete {
permission: Boolean!
}
type ContentTypeDocAccessFields_updatedAt {
create: ContentTypeDocAccessFields_updatedAt_Create
read: ContentTypeDocAccessFields_updatedAt_Read
update: ContentTypeDocAccessFields_updatedAt_Update
delete: ContentTypeDocAccessFields_updatedAt_Delete
}
type ContentTypeDocAccessFields_updatedAt_Create {
permission: Boolean!
}
type ContentTypeDocAccessFields_updatedAt_Read {
permission: Boolean!
}
type ContentTypeDocAccessFields_updatedAt_Update {
permission: Boolean!
}
type ContentTypeDocAccessFields_updatedAt_Delete {
permission: Boolean!
}
type ContentTypeDocAccessFields_createdAt {
create: ContentTypeDocAccessFields_createdAt_Create
read: ContentTypeDocAccessFields_createdAt_Read
update: ContentTypeDocAccessFields_createdAt_Update
delete: ContentTypeDocAccessFields_createdAt_Delete
}
type ContentTypeDocAccessFields_createdAt_Create {
permission: Boolean!
}
type ContentTypeDocAccessFields_createdAt_Read {
permission: Boolean!
}
type ContentTypeDocAccessFields_createdAt_Update {
permission: Boolean!
}
type ContentTypeDocAccessFields_createdAt_Delete {
permission: Boolean!
}
type ContentTypeCreateDocAccess {
permission: Boolean!
where: JSONObject
}
type ContentTypeReadDocAccess {
permission: Boolean!
where: JSONObject
}
type ContentTypeUpdateDocAccess {
permission: Boolean!
where: JSONObject
}
type ContentTypeDeleteDocAccess {
permission: Boolean!
where: JSONObject
}
type PayloadPreference {
id: String
user: PayloadPreference_User_Relationship!
@@ -2569,6 +3050,10 @@ input PayloadPreference_where_or {
OR: [PayloadPreference_where_or]
}
type countPayloadPreferences {
totalDocs: Int
}
type payload_preferencesDocAccess {
fields: PayloadPreferencesDocAccessFields
create: PayloadPreferencesCreateDocAccess
@@ -2728,8 +3213,10 @@ type Access {
custom_ids: custom_idsAccess
relation: relationAccess
dummy: dummyAccess
error_on_hooks: error_on_hooksAccess
payload_api_test_ones: payload_api_test_onesAccess
payload_api_test_twos: payload_api_test_twosAccess
content_type: content_typeAccess
payload_preferences: payload_preferencesAccess
}
@@ -3885,6 +4372,133 @@ type DummyDeleteAccess {
where: JSONObject
}
type error_on_hooksAccess {
fields: ErrorOnHooksFields
create: ErrorOnHooksCreateAccess
read: ErrorOnHooksReadAccess
update: ErrorOnHooksUpdateAccess
delete: ErrorOnHooksDeleteAccess
}
type ErrorOnHooksFields {
title: ErrorOnHooksFields_title
errorBeforeChange: ErrorOnHooksFields_errorBeforeChange
updatedAt: ErrorOnHooksFields_updatedAt
createdAt: ErrorOnHooksFields_createdAt
}
type ErrorOnHooksFields_title {
create: ErrorOnHooksFields_title_Create
read: ErrorOnHooksFields_title_Read
update: ErrorOnHooksFields_title_Update
delete: ErrorOnHooksFields_title_Delete
}
type ErrorOnHooksFields_title_Create {
permission: Boolean!
}
type ErrorOnHooksFields_title_Read {
permission: Boolean!
}
type ErrorOnHooksFields_title_Update {
permission: Boolean!
}
type ErrorOnHooksFields_title_Delete {
permission: Boolean!
}
type ErrorOnHooksFields_errorBeforeChange {
create: ErrorOnHooksFields_errorBeforeChange_Create
read: ErrorOnHooksFields_errorBeforeChange_Read
update: ErrorOnHooksFields_errorBeforeChange_Update
delete: ErrorOnHooksFields_errorBeforeChange_Delete
}
type ErrorOnHooksFields_errorBeforeChange_Create {
permission: Boolean!
}
type ErrorOnHooksFields_errorBeforeChange_Read {
permission: Boolean!
}
type ErrorOnHooksFields_errorBeforeChange_Update {
permission: Boolean!
}
type ErrorOnHooksFields_errorBeforeChange_Delete {
permission: Boolean!
}
type ErrorOnHooksFields_updatedAt {
create: ErrorOnHooksFields_updatedAt_Create
read: ErrorOnHooksFields_updatedAt_Read
update: ErrorOnHooksFields_updatedAt_Update
delete: ErrorOnHooksFields_updatedAt_Delete
}
type ErrorOnHooksFields_updatedAt_Create {
permission: Boolean!
}
type ErrorOnHooksFields_updatedAt_Read {
permission: Boolean!
}
type ErrorOnHooksFields_updatedAt_Update {
permission: Boolean!
}
type ErrorOnHooksFields_updatedAt_Delete {
permission: Boolean!
}
type ErrorOnHooksFields_createdAt {
create: ErrorOnHooksFields_createdAt_Create
read: ErrorOnHooksFields_createdAt_Read
update: ErrorOnHooksFields_createdAt_Update
delete: ErrorOnHooksFields_createdAt_Delete
}
type ErrorOnHooksFields_createdAt_Create {
permission: Boolean!
}
type ErrorOnHooksFields_createdAt_Read {
permission: Boolean!
}
type ErrorOnHooksFields_createdAt_Update {
permission: Boolean!
}
type ErrorOnHooksFields_createdAt_Delete {
permission: Boolean!
}
type ErrorOnHooksCreateAccess {
permission: Boolean!
where: JSONObject
}
type ErrorOnHooksReadAccess {
permission: Boolean!
where: JSONObject
}
type ErrorOnHooksUpdateAccess {
permission: Boolean!
where: JSONObject
}
type ErrorOnHooksDeleteAccess {
permission: Boolean!
where: JSONObject
}
type payload_api_test_onesAccess {
fields: PayloadApiTestOnesFields
create: PayloadApiTestOnesCreateAccess
@@ -4115,6 +4729,109 @@ type PayloadApiTestTwosDeleteAccess {
where: JSONObject
}
type content_typeAccess {
fields: ContentTypeFields
create: ContentTypeCreateAccess
read: ContentTypeReadAccess
update: ContentTypeUpdateAccess
delete: ContentTypeDeleteAccess
}
type ContentTypeFields {
contentType: ContentTypeFields_contentType
updatedAt: ContentTypeFields_updatedAt
createdAt: ContentTypeFields_createdAt
}
type ContentTypeFields_contentType {
create: ContentTypeFields_contentType_Create
read: ContentTypeFields_contentType_Read
update: ContentTypeFields_contentType_Update
delete: ContentTypeFields_contentType_Delete
}
type ContentTypeFields_contentType_Create {
permission: Boolean!
}
type ContentTypeFields_contentType_Read {
permission: Boolean!
}
type ContentTypeFields_contentType_Update {
permission: Boolean!
}
type ContentTypeFields_contentType_Delete {
permission: Boolean!
}
type ContentTypeFields_updatedAt {
create: ContentTypeFields_updatedAt_Create
read: ContentTypeFields_updatedAt_Read
update: ContentTypeFields_updatedAt_Update
delete: ContentTypeFields_updatedAt_Delete
}
type ContentTypeFields_updatedAt_Create {
permission: Boolean!
}
type ContentTypeFields_updatedAt_Read {
permission: Boolean!
}
type ContentTypeFields_updatedAt_Update {
permission: Boolean!
}
type ContentTypeFields_updatedAt_Delete {
permission: Boolean!
}
type ContentTypeFields_createdAt {
create: ContentTypeFields_createdAt_Create
read: ContentTypeFields_createdAt_Read
update: ContentTypeFields_createdAt_Update
delete: ContentTypeFields_createdAt_Delete
}
type ContentTypeFields_createdAt_Create {
permission: Boolean!
}
type ContentTypeFields_createdAt_Read {
permission: Boolean!
}
type ContentTypeFields_createdAt_Update {
permission: Boolean!
}
type ContentTypeFields_createdAt_Delete {
permission: Boolean!
}
type ContentTypeCreateAccess {
permission: Boolean!
where: JSONObject
}
type ContentTypeReadAccess {
permission: Boolean!
where: JSONObject
}
type ContentTypeUpdateAccess {
permission: Boolean!
where: JSONObject
}
type ContentTypeDeleteAccess {
permission: Boolean!
where: JSONObject
}
type payload_preferencesAccess {
fields: PayloadPreferencesFields
create: PayloadPreferencesCreateAccess
@@ -4296,12 +5013,18 @@ type Mutation {
createDummy(data: mutationDummyInput!, draft: Boolean): Dummy
updateDummy(id: String!, autosave: Boolean, data: mutationDummyUpdateInput!, draft: Boolean): Dummy
deleteDummy(id: String!): Dummy
createErrorOnHook(data: mutationErrorOnHookInput!, draft: Boolean): ErrorOnHook
updateErrorOnHook(id: String!, autosave: Boolean, data: mutationErrorOnHookUpdateInput!, draft: Boolean): ErrorOnHook
deleteErrorOnHook(id: String!): ErrorOnHook
createPayloadApiTestOne(data: mutationPayloadApiTestOneInput!, draft: Boolean): PayloadApiTestOne
updatePayloadApiTestOne(id: String!, autosave: Boolean, data: mutationPayloadApiTestOneUpdateInput!, draft: Boolean): PayloadApiTestOne
deletePayloadApiTestOne(id: String!): PayloadApiTestOne
createPayloadApiTestTwo(data: mutationPayloadApiTestTwoInput!, draft: Boolean): PayloadApiTestTwo
updatePayloadApiTestTwo(id: String!, autosave: Boolean, data: mutationPayloadApiTestTwoUpdateInput!, draft: Boolean): PayloadApiTestTwo
deletePayloadApiTestTwo(id: String!): PayloadApiTestTwo
createContentType(data: mutationContentTypeInput!, draft: Boolean): ContentType
updateContentType(id: String!, autosave: Boolean, data: mutationContentTypeUpdateInput!, draft: Boolean): ContentType
deleteContentType(id: String!): ContentType
createPayloadPreference(data: mutationPayloadPreferenceInput!, draft: Boolean): PayloadPreference
updatePayloadPreference(id: String!, autosave: Boolean, data: mutationPayloadPreferenceUpdateInput!, draft: Boolean): PayloadPreference
deletePayloadPreference(id: String!): PayloadPreference
@@ -4538,6 +5261,20 @@ input mutationDummyUpdateInput {
createdAt: String
}
input mutationErrorOnHookInput {
title: String
errorBeforeChange: Boolean
updatedAt: String
createdAt: String
}
input mutationErrorOnHookUpdateInput {
title: String
errorBeforeChange: Boolean
updatedAt: String
createdAt: String
}
input mutationPayloadApiTestOneInput {
payloadAPI: String
updatedAt: String
@@ -4564,6 +5301,18 @@ input mutationPayloadApiTestTwoUpdateInput {
createdAt: String
}
input mutationContentTypeInput {
contentType: String
updatedAt: String
createdAt: String
}
input mutationContentTypeUpdateInput {
contentType: String
updatedAt: String
createdAt: String
}
input mutationPayloadPreferenceInput {
user: PayloadPreference_UserRelationshipInput
key: String

View File

@@ -67,6 +67,15 @@ describe('collections-rest', () => {
expect(result.docs).toEqual(expect.arrayContaining(expectedDocs))
})
it('should count', async () => {
await createPost()
await createPost()
const { result, status } = await client.count()
expect(status).toEqual(200)
expect(result).toEqual({ totalDocs: 2 })
})
it('should find where id', async () => {
const post1 = await createPost()
await createPost()

View File

@@ -142,6 +142,21 @@ const TabsFields: CollectionConfig = {
type: 'text',
defaultValue: namedTabDefaultValue,
},
{
type: 'row',
fields: [
{
name: 'arrayInRow',
type: 'array',
fields: [
{
name: 'textInArrayInRow',
type: 'text',
},
],
},
],
},
],
},
{

View File

@@ -35,6 +35,11 @@ export const tabsDoc: Partial<TabsField> = {
},
],
text: namedTabText,
arrayInRow: [
{
text: "Hello, I'm some text in an array in a row",
},
],
},
localizedTab: {
text: localizedTextValue,

View File

@@ -355,6 +355,29 @@ describe('fields', () => {
await saveDocAndAssert(page)
await expect(field.locator('.rs__value-container')).toContainText('One')
})
test('should not allow filtering by hasMany field / equals / not equals', async () => {
await page.goto(url.list)
await page.locator('.list-controls__toggle-columns').click()
await page.locator('.list-controls__toggle-where').click()
await page.waitForSelector('.list-controls__where.rah-static--height-auto')
await page.locator('.where-builder__add-first-filter').click()
const conditionField = page.locator('.condition__field')
await conditionField.click()
const dropdownFieldOptions = conditionField.locator('.rs__option')
await dropdownFieldOptions.locator('text=Select Has Many').nth(0).click()
const operatorField = page.locator('.condition__operator')
await operatorField.click()
const dropdownOperatorOptions = operatorField.locator('.rs__option')
await expect(dropdownOperatorOptions.locator('text=equals')).toBeHidden()
await expect(dropdownOperatorOptions.locator('text=not equals')).toBeHidden()
})
})
describe('point', () => {

View File

@@ -1083,6 +1083,10 @@ export interface TabsField {
}[]
text?: string | null
defaultValue?: string | null
arrayInRow?: {
text: string
id?: string | null
}[]
}
namedTabWithDefaultValue: {
defaultValue?: string | null

View File

@@ -30,6 +30,12 @@ type CreateArgs<T = any> = {
slug?: string
}
type CountArgs = {
auth?: boolean
query?: Where
slug?: string
}
type FindArgs = {
auth?: boolean
depth?: number
@@ -112,6 +118,13 @@ type QueryResponse<T> = {
status: number
}
type CountResponse = {
result: {
totalDocs: number
}
status: number
}
export class RESTClient {
private readonly config: Config
@@ -127,6 +140,32 @@ export class RESTClient {
this.defaultSlug = args.defaultSlug
}
async count<T = any>(args?: CountArgs): Promise<CountResponse> {
const options = {
headers: { ...headers },
}
if (args?.auth !== false && this.token) {
options.headers.Authorization = `JWT ${this.token}`
}
const whereQuery = qs.stringify(
{
...(args?.query ? { where: args.query } : {}),
},
{
addQueryPrefix: true,
},
)
const slug = args?.slug || this.defaultSlug
const response = await fetch(`${this.serverURL}/api/${slug}/count${whereQuery}`, options)
const { status } = response
const result = await response.json()
if (result.errors) throw new Error(result.errors[0].message)
return { result, status }
}
async create<T = any>(args: CreateArgs): Promise<DocResponse<T>> {
const options = {
body: args.file ? args.data : JSON.stringify(args.data),

View File

@@ -437,6 +437,7 @@ describe('Versions', () => {
collection,
data: {
title: patchedTitle,
_status: 'draft',
},
draft: true,
locale: 'en',
@@ -450,6 +451,7 @@ describe('Versions', () => {
collection,
data: {
title: spanishTitle,
_status: 'draft',
},
draft: true,
locale: 'es',
@@ -529,9 +531,11 @@ describe('Versions', () => {
},
})
// bulk publish
const updated = await payload.update({
collection: draftCollectionSlug,
data: {
_status: 'published',
description: 'updated description',
},
draft: true,
@@ -544,8 +548,20 @@ describe('Versions', () => {
const updatedDoc = updated.docs?.[0]
// get the published doc
const findResult = await payload.find({
collection: draftCollectionSlug,
where: {
id: { equals: doc.id },
},
})
const findDoc = findResult.docs?.[0]
expect(updatedDoc.description).toStrictEqual('updated description')
expect(updatedDoc.title).toStrictEqual('updated title') // probably will fail
expect(updatedDoc.title).toStrictEqual('updated title')
expect(findDoc.title).toStrictEqual('updated title')
expect(findDoc.description).toStrictEqual('updated description')
})
})
@@ -1236,6 +1252,7 @@ describe('Versions', () => {
await payload.updateGlobal({
slug: globalSlug,
data: {
_status: 'draft',
title: updatedTitle2,
},
draft: true,
@@ -1245,6 +1262,7 @@ describe('Versions', () => {
await payload.updateGlobal({
slug: globalSlug,
data: {
_status: 'draft',
title: updatedTitle2,
},
draft: true,