diff --git a/src/admin/components/forms/field-types/Upload/SelectExisting/index.tsx b/src/admin/components/forms/field-types/Upload/SelectExisting/index.tsx index 473ddbfd67..061a35f515 100644 --- a/src/admin/components/forms/field-types/Upload/SelectExisting/index.tsx +++ b/src/admin/components/forms/field-types/Upload/SelectExisting/index.tsx @@ -8,6 +8,7 @@ import usePayloadAPI from '../../../../../hooks/usePayloadAPI'; import ListControls from '../../../../elements/ListControls'; import Paginator from '../../../../elements/Paginator'; import UploadGallery from '../../../../elements/UploadGallery'; +import { Field } from '../../../../../../fields/config/types'; import { Props } from './types'; import './index.scss'; @@ -45,7 +46,7 @@ const SelectExistingUploadModal: React.FC = (props) => { const [{ data }, { setParams }] = usePayloadAPI(apiURL, {}); useEffect(() => { - setFields(formatFields(collection)); + setFields(formatFields(collection) as Field[]); }, [collection]); useEffect(() => { diff --git a/src/admin/components/views/collections/Edit/formatFields.tsx b/src/admin/components/views/collections/Edit/formatFields.tsx index 9513c0bfe0..9b51e5ff09 100644 --- a/src/admin/components/views/collections/Edit/formatFields.tsx +++ b/src/admin/components/views/collections/Edit/formatFields.tsx @@ -1,8 +1,8 @@ -const formatFields = (collection, isEditing) => { - const fields = isEditing - ? collection.fields.filter(({ name }) => name !== 'id') - : collection.fields; - return fields; -}; +import { SanitizedCollectionConfig } from '../../../../../collections/config/types'; +import { Field } from '../../../../../fields/config/types'; + +const formatFields = (collection: SanitizedCollectionConfig, isEditing: boolean): Field[] => (isEditing + ? collection.fields.filter(({ name }) => name !== 'id') + : collection.fields); export default formatFields; diff --git a/src/admin/components/views/collections/List/formatFields.tsx b/src/admin/components/views/collections/List/formatFields.tsx index a16b8a2087..2785433834 100644 --- a/src/admin/components/views/collections/List/formatFields.tsx +++ b/src/admin/components/views/collections/List/formatFields.tsx @@ -1,5 +1,8 @@ -const formatFields = (config) => { - const hasIdField = config.fields.findIndex(({ name }) => name === 'id') > -1; +import { SanitizedCollectionConfig } from '../../../../../collections/config/types'; +import { Field } from '../../../../../fields/config/types'; + +const formatFields = (config: SanitizedCollectionConfig): (Field | { name: string, label: string, type: string })[] => { + const hasID = config.fields.findIndex(({ name }) => name === 'id') > -1; let fields = config.fields.reduce((formatted, field) => { if (field.hidden === true || field?.admin?.disabled === true) { return formatted; @@ -9,7 +12,7 @@ const formatFields = (config) => { ...formatted, field, ]; - }, hasIdField ? [] : [{ name: 'id', label: 'ID', type: 'text' }]); + }, hasID ? [] : [{ name: 'id', label: 'ID', type: 'text' }]); if (config.timestamps) { fields = fields.concat([ diff --git a/src/collections/buildSchema.ts b/src/collections/buildSchema.ts index 985ec2bd7e..df5d3c0541 100644 --- a/src/collections/buildSchema.ts +++ b/src/collections/buildSchema.ts @@ -12,11 +12,6 @@ const buildCollectionSchema = (collection: SanitizedCollectionConfig, config: Sa { timestamps: collection.timestamps !== false, ...schemaOptions }, ); - if (collection.idType) { - const idSchemaType = collection.idType === 'number' ? Number : String; - schema.add({ _id: idSchemaType }); - } - schema.plugin(paginate, { useEstimatedCount: true }) .plugin(buildQueryPlugin); diff --git a/src/collections/graphql/init.ts b/src/collections/graphql/init.ts index fccc240b22..a9b1610177 100644 --- a/src/collections/graphql/init.ts +++ b/src/collections/graphql/init.ts @@ -10,6 +10,7 @@ import { import formatName from '../../graphql/utilities/formatName'; import buildPaginatedListType from '../../graphql/schema/buildPaginatedListType'; import { BaseFields } from './types'; +import { getCollectionIDType } from '../../graphql/schema/buildMutationInputType'; function registerCollections(): void { const { @@ -28,13 +29,11 @@ function registerCollections(): void { singular, plural, }, - fields: initialFields, + fields, timestamps, }, } = collection; - const fields = initialFields.filter(({ name }) => name !== 'id'); - const singularLabel = formatName(singular); let pluralLabel = formatName(plural); @@ -49,23 +48,20 @@ function registerCollections(): void { collection.graphQL = {}; - const idField = initialFields.find(({ name }) => name === 'id'); - const idType = idField && idField.type === 'number' ? GraphQLInt : GraphQLString; + const idField = fields.find(({ name }) => name === 'id'); + const idType = getCollectionIDType(collection.config); - const baseFields: BaseFields = { - id: { - type: new GraphQLNonNull(idType), - }, - }; + const baseFields: BaseFields = {}; const whereInputFields = [ ...fields, ]; - if (idField) { + if (!idField) { + baseFields.id = { type: idType }; whereInputFields.push({ name: 'id', - type: idField.type, + type: 'text', }); } @@ -129,7 +125,7 @@ function registerCollections(): void { collection.graphQL.updateMutationInputType = new GraphQLNonNull(this.buildMutationInputType( `${singularLabel}Update`, - fields, + fields.filter((field) => field.name !== 'id'), `${singularLabel}Update`, true, )); diff --git a/src/config/validate.ts b/src/config/validate.ts index f53daa1a6b..f9e9bae304 100644 --- a/src/config/validate.ts +++ b/src/config/validate.ts @@ -1,9 +1,10 @@ +import { ValidationResult } from 'joi'; import schema from './schema'; import collectionSchema from '../collections/config/schema'; import Logger from '../utilities/logger'; import { SanitizedConfig } from './types'; import { SanitizedCollectionConfig } from '../collections/config/types'; -import fieldSchema from '../fields/config/schema'; +import fieldSchema, { idField } from '../fields/config/schema'; import { SanitizedGlobalConfig } from '../globals/config/types'; import globalSchema from '../globals/config/schema'; @@ -12,7 +13,17 @@ const logger = Logger(); const validateFields = (context: string, entity: SanitizedCollectionConfig | SanitizedGlobalConfig): string[] => { const errors: string[] = []; entity.fields.forEach((field) => { + let idResult: Partial = { error: null }; + if (field.name === 'id') { + idResult = idField.validate(field, { abortEarly: false }); + } + const result = fieldSchema.validate(field, { abortEarly: false }); + if (idResult.error) { + idResult.error.details.forEach(({ message }) => { + errors.push(`${context} "${entity.slug}" > Field "${field.name}" > ${message}`); + }); + } if (result.error) { result.error.details.forEach(({ message }) => { errors.push(`${context} "${entity.slug}" > Field "${field.name}" > ${message}`); diff --git a/src/fields/config/schema.ts b/src/fields/config/schema.ts index 20f90f6446..69821eb02c 100644 --- a/src/fields/config/schema.ts +++ b/src/fields/config/schema.ts @@ -47,6 +47,13 @@ export const baseField = joi.object().keys({ admin: baseAdminFields.default(), }).default(); +export const idField = baseField.keys({ + name: joi.string().valid('id'), + type: joi.string().valid('text', 'number', 'email', 'date'), + required: joi.not(false, 0).default(true), + localized: joi.invalid(true), +}); + export const text = baseField.keys({ type: joi.string().valid('text').required(), name: joi.string().required(), diff --git a/src/graphql/schema/buildMutationInputType.ts b/src/graphql/schema/buildMutationInputType.ts index a752a2ce65..3b176e19d8 100644 --- a/src/graphql/schema/buildMutationInputType.ts +++ b/src/graphql/schema/buildMutationInputType.ts @@ -12,16 +12,28 @@ import { GraphQLType, } from 'graphql'; import { GraphQLJSON } from 'graphql-type-json'; +import { GraphQLDateTime, GraphQLEmailAddress } from 'graphql-scalars'; import withNullableType from './withNullableType'; import formatName from '../utilities/formatName'; import combineParentName from '../utilities/combineParentName'; import { ArrayField, Field, FieldWithSubFields, GroupField, RelationshipField, RowField, SelectField } from '../../fields/config/types'; import { toWords } from '../../utilities/formatLabels'; import payload from '../../index'; +import { SanitizedCollectionConfig } from '../../collections/config/types'; -const getCollectionIDType = (config) => { +export const getCollectionIDType = (config: SanitizedCollectionConfig): GraphQLScalarType => { const idField = config.fields.find(({ name }) => name === 'id'); - return idField && idField.type === 'number' ? GraphQLInt : GraphQLString; + if (!idField) return GraphQLString; + switch (idField.type) { + case 'number': + return GraphQLInt; + case 'email': + return GraphQLEmailAddress; + case 'date': + return GraphQLDateTime; + default: + return GraphQLString; + } }; function buildMutationInputType(name: string, fields: Field[], parentName: string, forceNullable = false): GraphQLInputObjectType { @@ -77,7 +89,7 @@ function buildMutationInputType(name: string, fields: Field[], parentName: strin relationship: (field: RelationshipField) => { const { relationTo } = field; type PayloadGraphQLRelationshipType = GraphQLScalarType | GraphQLList | GraphQLInputObjectType; - let type: PayloadGraphQLRelationshipType = GraphQLString; + let type: PayloadGraphQLRelationshipType; if (Array.isArray(relationTo)) { const fullName = `${combineParentName(parentName, field.label === false ? toWords(field.name, true) : field.label)}RelationshipInput`;