feat: add count operation to collections (#5930)
This commit is contained in:
@@ -46,6 +46,7 @@ export const PublicUser: CollectionConfig = {
|
||||
| ------------------ | ------------------- |
|
||||
| **`PublicUser`** | `findByID` |
|
||||
| **`PublicUsers`** | `find` |
|
||||
| **`countPublicUsers`** | `count` |
|
||||
| **`mePublicUser`** | `me` auth operation |
|
||||
|
||||
**And the following mutations:**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
49
packages/db-mongodb/src/count.ts
Normal file
49
packages/db-mongodb/src/count.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { createDatabaseAdapter } from 'payload/database'
|
||||
import type { CollectionModel, GlobalModel } from './types.js'
|
||||
|
||||
import { connect } from './connect.js'
|
||||
import { count } from './count.js'
|
||||
import { create } from './create.js'
|
||||
import { createGlobal } from './createGlobal.js'
|
||||
import { createGlobalVersion } from './createGlobalVersion.js'
|
||||
@@ -112,6 +113,7 @@ export function mongooseAdapter({
|
||||
collections: {},
|
||||
connectOptions: connectOptions || {},
|
||||
connection: undefined,
|
||||
count,
|
||||
disableIndexHints,
|
||||
globals: undefined,
|
||||
mongoMemoryServer,
|
||||
@@ -119,7 +121,6 @@ export function mongooseAdapter({
|
||||
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
|
||||
url,
|
||||
versions: {},
|
||||
|
||||
// DatabaseAdapter
|
||||
beginTransaction: transactionOptions ? beginTransaction : undefined,
|
||||
commitTransaction,
|
||||
|
||||
65
packages/db-postgres/src/count.ts
Normal file
65
packages/db-postgres/src/count.ts
Normal 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) }
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { createDatabaseAdapter } from 'payload/database'
|
||||
import type { Args, PostgresAdapter } from './types.js'
|
||||
|
||||
import { connect } from './connect.js'
|
||||
import { count } from './count.js'
|
||||
import { create } from './create.js'
|
||||
import { createGlobal } from './createGlobal.js'
|
||||
import { createGlobalVersion } from './createGlobalVersion.js'
|
||||
@@ -76,6 +77,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
beginTransaction,
|
||||
commitTransaction,
|
||||
connect,
|
||||
count,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
|
||||
41
packages/graphql/src/resolvers/collections/count.ts
Normal file
41
packages/graphql/src/resolvers/collections/count.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import refresh from '../resolvers/auth/refresh.js'
|
||||
import resetPassword from '../resolvers/auth/resetPassword.js'
|
||||
import unlock from '../resolvers/auth/unlock.js'
|
||||
import verifyEmail from '../resolvers/auth/verifyEmail.js'
|
||||
import countResolver from '../resolvers/collections/count.ts'
|
||||
import createResolver from '../resolvers/collections/create.js'
|
||||
import getDeleteResolver from '../resolvers/collections/delete.js'
|
||||
import { docAccessResolver } from '../resolvers/collections/docAccess.js'
|
||||
@@ -183,6 +184,25 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
|
||||
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}`] = {
|
||||
type: buildPolicyType({
|
||||
type: 'collection',
|
||||
|
||||
22
packages/next/src/routes/rest/collections/count.ts
Normal file
22
packages/next/src/routes/rest/collections/count.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import { unlock } from './auth/unlock.js'
|
||||
import { verifyEmail } from './auth/verifyEmail.js'
|
||||
import { buildFormState } from './buildFormState.js'
|
||||
import { endpointsAreDisabled } from './checkEndpoints.js'
|
||||
import { count } from './collections/count.js'
|
||||
import { create } from './collections/create.js'
|
||||
import { deleteDoc } from './collections/delete.js'
|
||||
import { deleteByID } from './collections/deleteByID.js'
|
||||
@@ -55,6 +56,7 @@ const endpoints = {
|
||||
deleteByID,
|
||||
},
|
||||
GET: {
|
||||
count,
|
||||
'doc-access-by-id': docAccess,
|
||||
'doc-versions-by-id': findVersionByID,
|
||||
find,
|
||||
@@ -204,6 +206,7 @@ export const GET =
|
||||
// /:collection/init
|
||||
// /:collection/me
|
||||
// /:collection/versions
|
||||
// /:collection/count
|
||||
res = await (endpoints.collection.GET[slug2] as CollectionRouteHandler)({
|
||||
collection,
|
||||
req,
|
||||
|
||||
@@ -30,6 +30,7 @@ import type { AfterOperationArg, AfterOperationMap } from '../operations/utils.j
|
||||
|
||||
export type HookOperationType =
|
||||
| 'autosave'
|
||||
| 'count'
|
||||
| 'create'
|
||||
| 'delete'
|
||||
| 'forgotPassword'
|
||||
@@ -421,6 +422,7 @@ export type Collection = {
|
||||
customIDType?: 'number' | 'text'
|
||||
graphQL?: {
|
||||
JWT: GraphQLObjectType
|
||||
countType: GraphQLObjectType
|
||||
mutationInputType: GraphQLNonNull<any>
|
||||
paginatedType: GraphQLObjectType
|
||||
type: GraphQLObjectType
|
||||
|
||||
111
packages/payload/src/collections/operations/count.ts
Normal file
111
packages/payload/src/collections/operations/count.ts
Normal 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
|
||||
}
|
||||
}
|
||||
44
packages/payload/src/collections/operations/local/count.ts
Normal file
44
packages/payload/src/collections/operations/local/count.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import auth from '../../../auth/operations/local/index.js'
|
||||
import count from './count.js'
|
||||
import create from './create.js'
|
||||
import deleteLocal from './delete.js'
|
||||
import { duplicate } from './duplicate.js'
|
||||
@@ -11,6 +12,7 @@ import update from './update.js'
|
||||
|
||||
export default {
|
||||
auth,
|
||||
count,
|
||||
create,
|
||||
deleteLocal,
|
||||
duplicate,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { loginOperation } from '../../auth/operations/login.js'
|
||||
import type { refreshOperation } from '../../auth/operations/refresh.js'
|
||||
import type { PayloadRequest } from '../../types/index.js'
|
||||
import type { AfterOperationHook, SanitizedCollectionConfig, TypeWithID } from '../config/types.js'
|
||||
import type { countOperation } from './count.js'
|
||||
import type { createOperation } from './create.js'
|
||||
import type { deleteOperation } from './delete.js'
|
||||
import type { deleteByIDOperation } from './deleteByID.js'
|
||||
@@ -12,6 +13,7 @@ import type { updateOperation } from './update.js'
|
||||
import type { updateByIDOperation } from './updateByID.js'
|
||||
|
||||
export type AfterOperationMap<T extends TypeWithID> = {
|
||||
count: typeof countOperation
|
||||
create: typeof createOperation // todo: pass correct generic
|
||||
delete: typeof deleteOperation // todo: pass correct generic
|
||||
deleteByID: typeof deleteByIDOperation // 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'
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface BaseDatabaseAdapter {
|
||||
*/
|
||||
connect?: Connect
|
||||
|
||||
count: Count
|
||||
|
||||
create: Create
|
||||
|
||||
createGlobal: CreateGlobal
|
||||
@@ -200,6 +202,16 @@ export type FindArgs = {
|
||||
|
||||
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 = {
|
||||
limit?: number
|
||||
locale?: string
|
||||
|
||||
@@ -3,6 +3,8 @@ export type {
|
||||
BeginTransaction,
|
||||
CommitTransaction,
|
||||
Connect,
|
||||
Count,
|
||||
CountArgs,
|
||||
Create,
|
||||
CreateArgs,
|
||||
CreateGlobal,
|
||||
|
||||
@@ -9,6 +9,7 @@ export { registerFirstUserOperation } from '../auth/operations/registerFirstUser
|
||||
export { resetPasswordOperation } from '../auth/operations/resetPassword.js'
|
||||
export { unlockOperation } from '../auth/operations/unlock.js'
|
||||
export { verifyEmailOperation } from '../auth/operations/verifyEmail.js'
|
||||
export { countOperation } from '../collections/operations/count.js'
|
||||
|
||||
export { createOperation } from '../collections/operations/create.js'
|
||||
export { deleteOperation } from '../collections/operations/delete.js'
|
||||
|
||||
@@ -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 { AuthStrategy } from './auth/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 {
|
||||
ByIDOptions as DeleteByIDOptions,
|
||||
@@ -34,7 +35,7 @@ import type {
|
||||
Options as UpdateOptions,
|
||||
} from './collections/operations/local/update.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 { TypeWithID as GlobalTypeWithID, Globals } from './globals/config/types.js'
|
||||
import type { Options as FindGlobalOptions } from './globals/operations/local/findOne.js'
|
||||
@@ -81,6 +82,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
|
||||
@@ -92,7 +105,6 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
const { create } = localOperations
|
||||
return create<T>(this, options)
|
||||
}
|
||||
|
||||
db: DatabaseAdapter
|
||||
|
||||
decrypt = decrypt
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TestButton } from './TestButton.js'
|
||||
import {
|
||||
docLevelAccessSlug,
|
||||
firstArrayText,
|
||||
hiddenAccessCountSlug,
|
||||
hiddenAccessSlug,
|
||||
hiddenFieldsSlug,
|
||||
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) => {
|
||||
await payload.create({
|
||||
|
||||
@@ -8,6 +8,7 @@ import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import configPromise, { requestHeaders } from './config.js'
|
||||
import {
|
||||
firstArrayText,
|
||||
hiddenAccessCountSlug,
|
||||
hiddenAccessSlug,
|
||||
hiddenFieldsSlug,
|
||||
relyOnRequestHeadersSlug,
|
||||
@@ -420,6 +421,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,
|
||||
|
||||
@@ -12,7 +12,7 @@ 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 hiddenAccessSlug = 'hidden-access'
|
||||
export const hiddenAccessCountSlug = 'hidden-access-count'
|
||||
|
||||
export const noAdminAccessEmail = 'no-admin-access@payloadcms.com'
|
||||
|
||||
@@ -116,6 +116,20 @@ describe('collections-graphql', () => {
|
||||
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 () => {
|
||||
const query = `query {
|
||||
postIDs: Posts {
|
||||
@@ -848,6 +862,21 @@ describe('collections-graphql', () => {
|
||||
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 () => {
|
||||
const relation = await payload.create({
|
||||
collection: relationSlug,
|
||||
|
||||
@@ -69,6 +69,16 @@ describe('collections-rest', () => {
|
||||
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 () => {
|
||||
const post1 = await createPost()
|
||||
await createPost()
|
||||
@@ -510,6 +520,18 @@ describe('collections-rest', () => {
|
||||
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 () => {
|
||||
const response = await restClient.GET(`/${slug}`, {
|
||||
query: {
|
||||
|
||||
Reference in New Issue
Block a user