Skip to content

Commit 213b7c6

Browse files
authored
feat: generate types for joins (#9054)
### What? Generates types for `joins` property. Example from our `joins` test, keys are type-safe: <img width="708" alt="image" src="https://github.com/user-attachments/assets/f1fbbb9d-7c39-49a2-8aa2-a4793ae4ad7e"> Output in `payload-types.ts`: ```ts collectionsJoins: { categories: { relatedPosts: 'posts'; hasManyPosts: 'posts'; hasManyPostsLocalized: 'posts'; 'group.relatedPosts': 'posts'; 'group.camelCasePosts': 'posts'; filtered: 'posts'; singulars: 'singular'; }; }; ``` Additionally, we include type information about on which collection the join is, it will help when we have types generation for `where` and `sort`. ### Why? It provides a better DX as you don't need to memoize your keys. ### How? Modifies `configToJSONSchema` to generate the json schema for `collectionsJoins`, uses that type within `JoinQuery`
1 parent 7dc5256 commit 213b7c6

File tree

7 files changed

+96
-14
lines changed

7 files changed

+96
-14
lines changed

packages/payload/src/collections/operations/local/find.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
3333
draft?: boolean
3434
fallbackLocale?: TypedLocale
3535
includeLockStatus?: boolean
36-
joins?: JoinQuery
36+
joins?: JoinQuery<TSlug>
3737
limit?: number
3838
locale?: 'all' | TypedLocale
3939
overrideAccess?: boolean

packages/payload/src/collections/operations/local/findByID.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export type Options<
3737
fallbackLocale?: TypedLocale
3838
id: number | string
3939
includeLockStatus?: boolean
40-
joins?: JoinQuery
40+
joins?: JoinQuery<TSlug>
4141
locale?: 'all' | TypedLocale
4242
overrideAccess?: boolean
4343
populate?: PopulateType

packages/payload/src/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ export interface GeneratedTypes {
9797
}
9898
}
9999
}
100+
101+
collectionsJoinsUntyped: {
102+
[slug: string]: {
103+
[schemaPath: string]: CollectionSlug
104+
}
105+
}
100106
collectionsSelectUntyped: {
101107
[slug: string]: SelectType
102108
}
@@ -141,6 +147,12 @@ type ResolveCollectionSelectType<T> = 'collectionsSelect' extends keyof T
141147
? T['collectionsSelect']
142148
: // @ts-expect-error
143149
T['collectionsSelectUntyped']
150+
151+
type ResolveCollectionJoinsType<T> = 'collectionsJoins' extends keyof T
152+
? T['collectionsJoins']
153+
: // @ts-expect-error
154+
T['collectionsJoinsUntyped']
155+
144156
type ResolveGlobalType<T> = 'globals' extends keyof T
145157
? T['globals']
146158
: // @ts-expect-error
@@ -155,6 +167,9 @@ type ResolveGlobalSelectType<T> = 'globalsSelect' extends keyof T
155167
export type TypedCollection = ResolveCollectionType<GeneratedTypes>
156168

157169
export type TypedCollectionSelect = ResolveCollectionSelectType<GeneratedTypes>
170+
171+
export type TypedCollectionJoins = ResolveCollectionJoinsType<GeneratedTypes>
172+
158173
export type TypedGlobal = ResolveGlobalType<GeneratedTypes>
159174

160175
export type TypedGlobalSelect = ResolveGlobalSelectType<GeneratedTypes>

packages/payload/src/types/index.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
DataFromGlobalSlug,
1414
GlobalSlug,
1515
RequestContext,
16+
TypedCollectionJoins,
1617
TypedCollectionSelect,
1718
TypedLocale,
1819
TypedUser,
@@ -123,17 +124,20 @@ export type Sort = Array<string> | string
123124
/**
124125
* Applies pagination for join fields for including collection relationships
125126
*/
126-
export type JoinQuery =
127-
| {
128-
[schemaPath: string]:
129-
| {
130-
limit?: number
131-
sort?: string
132-
where?: Where
133-
}
127+
export type JoinQuery<TSlug extends CollectionSlug = string> =
128+
TypedCollectionJoins[TSlug] extends Record<string, string>
129+
?
134130
| false
135-
}
136-
| false
131+
| Partial<{
132+
[K in keyof TypedCollectionJoins[TSlug]]:
133+
| {
134+
limit?: number
135+
sort?: string
136+
where?: Where
137+
}
138+
| false
139+
}>
140+
: never
137141

138142
// eslint-disable-next-line @typescript-eslint/no-explicit-any
139143
export type Document = any

packages/payload/src/utilities/configToJSONSchema.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,43 @@ function generateEntitySelectSchemas(
9393
}
9494
}
9595

96+
function generateCollectionJoinsSchemas(collections: SanitizedCollectionConfig[]): JSONSchema4 {
97+
const properties = [...collections].reduce<Record<string, JSONSchema4>>(
98+
(acc, { slug, joins }) => {
99+
const schema = {
100+
type: 'object',
101+
additionalProperties: false,
102+
properties: {},
103+
required: [],
104+
} satisfies JSONSchema4
105+
106+
for (const collectionSlug in joins) {
107+
for (const join of joins[collectionSlug]) {
108+
schema.properties[join.schemaPath] = {
109+
type: 'string',
110+
enum: [collectionSlug],
111+
}
112+
schema.required.push(join.schemaPath)
113+
}
114+
}
115+
116+
if (Object.keys(schema.properties).length > 0) {
117+
acc[slug] = schema
118+
}
119+
120+
return acc
121+
},
122+
{},
123+
)
124+
125+
return {
126+
type: 'object',
127+
additionalProperties: false,
128+
properties,
129+
required: Object.keys(properties),
130+
}
131+
}
132+
96133
function generateLocaleEntitySchemas(localization: SanitizedConfig['localization']): JSONSchema4 {
97134
if (localization && 'locales' in localization && localization?.locales) {
98135
const localesFromConfig = localization?.locales
@@ -989,6 +1026,7 @@ export function configToJSONSchema(
9891026
properties: {
9901027
auth: generateAuthOperationSchemas(config.collections),
9911028
collections: generateEntitySchemas(config.collections || []),
1029+
collectionsJoins: generateCollectionJoinsSchemas(config.collections || []),
9921030
collectionsSelect: generateEntitySelectSchemas(config.collections || []),
9931031
db: generateDbEntitySchema(config),
9941032
globals: generateEntitySchemas(config.globals || []),
@@ -1001,6 +1039,7 @@ export function configToJSONSchema(
10011039
'locale',
10021040
'collections',
10031041
'collectionsSelect',
1042+
'collectionsJoins',
10041043
'globalsSelect',
10051044
'globals',
10061045
'auth',

test/joins/payload-types.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,30 @@ export interface Config {
2626
'payload-preferences': PayloadPreference;
2727
'payload-migrations': PayloadMigration;
2828
};
29-
collectionsSelect?: {
29+
collectionsJoins: {
30+
categories: {
31+
relatedPosts: 'posts';
32+
hasManyPosts: 'posts';
33+
hasManyPostsLocalized: 'posts';
34+
'group.relatedPosts': 'posts';
35+
'group.camelCasePosts': 'posts';
36+
filtered: 'posts';
37+
singulars: 'singular';
38+
};
39+
uploads: {
40+
relatedPosts: 'posts';
41+
};
42+
'categories-versions': {
43+
relatedVersions: 'versions';
44+
};
45+
'localized-categories': {
46+
relatedPosts: 'localized-posts';
47+
};
48+
'restricted-categories': {
49+
restrictedPosts: 'posts';
50+
};
51+
};
52+
collectionsSelect: {
3053
posts: PostsSelect<false> | PostsSelect<true>;
3154
categories: CategoriesSelect<false> | CategoriesSelect<true>;
3255
uploads: UploadsSelect<false> | UploadsSelect<true>;
@@ -46,7 +69,7 @@ export interface Config {
4669
defaultIDType: string;
4770
};
4871
globals: {};
49-
globalsSelect?: {};
72+
globalsSelect: {};
5073
locale: 'en' | 'es';
5174
user: User & {
5275
collection: 'users';

test/select/payload-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface Config {
2121
'payload-preferences': PayloadPreference;
2222
'payload-migrations': PayloadMigration;
2323
};
24+
collectionsJoins: {};
2425
collectionsSelect: {
2526
posts: PostsSelect<false> | PostsSelect<true>;
2627
'localized-posts': LocalizedPostsSelect<false> | LocalizedPostsSelect<true>;

0 commit comments

Comments
 (0)