From e80ead17a8335cddb2b6f032e6ef8454305e31c2 Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Thu, 19 Jan 2023 14:16:21 -0500 Subject: [PATCH] chore: extracts entityToJSONSchema from generateTypes --- src/bin/generateTypes.ts | 420 +-------------------------- src/initHTTP.ts | 1 - src/utilities/entityToJSONSchema.ts | 422 ++++++++++++++++++++++++++++ 3 files changed, 423 insertions(+), 420 deletions(-) create mode 100644 src/utilities/entityToJSONSchema.ts diff --git a/src/bin/generateTypes.ts b/src/bin/generateTypes.ts index eded842908..8b2d2103d0 100644 --- a/src/bin/generateTypes.ts +++ b/src/bin/generateTypes.ts @@ -2,428 +2,10 @@ 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 'json': { - fieldSchema = { type: 'object' }; - 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 generateEntityObject(config: SanitizedConfig, type: 'collections' | 'globals'): JSONSchema4 { - return { - type: 'object', - properties: Object.fromEntries(config[type].map(({ slug }) => [ - slug, - { - $ref: `#/definitions/${slug}`, - }, - ])), - required: config[type].map(({ slug }) => slug), - additionalProperties: false, - }; -} +import { entityToJsonSchema, generateEntityObject } from '../utilities/entityToJSONSchema'; function configToJsonSchema(config: SanitizedConfig): JSONSchema4 { return { diff --git a/src/initHTTP.ts b/src/initHTTP.ts index 9a8ca3e2ad..1fb3e5d34d 100644 --- a/src/initHTTP.ts +++ b/src/initHTTP.ts @@ -1,6 +1,5 @@ /* eslint-disable no-param-reassign */ import express, { NextFunction, Response } from 'express'; -import { Config as GeneratedTypes } from 'payload/generated-types'; import { InitOptions } from './config/types'; import authenticate from './express/middleware/authenticate'; diff --git a/src/utilities/entityToJSONSchema.ts b/src/utilities/entityToJSONSchema.ts new file mode 100644 index 0000000000..efd02c5e29 --- /dev/null +++ b/src/utilities/entityToJSONSchema.ts @@ -0,0 +1,422 @@ + +import { singular } from 'pluralize'; +import type { JSONSchema4 } from 'json-schema'; +import { fieldAffectsData, Field, Option, FieldAffectingData, tabHasName } from '../fields/config/types'; +import { SanitizedCollectionConfig } from '../collections/config/types'; +import { SanitizedGlobalConfig } from '../globals/config/types'; +import deepCopyObject from './deepCopyObject'; +import { groupOrTabHasRequiredSubfield } from './groupOrTabHasRequiredSubfield'; +import { toWords } from './formatLabels'; +import { SanitizedConfig } from '../config/types'; + +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 'json': { + fieldSchema = { type: 'object' }; + 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, + ], + }; +} + +export 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), + }; +} + +export function generateEntityObject(config: SanitizedConfig, type: 'collections' | 'globals'): JSONSchema4 { + return { + type: 'object', + properties: Object.fromEntries(config[type].map(({ slug }) => [ + slug, + { + $ref: `#/definitions/${slug}`, + }, + ])), + required: config[type].map(({ slug }) => slug), + additionalProperties: false, + }; +}