feat: add count operation to collections (#5930)
This commit is contained in:
@@ -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:**
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
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 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,
|
||||||
|
|||||||
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 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,
|
||||||
|
|||||||
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 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',
|
||||||
|
|||||||
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 { 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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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 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,
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export type {
|
|||||||
BeginTransaction,
|
BeginTransaction,
|
||||||
CommitTransaction,
|
CommitTransaction,
|
||||||
Connect,
|
Connect,
|
||||||
|
Count,
|
||||||
|
CountArgs,
|
||||||
Create,
|
Create,
|
||||||
CreateArgs,
|
CreateArgs,
|
||||||
CreateGlobal,
|
CreateGlobal,
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user