UNPKG

@r1tsu/payload

Version:

554 lines (553 loc) 24.1 kB
import pluralize from 'pluralize'; const { singular } = pluralize; import { fieldAffectsData, tabHasName } from '../fields/config/types.js'; import { deepCopyObject } from './deepCopyObject.js'; import { toWords } from './formatLabels.js'; import { getCollectionIDFieldTypes } from './getCollectionIDFieldTypes.js'; const fieldIsRequired = (field)=>{ const isConditional = Boolean(field?.admin && field?.admin?.condition); if (isConditional) return false; const isMarkedRequired = 'required' in field && field.required === true; if (fieldAffectsData(field) && isMarkedRequired) return true; // if any subfields are required, this field is required if ('fields' in field && field.type !== 'array') { return field.fields.some((subField)=>fieldIsRequired(subField)); } // if any tab subfields have required fields, this field is required if (field.type === 'tabs') { return field.tabs.some((tab)=>{ if ('name' in tab) { return tab.fields.some((subField)=>fieldIsRequired(subField)); } return false; }); } return false; }; function buildOptionEnums(options) { return options.map((option)=>{ if (typeof option === 'object' && 'value' in option) { return option.value; } return option; }); } function generateEntitySchemas(entities) { const properties = [ ...entities ].reduce((acc, { slug })=>{ acc[slug] = { $ref: `#/definitions/${slug}` }; return acc; }, {}); return { type: 'object', additionalProperties: false, properties, required: Object.keys(properties) }; } function generateLocaleEntitySchemas(localization) { if (localization && 'locales' in localization && localization?.locales) { const localesFromConfig = localization?.locales; const locales = [ ...localesFromConfig ].map((locale)=>{ return locale.code; }, []); return { type: 'string', enum: locales }; } return { type: 'null' }; } function generateAuthEntitySchemas(entities) { const properties = [ ...entities ].filter(({ auth })=>Boolean(auth)).map(({ slug })=>{ return { allOf: [ { $ref: `#/definitions/${slug}` }, { type: 'object', additionalProperties: false, properties: { collection: { type: 'string', enum: [ slug ] } }, required: [ 'collection' ] } ] }; }, {}); return { oneOf: properties }; } /** * Returns a JSON Schema Type with 'null' added if the field is not required. */ export function withNullableJSONSchemaType(fieldType, isRequired) { const fieldTypes = [ fieldType ]; if (isRequired) return fieldType; fieldTypes.push('null'); return fieldTypes; } export function fieldsToJSONSchema(/** * Used for relationship fields, to determine whether to use a string or number type for the ID. * While there is a default ID field type set by the db adapter, they can differ on a collection-level * if they have custom ID fields. */ collectionIDFieldTypes, fields, /** * Allows you to define new top-level interfaces that can be re-used in the output schema. */ interfaceNameDefinitions, config) { const requiredFieldNames = new Set(); return { properties: Object.fromEntries(fields.reduce((fieldSchemas, field)=>{ const isRequired = fieldAffectsData(field) && fieldIsRequired(field); if (isRequired) requiredFieldNames.add(field.name); let fieldSchema; switch(field.type){ case 'text': if (field.hasMany === true) { fieldSchema = { type: withNullableJSONSchemaType('array', isRequired), items: { type: 'string' } }; } else { fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }; } break; case 'textarea': case 'code': case 'email': case 'date': { fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }; break; } case 'number': { if (field.hasMany === true) { fieldSchema = { type: withNullableJSONSchemaType('array', isRequired), items: { type: 'number' } }; } else { fieldSchema = { type: withNullableJSONSchemaType('number', isRequired) }; } break; } case 'checkbox': { fieldSchema = { type: withNullableJSONSchemaType('boolean', isRequired) }; break; } case 'json': { fieldSchema = { type: [ 'object', 'array', 'string', 'number', 'boolean', 'null' ] }; break; } case 'richText': { if (field.editor.outputSchema) { fieldSchema = field.editor.outputSchema({ collectionIDFieldTypes, config, field, interfaceNameDefinitions, isRequired }); } else { // Maintain backwards compatibility with existing rich text editors fieldSchema = { type: withNullableJSONSchemaType('array', isRequired), items: { type: 'object' } }; } break; } case 'radio': { fieldSchema = { type: withNullableJSONSchemaType('string', isRequired), enum: buildOptionEnums(field.options) }; break; } case 'select': { const optionEnums = buildOptionEnums(field.options); if (field.hasMany) { fieldSchema = { type: withNullableJSONSchemaType('array', isRequired), items: { type: 'string', enum: optionEnums } }; } else { fieldSchema = { type: withNullableJSONSchemaType('string', isRequired), enum: optionEnums }; } break; } case 'point': { fieldSchema = { type: withNullableJSONSchemaType('array', isRequired), items: [ { type: 'number' }, { type: 'number' } ], maxItems: 2, minItems: 2 }; break; } case 'relationship': { if (Array.isArray(field.relationTo)) { if (field.hasMany) { fieldSchema = { type: withNullableJSONSchemaType('array', isRequired), items: { oneOf: field.relationTo.map((relation)=>{ return { type: 'object', additionalProperties: false, properties: { relationTo: { const: relation }, value: { oneOf: [ { type: collectionIDFieldTypes[relation] }, { $ref: `#/definitions/${relation}` } ] } }, required: [ 'value', 'relationTo' ] }; }) } }; } else { fieldSchema = { oneOf: field.relationTo.map((relation)=>{ return { type: withNullableJSONSchemaType('object', isRequired), additionalProperties: false, properties: { relationTo: { const: relation }, value: { oneOf: [ { type: collectionIDFieldTypes[relation] }, { $ref: `#/definitions/${relation}` } ] } }, required: [ 'value', 'relationTo' ] }; }) }; } } else if (field.hasMany) { fieldSchema = { type: withNullableJSONSchemaType('array', isRequired), items: { oneOf: [ { type: collectionIDFieldTypes[field.relationTo] }, { $ref: `#/definitions/${field.relationTo}` } ] } }; } else { fieldSchema = { oneOf: [ { type: withNullableJSONSchemaType(collectionIDFieldTypes[field.relationTo], isRequired) }, { $ref: `#/definitions/${field.relationTo}` } ] }; } break; } case 'upload': { fieldSchema = { oneOf: [ { type: collectionIDFieldTypes[field.relationTo] }, { $ref: `#/definitions/${field.relationTo}` } ] }; if (!isRequired) fieldSchema.oneOf.push({ type: 'null' }); break; } case 'blocks': { fieldSchema = { type: withNullableJSONSchemaType('array', isRequired), items: { oneOf: field.blocks.map((block)=>{ const blockFieldSchemas = fieldsToJSONSchema(collectionIDFieldTypes, block.fields, interfaceNameDefinitions, config); const blockSchema = { type: 'object', additionalProperties: false, properties: { ...blockFieldSchemas.properties, blockType: { const: block.slug } }, required: [ 'blockType', ...blockFieldSchemas.required ] }; if (block.interfaceName) { interfaceNameDefinitions.set(block.interfaceName, blockSchema); return { $ref: `#/definitions/${block.interfaceName}` }; } return blockSchema; }) } }; break; } case 'array': { fieldSchema = { type: withNullableJSONSchemaType('array', isRequired), items: { type: 'object', additionalProperties: false, ...fieldsToJSONSchema(collectionIDFieldTypes, field.fields, interfaceNameDefinitions, config) } }; if (field.interfaceName) { interfaceNameDefinitions.set(field.interfaceName, fieldSchema); fieldSchema = { $ref: `#/definitions/${field.interfaceName}` }; } break; } case 'row': case 'collapsible': { const childSchema = fieldsToJSONSchema(collectionIDFieldTypes, field.fields, interfaceNameDefinitions, config); Object.entries(childSchema.properties).forEach(([propName, propSchema])=>{ fieldSchemas.set(propName, propSchema); }); childSchema.required.forEach((propName)=>{ requiredFieldNames.add(propName); }); break; } case 'tabs': { field.tabs.forEach((tab)=>{ const childSchema = fieldsToJSONSchema(collectionIDFieldTypes, tab.fields, interfaceNameDefinitions, config); if (tabHasName(tab)) { // could have interface fieldSchemas.set(tab.name, { type: 'object', additionalProperties: false, ...childSchema }); requiredFieldNames.add(tab.name); } else { Object.entries(childSchema.properties).forEach(([propName, propSchema])=>{ fieldSchemas.set(propName, propSchema); }); childSchema.required.forEach((propName)=>{ requiredFieldNames.add(propName); }); } }); break; } case 'group': { fieldSchema = { type: 'object', additionalProperties: false, ...fieldsToJSONSchema(collectionIDFieldTypes, field.fields, interfaceNameDefinitions, config) }; if (field.interfaceName) { interfaceNameDefinitions.set(field.interfaceName, fieldSchema); fieldSchema = { $ref: `#/definitions/${field.interfaceName}` }; } break; } default: { break; } } if (fieldSchema && fieldAffectsData(field)) { fieldSchemas.set(field.name, fieldSchema); } return fieldSchemas; }, new Map())), required: Array.from(requiredFieldNames) }; } // This function is part of the public API and is exported through payload/utilities export function entityToJSONSchema(config, incomingEntity, interfaceNameDefinitions, defaultIDType) { const entity = deepCopyObject(incomingEntity); const title = entity.typescript?.interface ? entity.typescript.interface : singular(toWords(entity.slug, true)); const idField = { name: 'id', type: defaultIDType, required: true }; const customIdField = entity.fields.find((field)=>fieldAffectsData(field) && field.name === 'id'); if (customIdField && customIdField.type !== 'group' && customIdField.type !== 'tab') { customIdField.required = true; } else { entity.fields.unshift(idField); } // mark timestamp fields required if ('timestamps' in entity && entity.timestamps !== false) { entity.fields = entity.fields.map((field)=>{ if (fieldAffectsData(field) && (field.name === 'createdAt' || field.name === 'updatedAt')) { return { ...field, required: true }; } return field; }); } if ('auth' in entity && entity.auth && !entity.auth?.disableLocalStrategy) { entity.fields.push({ name: 'password', type: 'text' }); } // Used for relationship fields, to determine whether to use a string or number type for the ID. const collectionIDFieldTypes = getCollectionIDFieldTypes({ config, defaultIDType }); return { type: 'object', additionalProperties: false, title, ...fieldsToJSONSchema(collectionIDFieldTypes, entity.fields, interfaceNameDefinitions, config) }; } /** * This is used for generating the TypeScript types (payload-types.ts) with the payload generate:types command. */ export function configToJSONSchema(config, defaultIDType) { // a mutable Map to store custom top-level `interfaceName` types. Fields with an `interfaceName` property will be moved to the top-level definitions here const interfaceNameDefinitions = new Map(); // Collections and Globals have to be moved to the top-level definitions as well. Reason: The top-level type will be the `Config` type - we don't want all collection and global // types to be inlined inside the `Config` type const entityDefinitions = [ ...config.globals, ...config.collections ].reduce((acc, entity)=>{ acc[entity.slug] = entityToJSONSchema(config, entity, interfaceNameDefinitions, defaultIDType); return acc; }, {}); return { additionalProperties: false, definitions: { ...entityDefinitions, ...Object.fromEntries(interfaceNameDefinitions) }, // These properties here will be very simple, as all the complexity is in the definitions. These are just the properties for the top-level `Config` type type: 'object', properties: { collections: generateEntitySchemas(config.collections || []), globals: generateEntitySchemas(config.globals || []), locale: generateLocaleEntitySchemas(config.localization), user: generateAuthEntitySchemas(config.collections) }, required: [ 'user', 'locale', 'collections', 'globals' ], title: 'Config' }; } //# sourceMappingURL=configToJSONSchema.js.map