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:
Sasha
2024-11-06 22:43:07 +02:00
committed by GitHub
parent 7dc52567f1
commit 213b7c6fb6
7 changed files with 96 additions and 14 deletions

View File

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

View File

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

View File

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

View File

@@ -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]:
export type JoinQuery<TSlug extends CollectionSlug = string> =
TypedCollectionJoins[TSlug] extends Record<string, string>
?
| false
| Partial<{
[K in keyof TypedCollectionJoins[TSlug]]:
| {
limit?: number
sort?: string
where?: Where
}
| false
}
| false
}>
: never
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Document = any

View File

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

View File

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

View File

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