feat: add findDistinct operation (#13102)
Adds a new operation findDistinct that can give you distinct values of a
field for a given collection
Example:
Assume you have a collection posts with multiple documents, and some of
them share the same title:
```js
// Example dataset (some titles appear multiple times)
[
{ title: 'title-1' },
{ title: 'title-2' },
{ title: 'title-1' },
{ title: 'title-3' },
{ title: 'title-2' },
{ title: 'title-4' },
{ title: 'title-5' },
{ title: 'title-6' },
{ title: 'title-7' },
{ title: 'title-8' },
{ title: 'title-9' },
]
```
You can now retrieve all unique title values using findDistinct:
```js
const result = await payload.findDistinct({
collection: 'posts',
field: 'title',
})
console.log(result.values)
// Output:
// [
// 'title-1',
// 'title-2',
// 'title-3',
// 'title-4',
// 'title-5',
// 'title-6',
// 'title-7',
// 'title-8',
// 'title-9'
// ]
```
You can also limit the number of distinct results:
```js
const limitedResult = await payload.findDistinct({
collection: 'posts',
field: 'title',
sortOrder: 'desc',
limit: 3,
})
console.log(limitedResult.values)
// Output:
// [
// 'title-1',
// 'title-2',
// 'title-3'
// ]
```
You can also pass a `where` query to filter the documents.
This commit is contained in:
141
packages/db-mongodb/src/findDistinct.ts
Normal file
141
packages/db-mongodb/src/findDistinct.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import type { PipelineStage } from 'mongoose'
|
||||
|
||||
import { type FindDistinct, getFieldByPath } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const findDistinct: FindDistinct = async function (this: MongooseAdapter, args) {
|
||||
const { collectionConfig, Model } = getCollection({
|
||||
adapter: this,
|
||||
collectionSlug: args.collection,
|
||||
})
|
||||
|
||||
const session = await getSession(this, args.req)
|
||||
|
||||
const { where = {} } = args
|
||||
|
||||
const sortAggregation: PipelineStage[] = []
|
||||
|
||||
const sort = buildSortParam({
|
||||
adapter: this,
|
||||
config: this.payload.config,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
locale: args.locale,
|
||||
sort: args.sort ?? args.field,
|
||||
sortAggregation,
|
||||
timestamps: true,
|
||||
})
|
||||
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: args.collection,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
locale: args.locale,
|
||||
where,
|
||||
})
|
||||
|
||||
const fieldPathResult = getFieldByPath({
|
||||
fields: collectionConfig.flattenedFields,
|
||||
path: args.field,
|
||||
})
|
||||
let fieldPath = args.field
|
||||
if (fieldPathResult?.pathHasLocalized && args.locale) {
|
||||
fieldPath = fieldPathResult.localizedPath.replace('<locale>', args.locale)
|
||||
}
|
||||
|
||||
const page = args.page || 1
|
||||
|
||||
const sortProperty = Object.keys(sort)[0]! // assert because buildSortParam always returns at least 1 key.
|
||||
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
|
||||
|
||||
const pipeline: PipelineStage[] = [
|
||||
{
|
||||
$match: query,
|
||||
},
|
||||
...(sortAggregation.length > 0 ? sortAggregation : []),
|
||||
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
_field: `$${fieldPath}`,
|
||||
...(sortProperty === fieldPath
|
||||
? {}
|
||||
: {
|
||||
_sort: `$${sortProperty}`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
[sortProperty === fieldPath ? '_id._field' : '_id._sort']: sortDirection,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const getValues = async () => {
|
||||
return Model.aggregate(pipeline, { session }).then((res) =>
|
||||
res.map((each) => ({
|
||||
[args.field]: JSON.parse(JSON.stringify(each._id._field)),
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
if (args.limit) {
|
||||
pipeline.push({
|
||||
$skip: (page - 1) * args.limit,
|
||||
})
|
||||
pipeline.push({ $limit: args.limit })
|
||||
const totalDocs = await Model.aggregate(
|
||||
[
|
||||
{
|
||||
$match: query,
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: `$${fieldPath}`,
|
||||
},
|
||||
},
|
||||
{ $count: 'count' },
|
||||
],
|
||||
{
|
||||
session,
|
||||
},
|
||||
).then((res) => res[0]?.count ?? 0)
|
||||
const totalPages = Math.ceil(totalDocs / args.limit)
|
||||
const hasPrevPage = page > 1
|
||||
const hasNextPage = totalPages > page
|
||||
const pagingCounter = (page - 1) * args.limit + 1
|
||||
|
||||
return {
|
||||
hasNextPage,
|
||||
hasPrevPage,
|
||||
limit: args.limit,
|
||||
nextPage: hasNextPage ? page + 1 : null,
|
||||
page,
|
||||
pagingCounter,
|
||||
prevPage: hasPrevPage ? page - 1 : null,
|
||||
totalDocs,
|
||||
totalPages,
|
||||
values: await getValues(),
|
||||
}
|
||||
}
|
||||
|
||||
const values = await getValues()
|
||||
|
||||
return {
|
||||
hasNextPage: false,
|
||||
hasPrevPage: false,
|
||||
limit: 0,
|
||||
page: 1,
|
||||
pagingCounter: 1,
|
||||
totalDocs: values.length,
|
||||
totalPages: 1,
|
||||
values,
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ import { deleteOne } from './deleteOne.js'
|
||||
import { deleteVersions } from './deleteVersions.js'
|
||||
import { destroy } from './destroy.js'
|
||||
import { find } from './find.js'
|
||||
import { findDistinct } from './findDistinct.js'
|
||||
import { findGlobal } from './findGlobal.js'
|
||||
import { findGlobalVersions } from './findGlobalVersions.js'
|
||||
import { findOne } from './findOne.js'
|
||||
@@ -297,6 +298,7 @@ export function mongooseAdapter({
|
||||
destroy,
|
||||
disableFallbackSort,
|
||||
find,
|
||||
findDistinct,
|
||||
findGlobal,
|
||||
findGlobalVersions,
|
||||
findOne,
|
||||
|
||||
Reference in New Issue
Block a user