feat: allow to count related docs for join fields (#11395)
### What?
For the join field query adds ability to specify `count: true`, example:
```ts
const result = await payload.find({
joins: {
'group.relatedPosts': {
sort: '-title',
count: true,
},
},
collection: "categories",
})
result.group?.relatedPosts?.totalDocs // available
```
### Why?
Can be useful to implement full pagination / show total related
documents count in the UI.
### How?
Implements the logic in database adapters. In MongoDB it's additional
`$lookup` that has `$count` in the pipeline. In SQL, it's additional
subquery with `COUNT(*)`. Preserves the current behavior by default,
since counting introduces overhead.
Additionally, fixes a typescript generation error for join fields.
Before, `docs` and `hasNextPage` were marked as nullable, which is not
true, these fields cannot be `null`.
Additionally, fixes threading of `joinQuery` in
`transform/read/traverseFields` for group / tab fields recursive calls.
This commit is contained in:
@@ -158,6 +158,7 @@ object with:
|
|||||||
|
|
||||||
- `docs` an array of related documents or only IDs if the depth is reached
|
- `docs` an array of related documents or only IDs if the depth is reached
|
||||||
- `hasNextPage` a boolean indicating if there are additional documents
|
- `hasNextPage` a boolean indicating if there are additional documents
|
||||||
|
- `totalDocs` a total number of documents, exists only if `count: true` is passed to the join query
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -171,7 +172,8 @@ object with:
|
|||||||
}
|
}
|
||||||
// { ... }
|
// { ... }
|
||||||
],
|
],
|
||||||
"hasNextPage": false
|
"hasNextPage": false,
|
||||||
|
"totalDocs": 10, // if count: true is passed
|
||||||
}
|
}
|
||||||
// other fields...
|
// other fields...
|
||||||
}
|
}
|
||||||
@@ -184,6 +186,7 @@ object with:
|
|||||||
|
|
||||||
- `docs` an array of `relationTo` - the collection slug of the document and `value` - the document itself or the ID if the depth is reached
|
- `docs` an array of `relationTo` - the collection slug of the document and `value` - the document itself or the ID if the depth is reached
|
||||||
- `hasNextPage` a boolean indicating if there are additional documents
|
- `hasNextPage` a boolean indicating if there are additional documents
|
||||||
|
- `totalDocs` a total number of documents, exists only if `count: true` is passed to the join query
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -200,7 +203,8 @@ object with:
|
|||||||
}
|
}
|
||||||
// { ... }
|
// { ... }
|
||||||
],
|
],
|
||||||
"hasNextPage": false
|
"hasNextPage": false,
|
||||||
|
"totalDocs": 10, // if count: true is passed
|
||||||
}
|
}
|
||||||
// other fields...
|
// other fields...
|
||||||
}
|
}
|
||||||
@@ -215,10 +219,11 @@ returning. This is useful for performance reasons when you don't need the relate
|
|||||||
The following query options are supported:
|
The following query options are supported:
|
||||||
|
|
||||||
| Property | Description |
|
| Property | Description |
|
||||||
|-------------|-----------------------------------------------------------------------------------------------------|
|
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||||
| **`limit`** | The maximum related documents to be returned, default is 10. |
|
| **`limit`** | The maximum related documents to be returned, default is 10. |
|
||||||
| **`where`** | An optional `Where` query to filter joined documents. Will be merged with the field `where` object. |
|
| **`where`** | An optional `Where` query to filter joined documents. Will be merged with the field `where` object. |
|
||||||
| **`sort`** | A string used to order related results |
|
| **`sort`** | A string used to order related results |
|
||||||
|
| **`count`** | Whether include the count of related documents or not. Not included by default |
|
||||||
|
|
||||||
These can be applied to the local API, GraphQL, and REST API.
|
These can be applied to the local API, GraphQL, and REST API.
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export const buildJoinAggregation = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
count = false,
|
||||||
limit: limitJoin = join.field.defaultLimit ?? 10,
|
limit: limitJoin = join.field.defaultLimit ?? 10,
|
||||||
page,
|
page,
|
||||||
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
|
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
|
||||||
@@ -121,6 +122,28 @@ export const buildJoinAggregation = async ({
|
|||||||
const alias = `${as}.docs.${collectionSlug}`
|
const alias = `${as}.docs.${collectionSlug}`
|
||||||
aliases.push(alias)
|
aliases.push(alias)
|
||||||
|
|
||||||
|
const basePipeline = [
|
||||||
|
{
|
||||||
|
$addFields: {
|
||||||
|
relationTo: {
|
||||||
|
$literal: collectionSlug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
$and: [
|
||||||
|
{
|
||||||
|
$expr: {
|
||||||
|
$eq: [`$${join.field.on}`, '$$root_id_'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
$match,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
aggregate.push({
|
aggregate.push({
|
||||||
$lookup: {
|
$lookup: {
|
||||||
as: alias,
|
as: alias,
|
||||||
@@ -129,25 +152,7 @@ export const buildJoinAggregation = async ({
|
|||||||
root_id_: '$_id',
|
root_id_: '$_id',
|
||||||
},
|
},
|
||||||
pipeline: [
|
pipeline: [
|
||||||
{
|
...basePipeline,
|
||||||
$addFields: {
|
|
||||||
relationTo: {
|
|
||||||
$literal: collectionSlug,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
$and: [
|
|
||||||
{
|
|
||||||
$expr: {
|
|
||||||
$eq: [`$${join.field.on}`, '$$root_id_'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
$match,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
$sort: {
|
$sort: {
|
||||||
[sortProperty]: sortDirection,
|
[sortProperty]: sortDirection,
|
||||||
@@ -169,6 +174,24 @@ export const buildJoinAggregation = async ({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
aggregate.push({
|
||||||
|
$lookup: {
|
||||||
|
as: `${as}.totalDocs.${alias}`,
|
||||||
|
from: adapter.collections[collectionSlug].collection.name,
|
||||||
|
let: {
|
||||||
|
root_id_: '$_id',
|
||||||
|
},
|
||||||
|
pipeline: [
|
||||||
|
...basePipeline,
|
||||||
|
{
|
||||||
|
$count: 'result',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
aggregate.push({
|
aggregate.push({
|
||||||
@@ -179,6 +202,23 @@ export const buildJoinAggregation = async ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
aggregate.push({
|
||||||
|
$addFields: {
|
||||||
|
[`${as}.totalDocs`]: {
|
||||||
|
$add: aliases.map((alias) => ({
|
||||||
|
$ifNull: [
|
||||||
|
{
|
||||||
|
$first: `$${as}.totalDocs.${alias}.result`,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
aggregate.push({
|
aggregate.push({
|
||||||
$set: {
|
$set: {
|
||||||
[`${as}.docs`]: {
|
[`${as}.docs`]: {
|
||||||
@@ -222,6 +262,7 @@ export const buildJoinAggregation = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
count,
|
||||||
limit: limitJoin = join.field.defaultLimit ?? 10,
|
limit: limitJoin = join.field.defaultLimit ?? 10,
|
||||||
page,
|
page,
|
||||||
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
|
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
|
||||||
@@ -274,6 +315,31 @@ export const buildJoinAggregation = async ({
|
|||||||
polymorphicSuffix = '.value'
|
polymorphicSuffix = '.value'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addTotalDocsAggregation = (as: string, foreignField: string) =>
|
||||||
|
aggregate.push(
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
as: `${as}.totalDocs`,
|
||||||
|
foreignField,
|
||||||
|
from: adapter.collections[slug].collection.name,
|
||||||
|
localField: versions ? 'parent' : '_id',
|
||||||
|
pipeline: [
|
||||||
|
{
|
||||||
|
$match,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$count: 'result',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$addFields: {
|
||||||
|
[`${as}.totalDocs`]: { $ifNull: [{ $first: `$${as}.totalDocs.result` }, 0] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if (adapter.payload.config.localization && locale === 'all') {
|
if (adapter.payload.config.localization && locale === 'all') {
|
||||||
adapter.payload.config.localization.localeCodes.forEach((code) => {
|
adapter.payload.config.localization.localeCodes.forEach((code) => {
|
||||||
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${code}`
|
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${code}`
|
||||||
@@ -304,6 +370,7 @@ export const buildJoinAggregation = async ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if (limitJoin > 0) {
|
if (limitJoin > 0) {
|
||||||
aggregate.push({
|
aggregate.push({
|
||||||
$addFields: {
|
$addFields: {
|
||||||
@@ -313,6 +380,10 @@ export const buildJoinAggregation = async ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
addTotalDocsAggregation(as, `${join.field.on}${code}${polymorphicSuffix}`)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const localeSuffix =
|
const localeSuffix =
|
||||||
@@ -359,6 +430,11 @@ export const buildJoinAggregation = async ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
addTotalDocsAggregation(as, foreignField)
|
||||||
|
}
|
||||||
|
|
||||||
if (limitJoin > 0) {
|
if (limitJoin > 0) {
|
||||||
aggregate.push({
|
aggregate.push({
|
||||||
$addFields: {
|
$addFields: {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
|||||||
import type { SQLiteSelectBase } from 'drizzle-orm/sqlite-core'
|
import type { SQLiteSelectBase } from 'drizzle-orm/sqlite-core'
|
||||||
import type { FlattenedField, JoinQuery, SelectMode, SelectType, Where } from 'payload'
|
import type { FlattenedField, JoinQuery, SelectMode, SelectType, Where } from 'payload'
|
||||||
|
|
||||||
import { and, asc, desc, eq, or, sql } from 'drizzle-orm'
|
import { and, asc, count, desc, eq, or, sql } from 'drizzle-orm'
|
||||||
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
|
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
|
||||||
import toSnakeCase from 'to-snake-case'
|
import toSnakeCase from 'to-snake-case'
|
||||||
|
|
||||||
@@ -386,6 +386,7 @@ export const traverseFields = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
count: shouldCount = false,
|
||||||
limit: limitArg = field.defaultLimit ?? 10,
|
limit: limitArg = field.defaultLimit ?? 10,
|
||||||
page,
|
page,
|
||||||
sort = field.defaultSort,
|
sort = field.defaultSort,
|
||||||
@@ -480,6 +481,13 @@ export const traverseFields = ({
|
|||||||
sqlWhere = and(sqlWhere, buildSQLWhere(where, subQueryAlias))
|
sqlWhere = and(sqlWhere, buildSQLWhere(where, subQueryAlias))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldCount) {
|
||||||
|
currentArgs.extras[`${columnName}_count`] = sql`${db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(sql`${currentQuery.as(subQueryAlias)}`)
|
||||||
|
.where(sqlWhere)}`.as(`${columnName}_count`)
|
||||||
|
}
|
||||||
|
|
||||||
currentQuery = currentQuery.orderBy(sortOrder(sql`"sortPath"`)) as SQLSelect
|
currentQuery = currentQuery.orderBy(sortOrder(sql`"sortPath"`)) as SQLSelect
|
||||||
|
|
||||||
if (page && limit !== 0) {
|
if (page && limit !== 0) {
|
||||||
@@ -611,6 +619,20 @@ export const traverseFields = ({
|
|||||||
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
|
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
|
||||||
}).as(subQueryAlias)
|
}).as(subQueryAlias)
|
||||||
|
|
||||||
|
if (shouldCount) {
|
||||||
|
currentArgs.extras[`${columnName}_count`] = sql`${db
|
||||||
|
.select({
|
||||||
|
count: count(),
|
||||||
|
})
|
||||||
|
.from(
|
||||||
|
sql`${db
|
||||||
|
.select(selectFields as any)
|
||||||
|
.from(newAliasTable)
|
||||||
|
.where(subQueryWhere)
|
||||||
|
.as(`${subQueryAlias}_count_subquery`)}`,
|
||||||
|
)}`.as(`${subQueryAlias}_count`)
|
||||||
|
}
|
||||||
|
|
||||||
currentArgs.extras[columnName] = sql`${db
|
currentArgs.extras[columnName] = sql`${db
|
||||||
.select({
|
.select({
|
||||||
result: jsonAggBuildObject(adapter, {
|
result: jsonAggBuildObject(adapter, {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { FlattenedBlock, FlattenedField, JoinQuery, SanitizedConfig } from 'payload'
|
import type { FlattenedBlock, FlattenedField, JoinQuery, SanitizedConfig } from 'payload'
|
||||||
|
|
||||||
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
|
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
|
||||||
|
import toSnakeCase from 'to-snake-case'
|
||||||
|
|
||||||
import type { DrizzleAdapter } from '../../types.js'
|
import type { DrizzleAdapter } from '../../types.js'
|
||||||
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
|
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
|
||||||
@@ -398,7 +399,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === 'join') {
|
if (field.type === 'join') {
|
||||||
const { limit = field.defaultLimit ?? 10 } =
|
const { count, limit = field.defaultLimit ?? 10 } =
|
||||||
joinQuery?.[`${fieldPrefix.replaceAll('_', '.')}${field.name}`] || {}
|
joinQuery?.[`${fieldPrefix.replaceAll('_', '.')}${field.name}`] || {}
|
||||||
|
|
||||||
// raw hasMany results from SQLite
|
// raw hasMany results from SQLite
|
||||||
@@ -407,8 +408,8 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let fieldResult:
|
let fieldResult:
|
||||||
| { docs: unknown[]; hasNextPage: boolean }
|
| { docs: unknown[]; hasNextPage: boolean; totalDocs?: number }
|
||||||
| Record<string, { docs: unknown[]; hasNextPage: boolean }>
|
| Record<string, { docs: unknown[]; hasNextPage: boolean; totalDocs?: number }>
|
||||||
if (Array.isArray(fieldData)) {
|
if (Array.isArray(fieldData)) {
|
||||||
if (isLocalized && adapter.payload.config.localization) {
|
if (isLocalized && adapter.payload.config.localization) {
|
||||||
fieldResult = fieldData.reduce(
|
fieldResult = fieldData.reduce(
|
||||||
@@ -449,6 +450,17 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
const countPath = `${fieldName}_count`
|
||||||
|
if (typeof table[countPath] !== 'undefined') {
|
||||||
|
let value = Number(table[countPath])
|
||||||
|
if (Number.isNaN(value)) {
|
||||||
|
value = 0
|
||||||
|
}
|
||||||
|
fieldResult.totalDocs = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result[field.name] = fieldResult
|
result[field.name] = fieldResult
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -607,6 +619,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
deletions,
|
deletions,
|
||||||
fieldPrefix: groupFieldPrefix,
|
fieldPrefix: groupFieldPrefix,
|
||||||
fields: field.flattenedFields,
|
fields: field.flattenedFields,
|
||||||
|
joinQuery,
|
||||||
numbers,
|
numbers,
|
||||||
parentIsLocalized: parentIsLocalized || field.localized,
|
parentIsLocalized: parentIsLocalized || field.localized,
|
||||||
path: `${sanitizedPath}${field.name}`,
|
path: `${sanitizedPath}${field.name}`,
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export type JoinQuery<TSlug extends CollectionSlug = string> =
|
|||||||
| Partial<{
|
| Partial<{
|
||||||
[K in keyof TypedCollectionJoins[TSlug]]:
|
[K in keyof TypedCollectionJoins[TSlug]]:
|
||||||
| {
|
| {
|
||||||
|
count?: boolean
|
||||||
limit?: number
|
limit?: number
|
||||||
page?: number
|
page?: number
|
||||||
sort?: string
|
sort?: string
|
||||||
|
|||||||
@@ -434,14 +434,15 @@ export function fieldsToJSONSchema(
|
|||||||
|
|
||||||
fieldSchema = {
|
fieldSchema = {
|
||||||
...baseFieldSchema,
|
...baseFieldSchema,
|
||||||
type: withNullableJSONSchemaType('object', false),
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
properties: {
|
properties: {
|
||||||
docs: {
|
docs: {
|
||||||
type: withNullableJSONSchemaType('array', false),
|
type: 'array',
|
||||||
items,
|
items,
|
||||||
},
|
},
|
||||||
hasNextPage: { type: withNullableJSONSchemaType('boolean', false) },
|
hasNextPage: { type: 'boolean' },
|
||||||
|
totalDocs: { type: 'number' },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const sanitizeJoinParams = (
|
|||||||
joinQuery[schemaPath] = false
|
joinQuery[schemaPath] = false
|
||||||
} else {
|
} else {
|
||||||
joinQuery[schemaPath] = {
|
joinQuery[schemaPath] = {
|
||||||
|
count: joins[schemaPath].count === 'true',
|
||||||
limit: isNumber(joins[schemaPath]?.limit) ? Number(joins[schemaPath].limit) : undefined,
|
limit: isNumber(joins[schemaPath]?.limit) ? Number(joins[schemaPath].limit) : undefined,
|
||||||
page: isNumber(joins[schemaPath]?.page) ? Number(joins[schemaPath].page) : undefined,
|
page: isNumber(joins[schemaPath]?.page) ? Number(joins[schemaPath].page) : undefined,
|
||||||
sort: joins[schemaPath]?.sort ? joins[schemaPath].sort : undefined,
|
sort: joins[schemaPath]?.sort ? joins[schemaPath].sort : undefined,
|
||||||
|
|||||||
@@ -186,6 +186,36 @@ describe('Joins Field', () => {
|
|||||||
expect(categoryWithPosts.group.relatedPosts.docs[0].title).toStrictEqual('test 9')
|
expect(categoryWithPosts.group.relatedPosts.docs[0].title).toStrictEqual('test 9')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should count joins', async () => {
|
||||||
|
let categoryWithPosts = await payload.findByID({
|
||||||
|
id: category.id,
|
||||||
|
joins: {
|
||||||
|
'group.relatedPosts': {
|
||||||
|
sort: '-title',
|
||||||
|
count: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collection: categoriesSlug,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(categoryWithPosts.group.relatedPosts?.totalDocs).toBe(15)
|
||||||
|
|
||||||
|
// With limit 1
|
||||||
|
categoryWithPosts = await payload.findByID({
|
||||||
|
id: category.id,
|
||||||
|
joins: {
|
||||||
|
'group.relatedPosts': {
|
||||||
|
sort: '-title',
|
||||||
|
count: true,
|
||||||
|
limit: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collection: categoriesSlug,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(categoryWithPosts.group.relatedPosts?.totalDocs).toBe(15)
|
||||||
|
})
|
||||||
|
|
||||||
it('should populate relationships in joins', async () => {
|
it('should populate relationships in joins', async () => {
|
||||||
const { docs } = await payload.find({
|
const { docs } = await payload.find({
|
||||||
limit: 1,
|
limit: 1,
|
||||||
@@ -1302,6 +1332,39 @@ describe('Joins Field', () => {
|
|||||||
|
|
||||||
expect(parent.children?.docs).toHaveLength(1)
|
expect(parent.children?.docs).toHaveLength(1)
|
||||||
expect(parent.children.docs[0]?.value.title).toBe('doc-2')
|
expect(parent.children.docs[0]?.value.title).toBe('doc-2')
|
||||||
|
|
||||||
|
// counting
|
||||||
|
parent = await payload.findByID({
|
||||||
|
collection: 'multiple-collections-parents',
|
||||||
|
id: parent.id,
|
||||||
|
depth: 1,
|
||||||
|
joins: {
|
||||||
|
children: {
|
||||||
|
count: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(parent.children?.totalDocs).toBe(2)
|
||||||
|
|
||||||
|
// counting filtered
|
||||||
|
parent = await payload.findByID({
|
||||||
|
collection: 'multiple-collections-parents',
|
||||||
|
id: parent.id,
|
||||||
|
depth: 1,
|
||||||
|
joins: {
|
||||||
|
children: {
|
||||||
|
count: true,
|
||||||
|
where: {
|
||||||
|
relationTo: {
|
||||||
|
equals: 'multiple-collections-2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(parent.children?.totalDocs).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -216,9 +216,10 @@ export interface UserAuthOperations {
|
|||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
posts?: {
|
posts?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -324,9 +325,10 @@ export interface Post {
|
|||||||
export interface Upload {
|
export interface Upload {
|
||||||
id: string;
|
id: string;
|
||||||
relatedPosts?: {
|
relatedPosts?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -347,74 +349,90 @@ export interface Category {
|
|||||||
id: string;
|
id: string;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
relatedPosts?: {
|
relatedPosts?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Static Description
|
* Static Description
|
||||||
*/
|
*/
|
||||||
hasManyPosts?: {
|
hasManyPosts?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
hasManyPostsLocalized?: {
|
hasManyPostsLocalized?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
hiddenPosts?: {
|
hiddenPosts?: {
|
||||||
docs?: (string | HiddenPost)[] | null;
|
docs?: (string | HiddenPost)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
group?: {
|
group?: {
|
||||||
relatedPosts?: {
|
relatedPosts?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
camelCasePosts?: {
|
camelCasePosts?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
arrayPosts?: {
|
arrayPosts?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
localizedArrayPosts?: {
|
localizedArrayPosts?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
blocksPosts?: {
|
blocksPosts?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
polymorphic?: {
|
polymorphic?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
polymorphics?: {
|
polymorphics?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
localizedPolymorphic?: {
|
localizedPolymorphic?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
localizedPolymorphics?: {
|
localizedPolymorphics?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
singulars?: {
|
singulars?: {
|
||||||
docs?: (string | Singular)[] | null;
|
docs?: (string | Singular)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
filtered?: {
|
filtered?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
joinWithError?: {
|
joinWithError?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
enableErrorOnJoin?: boolean | null;
|
enableErrorOnJoin?: boolean | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -460,13 +478,15 @@ export interface Version {
|
|||||||
export interface CategoriesVersion {
|
export interface CategoriesVersion {
|
||||||
id: string;
|
id: string;
|
||||||
relatedVersions?: {
|
relatedVersions?: {
|
||||||
docs?: (string | Version)[] | null;
|
docs?: (string | Version)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
relatedVersionsMany?: {
|
relatedVersionsMany?: {
|
||||||
docs?: (string | Version)[] | null;
|
docs?: (string | Version)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
_status?: ('draft' | 'published') | null;
|
_status?: ('draft' | 'published') | null;
|
||||||
@@ -479,9 +499,10 @@ export interface SelfJoin {
|
|||||||
id: string;
|
id: string;
|
||||||
rel?: (string | null) | SelfJoin;
|
rel?: (string | null) | SelfJoin;
|
||||||
joins?: {
|
joins?: {
|
||||||
docs?: (string | SelfJoin)[] | null;
|
docs?: (string | SelfJoin)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -504,9 +525,10 @@ export interface LocalizedCategory {
|
|||||||
id: string;
|
id: string;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
relatedPosts?: {
|
relatedPosts?: {
|
||||||
docs?: (string | LocalizedPost)[] | null;
|
docs?: (string | LocalizedPost)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -518,9 +540,10 @@ export interface RestrictedCategory {
|
|||||||
id: string;
|
id: string;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
restrictedPosts?: {
|
restrictedPosts?: {
|
||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -532,9 +555,10 @@ export interface CategoriesJoinRestricted {
|
|||||||
id: string;
|
id: string;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
collectionRestrictedJoin?: {
|
collectionRestrictedJoin?: {
|
||||||
docs?: (string | CollectionRestricted)[] | null;
|
docs?: (string | CollectionRestricted)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -570,9 +594,10 @@ export interface DepthJoins1 {
|
|||||||
id: string;
|
id: string;
|
||||||
rel?: (string | null) | DepthJoins2;
|
rel?: (string | null) | DepthJoins2;
|
||||||
joins?: {
|
joins?: {
|
||||||
docs?: (string | DepthJoins3)[] | null;
|
docs?: (string | DepthJoins3)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -583,9 +608,10 @@ export interface DepthJoins1 {
|
|||||||
export interface DepthJoins2 {
|
export interface DepthJoins2 {
|
||||||
id: string;
|
id: string;
|
||||||
joins?: {
|
joins?: {
|
||||||
docs?: (string | DepthJoins1)[] | null;
|
docs?: (string | DepthJoins1)[];
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean;
|
||||||
} | null;
|
totalDocs?: number;
|
||||||
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -606,20 +632,19 @@ export interface DepthJoins3 {
|
|||||||
export interface MultipleCollectionsParent {
|
export interface MultipleCollectionsParent {
|
||||||
id: string;
|
id: string;
|
||||||
children?: {
|
children?: {
|
||||||
docs?:
|
docs?: (
|
||||||
| (
|
| {
|
||||||
| {
|
relationTo?: 'multiple-collections-1';
|
||||||
relationTo?: 'multiple-collections-1';
|
value: string | MultipleCollections1;
|
||||||
value: string | MultipleCollections1;
|
}
|
||||||
}
|
| {
|
||||||
| {
|
relationTo?: 'multiple-collections-2';
|
||||||
relationTo?: 'multiple-collections-2';
|
value: string | MultipleCollections2;
|
||||||
value: string | MultipleCollections2;
|
}
|
||||||
}
|
)[];
|
||||||
)[]
|
hasNextPage?: boolean;
|
||||||
| null;
|
totalDocs?: number;
|
||||||
hasNextPage?: boolean | null;
|
};
|
||||||
} | null;
|
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -656,24 +681,23 @@ export interface Folder {
|
|||||||
folder?: (string | null) | Folder;
|
folder?: (string | null) | Folder;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
children?: {
|
children?: {
|
||||||
docs?:
|
docs?: (
|
||||||
| (
|
| {
|
||||||
| {
|
relationTo?: 'folders';
|
||||||
relationTo?: 'folders';
|
value: string | Folder;
|
||||||
value: string | Folder;
|
}
|
||||||
}
|
| {
|
||||||
| {
|
relationTo?: 'example-pages';
|
||||||
relationTo?: 'example-pages';
|
value: string | ExamplePage;
|
||||||
value: string | ExamplePage;
|
}
|
||||||
}
|
| {
|
||||||
| {
|
relationTo?: 'example-posts';
|
||||||
relationTo?: 'example-posts';
|
value: string | ExamplePost;
|
||||||
value: string | ExamplePost;
|
}
|
||||||
}
|
)[];
|
||||||
)[]
|
hasNextPage?: boolean;
|
||||||
| null;
|
totalDocs?: number;
|
||||||
hasNextPage?: boolean | null;
|
};
|
||||||
} | null;
|
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user