chore: extracts entityToJSONSchema from generateTypes

This commit is contained in:
Jacob Fletcher
2023-01-19 14:16:21 -05:00
parent 8b08e5a1f9
commit e80ead17a8
3 changed files with 423 additions and 420 deletions

View File

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

View File

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

View File

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