/* eslint-disable no-nested-ternary */ import fs from 'fs'; import type { JSONSchema4 } from 'json-schema'; import { compile } from 'json-schema-to-typescript'; import payload from '..'; import { fieldAffectsData, Field, Option, FieldAffectingData } from '../fields/config/types'; import { SanitizedCollectionConfig } from '../collections/config/types'; import { SanitizedConfig } from '../config/types'; import loadConfig from '../config/load'; import { SanitizedGlobalConfig } from '../globals/config/types'; function getCollectionIDType(collections: SanitizedCollectionConfig[], slug: string): 'string' | 'number' { const matchedCollection = collections.find((collection) => collection.slug === slug); const customIdField = matchedCollection.fields.find((field) => 'name' in field && field.name === 'id'); if (customIdField && customIdField.type === 'number') { return 'number'; } return 'string'; } function returnOptionEnums(options: Option[]): string[] { return options.map((option) => { if (typeof option === 'object' && 'value' in option) { return option.value; } return option; }); } function generateFieldTypes(config: SanitizedConfig, fields: Field[]): { properties: { [k: string]: JSONSchema4; } required: string[] } { let topLevelProps = []; let requiredTopLevelProps = []; return { properties: Object.fromEntries( fields.reduce((properties, field) => { let fieldSchema: JSONSchema4; switch (field.type) { case 'text': case 'textarea': case 'code': case 'email': case 'date': { fieldSchema = { type: 'string' }; break; } case 'number': { fieldSchema = { type: 'number' }; break; } case 'checkbox': { fieldSchema = { type: 'boolean' }; break; } case 'richText': { fieldSchema = { type: 'array', items: { type: 'object', }, }; break; } case 'radio': { fieldSchema = { type: 'string', enum: returnOptionEnums(field.options), }; break; } case 'select': { const selectType: JSONSchema4 = { type: 'string', enum: returnOptionEnums(field.options), }; if (field.hasMany) { fieldSchema = { type: 'array', items: selectType, }; } else { fieldSchema = selectType; } break; } case 'point': { fieldSchema = { type: 'array', minItems: 2, maxItems: 2, items: [ { type: 'number', }, { type: 'number', }, ], }; break; } case 'relationship': { if (Array.isArray(field.relationTo)) { if (field.hasMany) { fieldSchema = { type: 'array', items: { oneOf: field.relationTo.map((relation) => { const idFieldType = getCollectionIDType(config.collections, relation); return { type: 'object', additionalProperties: false, properties: { value: { oneOf: [ { type: idFieldType, }, { $ref: `#/definitions/${relation}`, }, ], }, relationTo: { const: relation, }, }, required: ['value', 'relationTo'], }; }), }, }; } else { fieldSchema = { oneOf: field.relationTo.map((relation) => { const idFieldType = getCollectionIDType(config.collections, relation); return { type: 'object', additionalProperties: false, properties: { value: { oneOf: [ { type: idFieldType, }, { $ref: `#/definitions/${relation}`, }, ], }, relationTo: { const: relation, }, }, required: ['value', 'relationTo'], }; }), }; } } else { const idFieldType = getCollectionIDType(config.collections, field.relationTo); if (field.hasMany) { fieldSchema = { type: 'array', items: { oneOf: [ { type: idFieldType, }, { $ref: `#/definitions/${field.relationTo}`, }, ], }, }; } else { fieldSchema = { oneOf: [ { type: idFieldType, }, { $ref: `#/definitions/${field.relationTo}`, }, ], }; } } break; } case 'upload': { const idFieldType = getCollectionIDType(config.collections, field.relationTo); fieldSchema = { oneOf: [ { type: idFieldType, }, { $ref: `#/definitions/${field.relationTo}`, }, ], }; break; } case 'blocks': { fieldSchema = { type: 'array', items: { oneOf: field.blocks.map((block) => { const blockSchema = generateFieldTypes(config, block.fields); return { type: 'object', additionalProperties: false, properties: { ...blockSchema.properties, blockType: { const: block.slug, }, }, required: [ 'blockType', ...blockSchema.required, ], }; }), }, }; break; } case 'array': { fieldSchema = { type: 'array', items: { type: 'object', additionalProperties: false, ...generateFieldTypes(config, field.fields), }, }; break; } case 'row': { const topLevelFields = generateFieldTypes(config, field.fields); requiredTopLevelProps = requiredTopLevelProps.concat(topLevelFields.required); topLevelProps = topLevelProps.concat(Object.entries(topLevelFields.properties).map((prop) => prop)); break; } case 'group': { fieldSchema = { type: 'object', additionalProperties: false, ...generateFieldTypes(config, field.fields), }; break; } default: { break; } } if (fieldSchema && fieldAffectsData(field)) { return [ ...properties, [ field.name, { ...fieldSchema, }, ], ]; } return [ ...properties, ...topLevelProps, ]; }, []), ), required: [ ...fields .filter((field) => fieldAffectsData(field) && field.required === true) .map((field) => (fieldAffectsData(field) ? field.name : '')), ...requiredTopLevelProps, ], }; } function entityToJsonSchema(config: SanitizedConfig, entity: SanitizedCollectionConfig | SanitizedGlobalConfig): JSONSchema4 { const title = 'label' in entity ? entity.label : entity.labels.singular; const idField: FieldAffectingData = { type: 'text', name: 'id', required: true }; const customIdField = entity.fields.find((field) => fieldAffectsData(field) && field.name === 'id') as FieldAffectingData; if (customIdField) { customIdField.required = true; } else { entity.fields.unshift(idField); } return { title, type: 'object', additionalProperties: false, ...generateFieldTypes(config, entity.fields), }; } function configToJsonSchema(config: SanitizedConfig): JSONSchema4 { return { definitions: Object.fromEntries( [ ...config.globals.map((global) => [ global.slug, entityToJsonSchema(config, global), ]), ...config.collections.map((collection) => [ collection.slug, entityToJsonSchema(config, collection), ]), ], ), additionalProperties: false, }; } export function generateTypes(): void { const config = loadConfig(); payload.logger.info('Compiling TS types for Collections and Globals...'); const jsonSchema = configToJsonSchema(config); compile(jsonSchema, 'Config', { unreachableDefinitions: true, bannerComment: '/* tslint:disable */\n/**\n* This file was automatically generated by Payload CMS.\n* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,\n* and re-run `payload generate:types` to regenerate this file.\n*/', style: { singleQuote: true, }, }).then((compiled) => { fs.writeFileSync(config.typescript.outputFile, compiled); payload.logger.info(`Types written to ${config.typescript.outputFile}`); }); } // when generateTypes.js is launched directly if (module.id === require.main.id) { generateTypes(); }