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:
Sasha
2025-02-27 18:05:48 +02:00
committed by GitHub
parent bcc68572bf
commit 3436fb16ea
9 changed files with 345 additions and 139 deletions

View File

@@ -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.

View File

@@ -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: {

View File

@@ -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, {

View File

@@ -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}`,

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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)
}) })
}) })
}) })

View File

@@ -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;
} }