Skip to content

Commit

Permalink
feat: add count operation to collections (#5936)
Browse files Browse the repository at this point in the history
  • Loading branch information
r1tsuu committed Apr 21, 2024
1 parent 0b12aac commit c380dee
Show file tree
Hide file tree
Showing 27 changed files with 1,413 additions and 49 deletions.
11 changes: 6 additions & 5 deletions docs/graphql/overview.mdx
Expand Up @@ -43,11 +43,12 @@ export const PublicUser: CollectionConfig = {

**Payload will automatically open up the following queries:**

| Query Name | Operation |
| ------------------ | ------------------- |
| **`PublicUser`** | `findByID` |
| **`PublicUsers`** | `find` |
| **`mePublicUser`** | `me` auth operation |
| Query Name | Operation |
| ------------------ | ------------------- |
| **`PublicUser`** | `findByID` |
| **`PublicUsers`** | `find` |
| **`countPublicUsers`** | `count` |
| **`mePublicUser`** | `me` auth operation |

**And the following mutations:**

Expand Down
16 changes: 16 additions & 0 deletions docs/local-api/overview.mdx
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions docs/rest-api/overview.mdx
Expand Up @@ -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",
Expand Down
49 changes: 49 additions & 0 deletions packages/db-mongodb/src/count.ts
@@ -0,0 +1,49 @@
import type { QueryOptions } from 'mongoose'
import type { Count } from 'payload/database'
import type { PayloadRequest } from 'payload/types'

import { flattenWhereToOperators } from 'payload/database'

import type { MongooseAdapter } from '.'

import { withSession } from './withSession'

export const count: Count = async function count(
this: MongooseAdapter,
{ collection, locale, req = {} as PayloadRequest, where },
) {
const Model = this.collections[collection]
const options: QueryOptions = withSession(this, req.transactionID)

let hasNearConstraint = false

if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}

const query = await Model.buildQuery({
locale,
payload: this.payload,
where,
})

// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0

if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
options.hint = {
_id: 1,
}
}

const result = await Model.countDocuments(query, options)

return {
totalDocs: result,
}
}
3 changes: 2 additions & 1 deletion packages/db-mongodb/src/index.ts
Expand Up @@ -11,6 +11,7 @@ import { createDatabaseAdapter } from 'payload/database'
import type { CollectionModel, GlobalModel } from './types'

import { connect } from './connect'
import { count } from './count'
import { create } from './create'
import { createGlobal } from './createGlobal'
import { createGlobalVersion } from './createGlobalVersion'
Expand Down Expand Up @@ -108,14 +109,14 @@ export function mongooseAdapter({
collections: {},
connectOptions: connectOptions || {},
connection: undefined,
count,
disableIndexHints,
globals: undefined,
mongoMemoryServer: undefined,
sessions: {},
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
url,
versions: {},

// DatabaseAdapter
beginTransaction: transactionOptions ? beginTransaction : undefined,
commitTransaction,
Expand Down
65 changes: 65 additions & 0 deletions packages/db-postgres/src/count.ts
@@ -0,0 +1,65 @@
import type { Count } from 'payload/database'
import type { SanitizedCollectionConfig } from 'payload/types'

import { sql } from 'drizzle-orm'

import type { ChainedMethods } from './find/chainMethods'
import type { PostgresAdapter } from './types'

import { chainMethods } from './find/chainMethods'
import buildQuery from './queries/buildQuery'
import { getTableName } from './schema/getTableName'

export const count: Count = async function count(
this: PostgresAdapter,
{ collection, locale, req, where: whereArg },
) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config

const tableName = getTableName({
adapter: this,
config: collectionConfig,
})

const db = this.sessions[req.transactionID]?.db || this.drizzle
const table = this.tables[tableName]

const { joinAliases, joins, where } = await buildQuery({
adapter: this,
fields: collectionConfig.fields,
locale,
tableName,
where: whereArg,
})

const selectCountMethods: ChainedMethods = []

joinAliases.forEach(({ condition, table }) => {
selectCountMethods.push({
args: [table, condition],
method: 'leftJoin',
})
})

Object.entries(joins).forEach(([joinTable, condition]) => {
if (joinTable) {
selectCountMethods.push({
args: [this.tables[joinTable], condition],
method: 'leftJoin',
})
}
})

const countResult = await chainMethods({
methods: selectCountMethods,
query: db
.select({
count: sql<number>`count
(DISTINCT ${this.tables[tableName].id})`,
})
.from(table)
.where(where),
})

return { totalDocs: Number(countResult[0].count) }
}
2 changes: 2 additions & 0 deletions packages/db-postgres/src/index.ts
Expand Up @@ -7,6 +7,7 @@ import { createDatabaseAdapter } from 'payload/database'
import type { Args, PostgresAdapter, PostgresAdapterResult } from './types'

import { connect } from './connect'
import { count } from './count'
import { create } from './create'
import { createGlobal } from './createGlobal'
import { createGlobalVersion } from './createGlobalVersion'
Expand Down Expand Up @@ -70,6 +71,7 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
beginTransaction,
commitTransaction,
connect,
count,
create,
createGlobal,
createGlobalVersion,
Expand Down
6 changes: 6 additions & 0 deletions packages/payload/src/collections/buildEndpoints.ts
Expand Up @@ -11,6 +11,7 @@ import registerFirstUserHandler from '../auth/requestHandlers/registerFirstUser'
import resetPassword from '../auth/requestHandlers/resetPassword'
import unlock from '../auth/requestHandlers/unlock'
import verifyEmail from '../auth/requestHandlers/verifyEmail'
import count from './requestHandlers/count'
import create from './requestHandlers/create'
import deleteHandler from './requestHandlers/delete'
import deleteByID from './requestHandlers/deleteByID'
Expand Down Expand Up @@ -124,6 +125,11 @@ const buildEndpoints = (collection: SanitizedCollectionConfig): Endpoint[] => {
method: 'post',
path: '/',
},
{
handler: count,
method: 'get',
path: '/count',
},
{
handler: docAccessRequestHandler,
method: 'get',
Expand Down
2 changes: 2 additions & 0 deletions packages/payload/src/collections/config/types.ts
Expand Up @@ -30,6 +30,7 @@ import type { AfterOperationArg, AfterOperationMap } from '../operations/utils'

export type HookOperationType =
| 'autosave'
| 'count'
| 'create'
| 'delete'
| 'forgotPassword'
Expand Down Expand Up @@ -465,6 +466,7 @@ export type Collection = {
config: SanitizedCollectionConfig
graphQL?: {
JWT: GraphQLObjectType
countType: GraphQLObjectType
mutationInputType: GraphQLNonNull<any>
paginatedType: GraphQLObjectType
type: GraphQLObjectType
Expand Down

0 comments on commit c380dee

Please sign in to comment.