Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add count operation to collections #5936

Merged
merged 2 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 6 additions & 5 deletions docs/graphql/overview.mdx
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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