Files
payload/src/bin/generateTypes.ts
Dan Ribbens bab34d82f5 feat: add i18n to admin panel (#1326)
Co-authored-by: shikhantmaungs <shinkhantmaungs@gmail.com>
Co-authored-by: Thomas Ghysels <info@thomasg.be>
Co-authored-by: Kokutse Djoguenou <kokutse@Kokutses-MacBook-Pro.local>
Co-authored-by: Christian Gil <47041342+ChrisGV04@users.noreply.github.com>
Co-authored-by: Łukasz Rabiec <lukaszrabiec@gmail.com>
Co-authored-by: Jenny <jennifer.eberlei@gmail.com>
Co-authored-by: Hung Vu <hunghvu2017@gmail.com>
Co-authored-by: Shin Khant Maung <101539335+shinkhantmaungs@users.noreply.github.com>
Co-authored-by: Carlo Brualdi <carlo.brualdi@gmail.com>
Co-authored-by: Ariel Tonglet <ariel.tonglet@gmail.com>
Co-authored-by: Roman Ryzhikov <general+github@ya.ru>
Co-authored-by: maekoya <maekoya@stromatolite.jp>
Co-authored-by: Emilia Trollros <3m1l1a@emiliatrollros.se>
Co-authored-by: Kokutse J Djoguenou <90865585+Julesdj@users.noreply.github.com>
Co-authored-by: Mitch Dries <mitch.dries@gmail.com>

BREAKING CHANGE: If you assigned labels to collections, globals or block names, you need to update your config! Your GraphQL schema and generated Typescript interfaces may have changed. Payload no longer uses labels for code based naming. To prevent breaking changes to your GraphQL API and typescript types in your project, you can assign the below properties to match what Payload previously generated for you from labels.

On Collections
Use `graphQL.singularName`, `graphQL.pluralName` for GraphQL schema names.
Use `typescript.interface` for typescript generation name.

On Globals
Use `graphQL.name` for GraphQL Schema name.
Use `typescript.interface` for typescript generation name.

On Blocks (within Block fields)
Use `graphQL.singularName` for graphQL schema names.
2022-11-18 07:36:30 -05:00

452 lines
13 KiB
TypeScript

/* eslint-disable no-nested-ternary */
import fs from 'fs';
import type { JSONSchema4 } from 'json-schema';
import { compile } from 'json-schema-to-typescript';
import { singular } from 'pluralize';
import Logger from '../utilities/logger';
import { fieldAffectsData, Field, Option, FieldAffectingData, tabHasName } 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';
import deepCopyObject from '../utilities/deepCopyObject';
import { groupOrTabHasRequiredSubfield } from '../utilities/groupOrTabHasRequiredSubfield';
import { toWords } from '../utilities/formatLabels';
const nonOptionalFieldTypes = ['group', 'array', 'blocks'];
const propertyIsOptional = (field: Field) => {
return fieldAffectsData(field) && (field.required === true || nonOptionalFieldTypes.includes(field.type));
};
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 = {
oneOf: [
{
type: 'array',
items: {
oneOf: field.relationTo.map((relation) => {
const idFieldType = getCollectionIDType(config.collections, relation);
return {
type: 'object',
additionalProperties: false,
properties: {
value: {
type: idFieldType,
},
relationTo: {
const: relation,
},
},
required: ['value', 'relationTo'],
};
}),
},
},
{
type: 'array',
items: {
oneOf: field.relationTo.map((relation) => {
return {
type: 'object',
additionalProperties: false,
properties: {
value: {
$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 = {
oneOf: [
{
type: 'array',
items: {
type: idFieldType,
},
},
{
type: 'array',
items: {
$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':
case 'collapsible': {
const topLevelFields = generateFieldTypes(config, field.fields);
requiredTopLevelProps = requiredTopLevelProps.concat(topLevelFields.required);
topLevelProps = topLevelProps.concat(Object.entries(topLevelFields.properties).map((prop) => prop));
break;
}
case 'tabs': {
field.tabs.forEach((tab) => {
if (tabHasName(tab)) {
const hasRequiredSubfields = groupOrTabHasRequiredSubfield(tab);
if (hasRequiredSubfields) requiredTopLevelProps.push(tab.name);
topLevelProps.push([
tab.name,
{
type: 'object',
additionalProperties: false,
...generateFieldTypes(config, tab.fields),
},
]);
} else {
const topLevelFields = generateFieldTypes(config, tab.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(propertyIsOptional)
.map((field) => (fieldAffectsData(field) ? field.name : '')),
...requiredTopLevelProps,
],
};
}
function entityToJsonSchema(config: SanitizedConfig, incomingEntity: SanitizedCollectionConfig | SanitizedGlobalConfig): JSONSchema4 {
const entity = deepCopyObject(incomingEntity);
const title = entity.typescript?.interface ? entity.typescript.interface : singular(toWords(entity.slug, true));
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);
}
if ('timestamps' in entity && entity.timestamps !== false) {
entity.fields.push(
{
type: 'text',
name: 'createdAt',
required: true,
},
{
type: 'text',
name: 'updatedAt',
required: true,
},
);
}
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 logger = Logger();
const config = loadConfig();
const outputFile = process.env.PAYLOAD_TS_OUTPUT_PATH || config.typescript.outputFile;
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(outputFile, compiled);
logger.info(`Types written to ${outputFile}`);
});
}
// when generateTypes.js is launched directly
if (module.id === require.main.id) {
generateTypes();
}