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`
This commit is contained in:
@@ -33,7 +33,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
|
||||
draft?: boolean
|
||||
fallbackLocale?: TypedLocale
|
||||
includeLockStatus?: boolean
|
||||
joins?: JoinQuery
|
||||
joins?: JoinQuery<TSlug>
|
||||
limit?: number
|
||||
locale?: 'all' | TypedLocale
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -37,7 +37,7 @@ export type Options<
|
||||
fallbackLocale?: TypedLocale
|
||||
id: number | string
|
||||
includeLockStatus?: boolean
|
||||
joins?: JoinQuery
|
||||
joins?: JoinQuery<TSlug>
|
||||
locale?: 'all' | TypedLocale
|
||||
overrideAccess?: boolean
|
||||
populate?: PopulateType
|
||||
|
||||
@@ -97,6 +97,12 @@ export interface GeneratedTypes {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectionsJoinsUntyped: {
|
||||
[slug: string]: {
|
||||
[schemaPath: string]: CollectionSlug
|
||||
}
|
||||
}
|
||||
collectionsSelectUntyped: {
|
||||
[slug: string]: SelectType
|
||||
}
|
||||
@@ -141,6 +147,12 @@ type ResolveCollectionSelectType<T> = 'collectionsSelect' extends keyof T
|
||||
? T['collectionsSelect']
|
||||
: // @ts-expect-error
|
||||
T['collectionsSelectUntyped']
|
||||
|
||||
type ResolveCollectionJoinsType<T> = 'collectionsJoins' extends keyof T
|
||||
? T['collectionsJoins']
|
||||
: // @ts-expect-error
|
||||
T['collectionsJoinsUntyped']
|
||||
|
||||
type ResolveGlobalType<T> = 'globals' extends keyof T
|
||||
? T['globals']
|
||||
: // @ts-expect-error
|
||||
@@ -155,6 +167,9 @@ type ResolveGlobalSelectType<T> = 'globalsSelect' extends keyof T
|
||||
export type TypedCollection = ResolveCollectionType<GeneratedTypes>
|
||||
|
||||
export type TypedCollectionSelect = ResolveCollectionSelectType<GeneratedTypes>
|
||||
|
||||
export type TypedCollectionJoins = ResolveCollectionJoinsType<GeneratedTypes>
|
||||
|
||||
export type TypedGlobal = ResolveGlobalType<GeneratedTypes>
|
||||
|
||||
export type TypedGlobalSelect = ResolveGlobalSelectType<GeneratedTypes>
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
DataFromGlobalSlug,
|
||||
GlobalSlug,
|
||||
RequestContext,
|
||||
TypedCollectionJoins,
|
||||
TypedCollectionSelect,
|
||||
TypedLocale,
|
||||
TypedUser,
|
||||
@@ -123,17 +124,20 @@ export type Sort = Array<string> | string
|
||||
/**
|
||||
* Applies pagination for join fields for including collection relationships
|
||||
*/
|
||||
export type JoinQuery =
|
||||
| {
|
||||
[schemaPath: string]:
|
||||
| {
|
||||
limit?: number
|
||||
sort?: string
|
||||
where?: Where
|
||||
}
|
||||
export type JoinQuery<TSlug extends CollectionSlug = string> =
|
||||
TypedCollectionJoins[TSlug] extends Record<string, string>
|
||||
?
|
||||
| false
|
||||
}
|
||||
| false
|
||||
| Partial<{
|
||||
[K in keyof TypedCollectionJoins[TSlug]]:
|
||||
| {
|
||||
limit?: number
|
||||
sort?: string
|
||||
where?: Where
|
||||
}
|
||||
| false
|
||||
}>
|
||||
: never
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type Document = any
|
||||
|
||||
@@ -93,6 +93,43 @@ function generateEntitySelectSchemas(
|
||||
}
|
||||
}
|
||||
|
||||
function generateCollectionJoinsSchemas(collections: SanitizedCollectionConfig[]): JSONSchema4 {
|
||||
const properties = [...collections].reduce<Record<string, JSONSchema4>>(
|
||||
(acc, { slug, joins }) => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {},
|
||||
required: [],
|
||||
} satisfies JSONSchema4
|
||||
|
||||
for (const collectionSlug in joins) {
|
||||
for (const join of joins[collectionSlug]) {
|
||||
schema.properties[join.schemaPath] = {
|
||||
type: 'string',
|
||||
enum: [collectionSlug],
|
||||
}
|
||||
schema.required.push(join.schemaPath)
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(schema.properties).length > 0) {
|
||||
acc[slug] = schema
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
return {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties,
|
||||
required: Object.keys(properties),
|
||||
}
|
||||
}
|
||||
|
||||
function generateLocaleEntitySchemas(localization: SanitizedConfig['localization']): JSONSchema4 {
|
||||
if (localization && 'locales' in localization && localization?.locales) {
|
||||
const localesFromConfig = localization?.locales
|
||||
@@ -989,6 +1026,7 @@ export function configToJSONSchema(
|
||||
properties: {
|
||||
auth: generateAuthOperationSchemas(config.collections),
|
||||
collections: generateEntitySchemas(config.collections || []),
|
||||
collectionsJoins: generateCollectionJoinsSchemas(config.collections || []),
|
||||
collectionsSelect: generateEntitySelectSchemas(config.collections || []),
|
||||
db: generateDbEntitySchema(config),
|
||||
globals: generateEntitySchemas(config.globals || []),
|
||||
@@ -1001,6 +1039,7 @@ export function configToJSONSchema(
|
||||
'locale',
|
||||
'collections',
|
||||
'collectionsSelect',
|
||||
'collectionsJoins',
|
||||
'globalsSelect',
|
||||
'globals',
|
||||
'auth',
|
||||
|
||||
@@ -26,7 +26,30 @@ export interface Config {
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsSelect?: {
|
||||
collectionsJoins: {
|
||||
categories: {
|
||||
relatedPosts: 'posts';
|
||||
hasManyPosts: 'posts';
|
||||
hasManyPostsLocalized: 'posts';
|
||||
'group.relatedPosts': 'posts';
|
||||
'group.camelCasePosts': 'posts';
|
||||
filtered: 'posts';
|
||||
singulars: 'singular';
|
||||
};
|
||||
uploads: {
|
||||
relatedPosts: 'posts';
|
||||
};
|
||||
'categories-versions': {
|
||||
relatedVersions: 'versions';
|
||||
};
|
||||
'localized-categories': {
|
||||
relatedPosts: 'localized-posts';
|
||||
};
|
||||
'restricted-categories': {
|
||||
restrictedPosts: 'posts';
|
||||
};
|
||||
};
|
||||
collectionsSelect: {
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
categories: CategoriesSelect<false> | CategoriesSelect<true>;
|
||||
uploads: UploadsSelect<false> | UploadsSelect<true>;
|
||||
@@ -46,7 +69,7 @@ export interface Config {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect?: {};
|
||||
globalsSelect: {};
|
||||
locale: 'en' | 'es';
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface Config {
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
'localized-posts': LocalizedPostsSelect<false> | LocalizedPostsSelect<true>;
|
||||
|
||||
Reference in New Issue
Block a user