feat: add count operation to collections (#5930)

This commit is contained in:
Ritsu
2024-04-20 21:45:44 +03:00
committed by GitHub
parent d987e5628a
commit d5cbbc472d
25 changed files with 538 additions and 9 deletions

View File

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

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 #### Update by ID
```js ```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", operation: "Create",
method: "POST", 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 './index.js'
import { withSession } from './withSession.js'
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

@@ -12,6 +12,7 @@ import { createDatabaseAdapter } from 'payload/database'
import type { CollectionModel, GlobalModel } from './types.js' import type { CollectionModel, GlobalModel } from './types.js'
import { connect } from './connect.js' import { connect } from './connect.js'
import { count } from './count.js'
import { create } from './create.js' import { create } from './create.js'
import { createGlobal } from './createGlobal.js' import { createGlobal } from './createGlobal.js'
import { createGlobalVersion } from './createGlobalVersion.js' import { createGlobalVersion } from './createGlobalVersion.js'
@@ -112,6 +113,7 @@ export function mongooseAdapter({
collections: {}, collections: {},
connectOptions: connectOptions || {}, connectOptions: connectOptions || {},
connection: undefined, connection: undefined,
count,
disableIndexHints, disableIndexHints,
globals: undefined, globals: undefined,
mongoMemoryServer, mongoMemoryServer,
@@ -119,7 +121,6 @@ export function mongooseAdapter({
transactionOptions: transactionOptions === false ? undefined : transactionOptions, transactionOptions: transactionOptions === false ? undefined : transactionOptions,
url, url,
versions: {}, versions: {},
// DatabaseAdapter // DatabaseAdapter
beginTransaction: transactionOptions ? beginTransaction : undefined, beginTransaction: transactionOptions ? beginTransaction : undefined,
commitTransaction, 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.js'
import type { PostgresAdapter } from './types.js'
import { chainMethods } from './find/chainMethods.js'
import buildQuery from './queries/buildQuery.js'
import { getTableName } from './schema/getTableName.js'
export const count: Count = async function count(
this: PostgresAdapter,
{ collection, locale, req, where: whereArg },
) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = 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

@@ -8,6 +8,7 @@ import { createDatabaseAdapter } from 'payload/database'
import type { Args, PostgresAdapter } from './types.js' import type { Args, PostgresAdapter } from './types.js'
import { connect } from './connect.js' import { connect } from './connect.js'
import { count } from './count.js'
import { create } from './create.js' import { create } from './create.js'
import { createGlobal } from './createGlobal.js' import { createGlobal } from './createGlobal.js'
import { createGlobalVersion } from './createGlobalVersion.js' import { createGlobalVersion } from './createGlobalVersion.js'
@@ -76,6 +77,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
beginTransaction, beginTransaction,
commitTransaction, commitTransaction,
connect, connect,
count,
create, create,
createGlobal, createGlobal,
createGlobalVersion, createGlobalVersion,

View File

@@ -0,0 +1,41 @@
import type { PayloadRequest, Where } from 'payload/types'
import type { Collection } from 'payload/types'
import { countOperation } from 'payload/operations'
import { isolateObjectProperty } from 'payload/utilities'
import type { Context } from '../types.js'
export type Resolver = (
_: unknown,
args: {
data: Record<string, unknown>
locale?: string
where?: Where
},
context: {
req: PayloadRequest
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => Promise<{ totalDocs: number }>
export default function countResolver(collection: Collection): Resolver {
return async function resolver(_, args, context: 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(req, 'transactionID'),
where: args.where,
}
const results = await countOperation(options)
return results
}
}

View File

@@ -24,6 +24,7 @@ import refresh from '../resolvers/auth/refresh.js'
import resetPassword from '../resolvers/auth/resetPassword.js' import resetPassword from '../resolvers/auth/resetPassword.js'
import unlock from '../resolvers/auth/unlock.js' import unlock from '../resolvers/auth/unlock.js'
import verifyEmail from '../resolvers/auth/verifyEmail.js' import verifyEmail from '../resolvers/auth/verifyEmail.js'
import countResolver from '../resolvers/collections/count.ts'
import createResolver from '../resolvers/collections/create.js' import createResolver from '../resolvers/collections/create.js'
import getDeleteResolver from '../resolvers/collections/delete.js' import getDeleteResolver from '../resolvers/collections/delete.js'
import { docAccessResolver } from '../resolvers/collections/docAccess.js' import { docAccessResolver } from '../resolvers/collections/docAccess.js'
@@ -183,6 +184,25 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
resolve: findResolver(collection), resolve: findResolver(collection),
} }
graphqlResult.Query.fields[`count${pluralName}`] = {
type: new GraphQLObjectType({
name: `count${pluralName}`,
fields: {
totalDocs: { type: GraphQLInt },
},
}),
args: {
draft: { type: GraphQLBoolean },
where: { type: collection.graphQL.whereInputType },
...(config.localization
? {
locale: { type: graphqlResult.types.localeInputType },
}
: {}),
},
resolve: countResolver(collection),
}
graphqlResult.Query.fields[`docAccess${singularName}`] = { graphqlResult.Query.fields[`docAccess${singularName}`] = {
type: buildPolicyType({ type: buildPolicyType({
type: 'collection', type: 'collection',

View File

@@ -0,0 +1,22 @@
import type { Where } from 'payload/types'
import httpStatus from 'http-status'
import { countOperation } from 'payload/operations'
import type { CollectionRouteHandler } from '../types.js'
export const count: CollectionRouteHandler = async ({ collection, req }) => {
const { where } = req.query as {
where?: Where
}
const result = await countOperation({
collection,
req,
where,
})
return Response.json(result, {
status: httpStatus.OK,
})
}

View File

@@ -25,6 +25,7 @@ import { unlock } from './auth/unlock.js'
import { verifyEmail } from './auth/verifyEmail.js' import { verifyEmail } from './auth/verifyEmail.js'
import { buildFormState } from './buildFormState.js' import { buildFormState } from './buildFormState.js'
import { endpointsAreDisabled } from './checkEndpoints.js' import { endpointsAreDisabled } from './checkEndpoints.js'
import { count } from './collections/count.js'
import { create } from './collections/create.js' import { create } from './collections/create.js'
import { deleteDoc } from './collections/delete.js' import { deleteDoc } from './collections/delete.js'
import { deleteByID } from './collections/deleteByID.js' import { deleteByID } from './collections/deleteByID.js'
@@ -55,6 +56,7 @@ const endpoints = {
deleteByID, deleteByID,
}, },
GET: { GET: {
count,
'doc-access-by-id': docAccess, 'doc-access-by-id': docAccess,
'doc-versions-by-id': findVersionByID, 'doc-versions-by-id': findVersionByID,
find, find,
@@ -204,6 +206,7 @@ export const GET =
// /:collection/init // /:collection/init
// /:collection/me // /:collection/me
// /:collection/versions // /:collection/versions
// /:collection/count
res = await (endpoints.collection.GET[slug2] as CollectionRouteHandler)({ res = await (endpoints.collection.GET[slug2] as CollectionRouteHandler)({
collection, collection,
req, req,

View File

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

View File

@@ -0,0 +1,111 @@
import type { AccessResult } from '../../config/types.js'
import type { PayloadRequest, Where } from '../../types/index.js'
import type { Collection, TypeWithID } from '../config/types.js'
import executeAccess from '../../auth/executeAccess.js'
import { combineQueries } from '../../database/combineQueries.js'
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { buildAfterOperation } from './utils.js'
export type Arguments = {
collection: Collection
disableErrors?: boolean
overrideAccess?: boolean
req?: PayloadRequest
where?: Where
}
export const countOperation = async <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
}
}

View File

@@ -0,0 +1,44 @@
import type { GeneratedTypes, Payload } from '../../../index.js'
import type { Document, PayloadRequest, RequestContext, Where } from '../../../types/index.js'
import { APIError } from '../../../errors/index.js'
import { createLocalReq } from '../../../utilities/createLocalReq.js'
import { countOperation } from '../count.js'
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
depth?: number
disableErrors?: boolean
locale?: GeneratedTypes['locale']
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. Count Operation.`,
)
}
return countOperation({
collection,
disableErrors,
overrideAccess,
req: await createLocalReq(options, payload),
where,
})
}

View File

@@ -1,4 +1,5 @@
import auth from '../../../auth/operations/local/index.js' import auth from '../../../auth/operations/local/index.js'
import count from './count.js'
import create from './create.js' import create from './create.js'
import deleteLocal from './delete.js' import deleteLocal from './delete.js'
import { duplicate } from './duplicate.js' import { duplicate } from './duplicate.js'
@@ -11,6 +12,7 @@ import update from './update.js'
export default { export default {
auth, auth,
count,
create, create,
deleteLocal, deleteLocal,
duplicate, duplicate,

View File

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

View File

@@ -21,6 +21,8 @@ export interface BaseDatabaseAdapter {
*/ */
connect?: Connect connect?: Connect
count: Count
create: Create create: Create
createGlobal: CreateGlobal createGlobal: CreateGlobal
@@ -200,6 +202,16 @@ export type FindArgs = {
export type Find = <T = TypeWithID>(args: FindArgs) => Promise<PaginatedDocs<T>> export type Find = <T = TypeWithID>(args: FindArgs) => Promise<PaginatedDocs<T>>
export type CountArgs = {
collection: string
locale?: string
/** Setting limit to 1 is equal to the previous Model.findOne(). Setting limit to 0 disables the limit */
req: PayloadRequest
where?: Where
}
export type Count = (args: CountArgs) => Promise<{ totalDocs: number }>
type BaseVersionArgs = { type BaseVersionArgs = {
limit?: number limit?: number
locale?: string locale?: string

View File

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

View File

@@ -9,6 +9,7 @@ export { registerFirstUserOperation } from '../auth/operations/registerFirstUser
export { resetPasswordOperation } from '../auth/operations/resetPassword.js' export { resetPasswordOperation } from '../auth/operations/resetPassword.js'
export { unlockOperation } from '../auth/operations/unlock.js' export { unlockOperation } from '../auth/operations/unlock.js'
export { verifyEmailOperation } from '../auth/operations/verifyEmail.js' export { verifyEmailOperation } from '../auth/operations/verifyEmail.js'
export { countOperation } from '../collections/operations/count.js'
export { createOperation } from '../collections/operations/create.js' export { createOperation } from '../collections/operations/create.js'
export { deleteOperation } from '../collections/operations/delete.js' export { deleteOperation } from '../collections/operations/delete.js'

View File

@@ -16,6 +16,7 @@ import type { Result as LoginResult } from './auth/operations/login.js'
import type { Result as ResetPasswordResult } from './auth/operations/resetPassword.js' import type { Result as ResetPasswordResult } from './auth/operations/resetPassword.js'
import type { AuthStrategy } from './auth/types.js' import type { AuthStrategy } from './auth/types.js'
import type { BulkOperationResult, Collection, TypeWithID } from './collections/config/types.js' import type { BulkOperationResult, Collection, TypeWithID } from './collections/config/types.js'
import type { Options as CountOptions } from './collections/operations/local/count.js'
import type { Options as CreateOptions } from './collections/operations/local/create.js' import type { Options as CreateOptions } from './collections/operations/local/create.js'
import type { import type {
ByIDOptions as DeleteByIDOptions, ByIDOptions as DeleteByIDOptions,
@@ -34,7 +35,7 @@ import type {
Options as UpdateOptions, Options as UpdateOptions,
} from './collections/operations/local/update.js' } from './collections/operations/local/update.js'
import type { EmailOptions, InitOptions, SanitizedConfig } from './config/types.js' import type { EmailOptions, InitOptions, SanitizedConfig } from './config/types.js'
import type { BaseDatabaseAdapter, PaginatedDocs } from './database/types.js' import type { BaseDatabaseAdapter, CountArgs, PaginatedDocs } from './database/types.js'
import type { BuildEmailResult } from './email/types.js' import type { BuildEmailResult } from './email/types.js'
import type { TypeWithID as GlobalTypeWithID, Globals } from './globals/config/types.js' import type { TypeWithID as GlobalTypeWithID, Globals } from './globals/config/types.js'
import type { Options as FindGlobalOptions } from './globals/operations/local/findOne.js' import type { Options as FindGlobalOptions } from './globals/operations/local/findOne.js'
@@ -81,6 +82,18 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
config: SanitizedConfig 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 * @description Performs create operation
* @param options * @param options
@@ -92,7 +105,6 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
const { create } = localOperations const { create } = localOperations
return create<T>(this, options) return create<T>(this, options)
} }
db: DatabaseAdapter db: DatabaseAdapter
decrypt = decrypt decrypt = decrypt

View File

@@ -6,6 +6,7 @@ import { TestButton } from './TestButton.js'
import { import {
docLevelAccessSlug, docLevelAccessSlug,
firstArrayText, firstArrayText,
hiddenAccessCountSlug,
hiddenAccessSlug, hiddenAccessSlug,
hiddenFieldsSlug, hiddenFieldsSlug,
noAdminAccessEmail, noAdminAccessEmail,
@@ -428,6 +429,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) => { onInit: async (payload) => {
await payload.create({ await payload.create({

View File

@@ -8,6 +8,7 @@ import { initPayloadInt } from '../helpers/initPayloadInt.js'
import configPromise, { requestHeaders } from './config.js' import configPromise, { requestHeaders } from './config.js'
import { import {
firstArrayText, firstArrayText,
hiddenAccessCountSlug,
hiddenAccessSlug, hiddenAccessSlug,
hiddenFieldsSlug, hiddenFieldsSlug,
relyOnRequestHeadersSlug, relyOnRequestHeadersSlug,
@@ -420,6 +421,30 @@ describe('Access Control', () => {
expect(docs).toHaveLength(1) 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 () => { it('should respect query constraint using hidden field on versions', async () => {
await payload.create({ await payload.create({
collection: restrictedVersionsSlug, collection: restrictedVersionsSlug,

View File

@@ -12,7 +12,7 @@ export const siblingDataSlug = 'sibling-data'
export const relyOnRequestHeadersSlug = 'rely-on-request-headers' export const relyOnRequestHeadersSlug = 'rely-on-request-headers'
export const docLevelAccessSlug = 'doc-level-access' export const docLevelAccessSlug = 'doc-level-access'
export const hiddenFieldsSlug = 'hidden-fields' export const hiddenFieldsSlug = 'hidden-fields'
export const hiddenAccessSlug = 'hidden-access' export const hiddenAccessSlug = 'hidden-access'
export const hiddenAccessCountSlug = 'hidden-access-count'
export const noAdminAccessEmail = 'no-admin-access@payloadcms.com' export const noAdminAccessEmail = 'no-admin-access@payloadcms.com'

View File

@@ -116,6 +116,20 @@ describe('collections-graphql', () => {
expect(docs).toContainEqual(expect.objectContaining({ id: existingDoc.id })) expect(docs).toContainEqual(expect.objectContaining({ id: existingDoc.id }))
}) })
it('should count', async () => {
const query = `query {
countPosts {
totalDocs
}
}`
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
.then((res) => res.json())
const { totalDocs } = data.countPosts
expect(typeof totalDocs).toBe('number')
})
it('should read using multiple queries', async () => { it('should read using multiple queries', async () => {
const query = `query { const query = `query {
postIDs: Posts { postIDs: Posts {
@@ -848,6 +862,21 @@ describe('collections-graphql', () => {
expect(docs[0].relationToCustomID.id).toStrictEqual(1) expect(docs[0].relationToCustomID.id).toStrictEqual(1)
}) })
it('should query on relationships with custom IDs - count', async () => {
const query = `query {
countPosts(where: { title: { equals: "has custom ID relation" }}) {
totalDocs
}
}`
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
.then((res) => res.json())
const { totalDocs } = data.countPosts
expect(totalDocs).toStrictEqual(1)
})
it('should query a document with a deleted relationship', async () => { it('should query a document with a deleted relationship', async () => {
const relation = await payload.create({ const relation = await payload.create({
collection: relationSlug, collection: relationSlug,

View File

@@ -69,6 +69,16 @@ describe('collections-rest', () => {
expect(result.docs).toEqual(expect.arrayContaining(expectedDocs)) expect(result.docs).toEqual(expect.arrayContaining(expectedDocs))
}) })
it('should count', async () => {
await createPost()
await createPost()
const response = await restClient.GET(`/${slug}/count`)
const result = await response.json()
expect(response.status).toEqual(200)
expect(result).toEqual({ totalDocs: 2 })
})
it('should find where id', async () => { it('should find where id', async () => {
const post1 = await createPost() const post1 = await createPost()
await createPost() await createPost()
@@ -510,6 +520,18 @@ describe('collections-rest', () => {
expect(result.totalDocs).toEqual(1) expect(result.totalDocs).toEqual(1)
}) })
it('should count query by property value', async () => {
const response = await restClient.GET(`/${slug}/count`, {
query: {
where: { relationField: { equals: relation.id } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result).toEqual({ totalDocs: 1 })
})
it('query by id', async () => { it('query by id', async () => {
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${slug}`, {
query: { query: {