From 3b9fdf3ffdc49ff9a2c0e56f5438810c5f397f86 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 15 Jul 2022 21:06:12 -0700 Subject: [PATCH] feat: ensures nested fields not affecting data can be traversed properly --- .../elements/ColumnSelector/index.tsx | 4 +- .../elements/WhereBuilder/index.tsx | 2 +- .../views/collections/List/buildColumns.tsx | 104 +++----- .../collections/List/getInitialColumns.ts | 55 +++-- src/admin/utilities/flattenTopLevelFields.ts | 35 +++ src/collections/graphql/init.ts | 5 +- src/graphql/schema/buildMutationInputType.ts | 230 ++++++++---------- src/graphql/schema/buildObjectType.ts | 225 +++++++++-------- src/graphql/schema/withNullableType.ts | 1 - src/utilities/flattenTopLevelFields.ts | 15 -- 10 files changed, 328 insertions(+), 348 deletions(-) create mode 100644 src/admin/utilities/flattenTopLevelFields.ts delete mode 100644 src/utilities/flattenTopLevelFields.ts diff --git a/src/admin/components/elements/ColumnSelector/index.tsx b/src/admin/components/elements/ColumnSelector/index.tsx index 54b6c6c747..29be114edb 100644 --- a/src/admin/components/elements/ColumnSelector/index.tsx +++ b/src/admin/components/elements/ColumnSelector/index.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import flattenTopLevelFields from '../../../../utilities/flattenTopLevelFields'; +import flattenTopLevelFields from '../../../utilities/flattenTopLevelFields'; import Pill from '../Pill'; import Plus from '../../icons/Plus'; import X from '../../icons/X'; @@ -16,7 +16,7 @@ const ColumnSelector: React.FC = (props) => { setColumns, } = props; - const [fields] = useState(() => flattenTopLevelFields(collection.fields)); + const [fields] = useState(() => flattenTopLevelFields(collection.fields, true)); return (
diff --git a/src/admin/components/elements/WhereBuilder/index.tsx b/src/admin/components/elements/WhereBuilder/index.tsx index 584621695f..3bd384087a 100644 --- a/src/admin/components/elements/WhereBuilder/index.tsx +++ b/src/admin/components/elements/WhereBuilder/index.tsx @@ -7,7 +7,7 @@ import Button from '../Button'; import reducer from './reducer'; import Condition from './Condition'; import fieldTypes from './field-types'; -import flattenTopLevelFields from '../../../../utilities/flattenTopLevelFields'; +import flattenTopLevelFields from '../../../utilities/flattenTopLevelFields'; import { useSearchParams } from '../../utilities/SearchParams'; import validateWhereQuery from './validateWhereQuery'; import { Where } from '../../../../types'; diff --git a/src/admin/components/views/collections/List/buildColumns.tsx b/src/admin/components/views/collections/List/buildColumns.tsx index d9d0bc9e66..4168888ba0 100644 --- a/src/admin/components/views/collections/List/buildColumns.tsx +++ b/src/admin/components/views/collections/List/buildColumns.tsx @@ -3,75 +3,51 @@ import Cell from './Cell'; import SortColumn from '../../../elements/SortColumn'; import { SanitizedCollectionConfig } from '../../../../../collections/config/types'; import { Column } from '../../../elements/Table/types'; -import { fieldHasSubFields, Field, fieldAffectsData, fieldIsPresentationalOnly } from '../../../../../fields/config/types'; +import { fieldIsPresentationalOnly } from '../../../../../fields/config/types'; +import flattenFields from '../../../../utilities/flattenTopLevelFields'; -const buildColumns = (collection: SanitizedCollectionConfig, columns: string[]): Column[] => (columns || []).reduce((cols, col, colIndex) => { - let field = null; +const buildColumns = (collection: SanitizedCollectionConfig, columns: string[]): Column[] => { + const flattenedFields = flattenFields(collection.fields, true); - const fields = [ - ...collection.fields, - { - name: 'id', - type: 'text', - label: 'ID', - } as Field, - { - name: 'updatedAt', - type: 'date', - label: 'Updated At', - } as Field, - { - name: 'createdAt', - type: 'date', - label: 'Created At', - } as Field, - ]; + return (columns || []).reduce((cols, col, colIndex) => { + let field = null; - fields.forEach((fieldToCheck) => { - if ((fieldAffectsData(fieldToCheck) || fieldIsPresentationalOnly(fieldToCheck)) && fieldToCheck.name === col) { - field = fieldToCheck; - } + flattenedFields.forEach((fieldToCheck) => { + if (fieldToCheck.name === col) { + field = fieldToCheck; + } + }); - if (!fieldAffectsData(fieldToCheck) && fieldHasSubFields(fieldToCheck)) { - fieldToCheck.fields.forEach((subField) => { - if (fieldAffectsData(subField) && subField.name === col) { - field = subField; - } - }); - } - - return false; - }); - - if (field) { - return [ - ...cols, - { - accessor: field.name, - components: { - Heading: ( - - ), - renderCell: (rowData, cellData) => ( - - ), + if (field) { + return [ + ...cols, + { + accessor: field.name, + components: { + Heading: ( + + ), + renderCell: (rowData, cellData) => ( + + ), + }, }, - }, - ]; - } + ]; + } - return cols; -}, []); + return cols; + }, []); +}; export default buildColumns; diff --git a/src/admin/components/views/collections/List/getInitialColumns.ts b/src/admin/components/views/collections/List/getInitialColumns.ts index 3494caaf6d..27c3cce361 100644 --- a/src/admin/components/views/collections/List/getInitialColumns.ts +++ b/src/admin/components/views/collections/List/getInitialColumns.ts @@ -1,5 +1,33 @@ import { Field, fieldHasSubFields, fieldAffectsData } from '../../../../../fields/config/types'; +const getRemainingColumns = (fields: Field[], useAsTitle: string): string[] => fields.reduce((remaining, field) => { + if (fieldAffectsData(field) && field.name === useAsTitle) { + return remaining; + } + + if (!fieldAffectsData(field) && fieldHasSubFields(field)) { + return [ + ...remaining, + ...getRemainingColumns(field.fields, useAsTitle), + ]; + } + + if (field.type === 'tabs') { + return [ + ...remaining, + ...field.tabs.reduce((tabFieldColumns, tab) => [ + ...tabFieldColumns, + ...getRemainingColumns(tab.fields, useAsTitle), + ], []), + ]; + } + + return [ + ...remaining, + field.name, + ]; +}, []); + const getInitialColumnState = (fields: Field[], useAsTitle: string, defaultColumns: string[]): string[] => { let initialColumns = []; @@ -12,32 +40,7 @@ const getInitialColumnState = (fields: Field[], useAsTitle: string, defaultColum initialColumns.push(useAsTitle); } - const remainingColumns = fields.reduce((remaining, field) => { - if (fieldAffectsData(field) && field.name === useAsTitle) { - return remaining; - } - - if (!fieldAffectsData(field) && fieldHasSubFields(field)) { - return [ - ...remaining, - ...field.fields.reduce((subFields, subField) => { - if (fieldAffectsData(subField)) { - return [ - ...subFields, - subField.name, - ]; - } - - return subFields; - }, []), - ]; - } - - return [ - ...remaining, - field.name, - ]; - }, []); + const remainingColumns = getRemainingColumns(fields, useAsTitle); initialColumns = initialColumns.concat(remainingColumns); initialColumns = initialColumns.slice(0, 4); diff --git a/src/admin/utilities/flattenTopLevelFields.ts b/src/admin/utilities/flattenTopLevelFields.ts new file mode 100644 index 0000000000..5a2a13fd89 --- /dev/null +++ b/src/admin/utilities/flattenTopLevelFields.ts @@ -0,0 +1,35 @@ +import { Field, FieldAffectingData, fieldAffectsData, fieldHasSubFields, fieldIsPresentationalOnly, FieldPresentationalOnly } from '../../fields/config/types'; + +const flattenFields = (fields: Field[], keepPresentationalFields?: boolean): (FieldAffectingData | FieldPresentationalOnly)[] => { + return fields.reduce((fieldsToUse, field) => { + if (fieldAffectsData(field) || (keepPresentationalFields && fieldIsPresentationalOnly(field))) { + return [ + ...fieldsToUse, + field, + ]; + } + + if (fieldHasSubFields(field)) { + return [ + ...fieldsToUse, + ...flattenFields(field.fields, keepPresentationalFields), + ]; + } + + if (field.type === 'tabs') { + return [ + ...fieldsToUse, + ...field.tabs.reduce((tabFields, tab) => { + return [ + ...tabFields, + ...flattenFields(tab.fields, keepPresentationalFields), + ]; + }, []), + ]; + } + + return fieldsToUse; + }, []); +}; + +export default flattenFields; diff --git a/src/collections/graphql/init.ts b/src/collections/graphql/init.ts index e0580ff2cc..5f2ebd3688 100644 --- a/src/collections/graphql/init.ts +++ b/src/collections/graphql/init.ts @@ -10,7 +10,6 @@ import { import formatName from '../../graphql/utilities/formatName'; import buildPaginatedListType from '../../graphql/schema/buildPaginatedListType'; -import { BaseFields } from './types'; import buildMutationInputType, { getCollectionIDType } from '../../graphql/schema/buildMutationInputType'; import { buildVersionCollectionFields } from '../../versions/buildCollectionFields'; import createResolver from './resolvers/create'; @@ -31,7 +30,7 @@ import unlock from '../../auth/graphql/resolvers/unlock'; import refresh from '../../auth/graphql/resolvers/refresh'; import { Payload } from '../..'; import { Field, fieldAffectsData } from '../../fields/config/types'; -import buildObjectType from '../../graphql/schema/buildObjectType'; +import buildObjectType, { ObjectTypeConfig } from '../../graphql/schema/buildObjectType'; import buildWhereInputType from '../../graphql/schema/buildWhereInputType'; import getDeleteResolver from './resolvers/delete'; @@ -66,7 +65,7 @@ function initCollectionsGraphQL(payload: Payload): void { const idField = fields.find((field) => fieldAffectsData(field) && field.name === 'id'); const idType = getCollectionIDType(collection.config); - const baseFields: BaseFields = {}; + const baseFields: ObjectTypeConfig = {}; const whereInputFields = [ ...fields, diff --git a/src/graphql/schema/buildMutationInputType.ts b/src/graphql/schema/buildMutationInputType.ts index 1ff63ab613..11a18dd170 100644 --- a/src/graphql/schema/buildMutationInputType.ts +++ b/src/graphql/schema/buildMutationInputType.ts @@ -3,6 +3,7 @@ import { GraphQLBoolean, GraphQLEnumType, GraphQLFloat, + GraphQLInputFieldConfig, GraphQLInputObjectType, GraphQLInt, GraphQLList, @@ -15,7 +16,7 @@ import { GraphQLJSON } from 'graphql-type-json'; import withNullableType from './withNullableType'; import formatName from '../utilities/formatName'; import combineParentName from '../utilities/combineParentName'; -import { ArrayField, CodeField, DateField, EmailField, Field, fieldHasSubFields, fieldAffectsData, fieldIsPresentationalOnly, GroupField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TextareaField, TextField, UploadField, CollapsibleField, TabsField } from '../../fields/config/types'; +import { ArrayField, CodeField, DateField, EmailField, Field, fieldAffectsData, fieldIsPresentationalOnly, GroupField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TextareaField, TextField, UploadField, CollapsibleField, TabsField, CheckboxField, BlockField } from '../../fields/config/types'; import { toWords } from '../../utilities/formatLabels'; import { Payload } from '../../index'; import { SanitizedCollectionConfig } from '../../collections/config/types'; @@ -31,23 +32,60 @@ export const getCollectionIDType = (config: SanitizedCollectionConfig): GraphQLS } }; +export type InputObjectTypeConfig = { + [path: string]: GraphQLInputFieldConfig +} + function buildMutationInputType(payload: Payload, name: string, fields: Field[], parentName: string, forceNullable = false): GraphQLInputObjectType { const fieldToSchemaMap = { - number: (field: NumberField) => { + number: (inputObjectTypeConfig: InputObjectTypeConfig, field: NumberField) => { const type = field.name === 'id' ? GraphQLInt : GraphQLFloat; - return { type: withNullableType(field, type, forceNullable) }; + return { + ...inputObjectTypeConfig, + [field.name]: { type: withNullableType(field, type, forceNullable) }, + }; }, - text: (field: TextField) => ({ type: withNullableType(field, GraphQLString, forceNullable) }), - email: (field: EmailField) => ({ type: withNullableType(field, GraphQLString, forceNullable) }), - textarea: (field: TextareaField) => ({ type: withNullableType(field, GraphQLString, forceNullable) }), - richText: (field: RichTextField) => ({ type: withNullableType(field, GraphQLJSON, forceNullable) }), - code: (field: CodeField) => ({ type: withNullableType(field, GraphQLString, forceNullable) }), - date: (field: DateField) => ({ type: withNullableType(field, GraphQLString, forceNullable) }), - upload: (field: UploadField) => ({ type: withNullableType(field, GraphQLString, forceNullable) }), - radio: (field: RadioField) => ({ type: withNullableType(field, GraphQLString, forceNullable) }), - point: (field: PointField) => ({ type: withNullableType(field, GraphQLList(GraphQLFloat), forceNullable) }), - checkbox: () => ({ type: GraphQLBoolean }), - select: (field: SelectField) => { + text: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextField) => ({ + ...inputObjectTypeConfig, + [field.name]: { type: withNullableType(field, GraphQLString, forceNullable) }, + }), + email: (inputObjectTypeConfig: InputObjectTypeConfig, field: EmailField) => ({ + ...inputObjectTypeConfig, + [field.name]: { type: withNullableType(field, GraphQLString, forceNullable) }, + }), + textarea: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextareaField) => ({ + ...inputObjectTypeConfig, + [field.name]: { type: withNullableType(field, GraphQLString, forceNullable) }, + }), + richText: (inputObjectTypeConfig: InputObjectTypeConfig, field: RichTextField) => ({ + ...inputObjectTypeConfig, + [field.name]: { type: withNullableType(field, GraphQLJSON, forceNullable) }, + }), + code: (inputObjectTypeConfig: InputObjectTypeConfig, field: CodeField) => ({ + ...inputObjectTypeConfig, + [field.name]: { type: withNullableType(field, GraphQLString, forceNullable) }, + }), + date: (inputObjectTypeConfig: InputObjectTypeConfig, field: DateField) => ({ + ...inputObjectTypeConfig, + [field.name]: { type: withNullableType(field, GraphQLString, forceNullable) }, + }), + upload: (inputObjectTypeConfig: InputObjectTypeConfig, field: UploadField) => ({ + ...inputObjectTypeConfig, + [field.name]: { type: withNullableType(field, GraphQLString, forceNullable) }, + }), + radio: (inputObjectTypeConfig: InputObjectTypeConfig, field: RadioField) => ({ + ...inputObjectTypeConfig, + [field.name]: { type: withNullableType(field, GraphQLString, forceNullable) }, + }), + point: (inputObjectTypeConfig: InputObjectTypeConfig, field: PointField) => ({ + ...inputObjectTypeConfig, + [field.name]: { type: withNullableType(field, GraphQLList(GraphQLFloat), forceNullable) }, + }), + checkbox: (inputObjectTypeConfig: InputObjectTypeConfig, field: CheckboxField) => ({ + ...inputObjectTypeConfig, + [field.name]: { type: GraphQLBoolean }, + }), + select: (inputObjectTypeConfig: InputObjectTypeConfig, field: SelectField) => { const formattedName = `${combineParentName(parentName, field.name)}_MutationInput`; let type: GraphQLType = new GraphQLEnumType({ name: formattedName, @@ -77,9 +115,12 @@ function buildMutationInputType(payload: Payload, name: string, fields: Field[], type = field.hasMany ? new GraphQLList(type) : type; type = withNullableType(field, type, forceNullable); - return { type }; + return { + ...inputObjectTypeConfig, + [field.name]: { type }, + }; }, - relationship: (field: RelationshipField) => { + relationship: (inputObjectTypeConfig: InputObjectTypeConfig, field: RelationshipField) => { const { relationTo } = field; type PayloadGraphQLRelationshipType = GraphQLScalarType | GraphQLList | GraphQLInputObjectType; let type: PayloadGraphQLRelationshipType; @@ -107,138 +148,65 @@ function buildMutationInputType(payload: Payload, name: string, fields: Field[], type = getCollectionIDType(payload.collections[relationTo].config); } - return { type: field.hasMany ? new GraphQLList(type) : type }; + return { + ...inputObjectTypeConfig, + [field.name]: { type: field.hasMany ? new GraphQLList(type) : type }, + }; }, - array: (field: ArrayField) => { + array: (inputObjectTypeConfig: InputObjectTypeConfig, field: ArrayField) => { const fullName = combineParentName(parentName, field.label === false ? toWords(field.name, true) : field.label); let type: GraphQLType | GraphQLList = buildMutationInputType(payload, fullName, field.fields, fullName); type = new GraphQLList(withNullableType(field, type, forceNullable)); - return { type }; + return { + ...inputObjectTypeConfig, + [field.name]: { type }, + }; }, - group: (field: GroupField) => { + group: (inputObjectTypeConfig: InputObjectTypeConfig, field: GroupField) => { const requiresAtLeastOneField = field.fields.some((subField) => (!fieldIsPresentationalOnly(subField) && subField.required && !subField.localized)); const fullName = combineParentName(parentName, field.label === false ? toWords(field.name, true) : field.label); let type: GraphQLType = buildMutationInputType(payload, fullName, field.fields, fullName); if (requiresAtLeastOneField) type = new GraphQLNonNull(type); - return { type }; + return { + ...inputObjectTypeConfig, + [field.name]: { type }, + }; }, - blocks: () => ({ type: GraphQLJSON }), - row: (field: RowField) => field.fields.reduce((acc, rowField: RowField) => { - const getFieldSchema = fieldToSchemaMap[rowField.type]; - - if (getFieldSchema) { - const fieldSchema = getFieldSchema(rowField); - - return [ - ...acc, - fieldSchema, - ]; - } - - return acc; - }, []), - collapsible: (field: CollapsibleField) => field.fields.reduce((acc, collapsibleField: CollapsibleField) => { - const getFieldSchema = fieldToSchemaMap[collapsibleField.type]; - - if (getFieldSchema) { - const fieldSchema = getFieldSchema(collapsibleField); - - return [ - ...acc, - fieldSchema, - ]; - } - - return acc; - }, []), - tabs: (field: TabsField) => field.tabs.reduce((acc, tab) => { - const test = [ + blocks: (inputObjectTypeConfig: InputObjectTypeConfig, field: BlockField) => ({ + ...inputObjectTypeConfig, + [field.name]: { type: GraphQLJSON }, + }), + row: (inputObjectTypeConfig: InputObjectTypeConfig, field: RowField) => field.fields.reduce((acc, subField: Field) => { + const addSubField = fieldToSchemaMap[subField.type]; + return addSubField(acc, subField); + }, inputObjectTypeConfig), + collapsible: (inputObjectTypeConfig: InputObjectTypeConfig, field: CollapsibleField) => field.fields.reduce((acc, subField: CollapsibleField) => { + const addSubField = fieldToSchemaMap[subField.type]; + return addSubField(acc, subField); + }, inputObjectTypeConfig), + tabs: (inputObjectTypeConfig: InputObjectTypeConfig, field: TabsField) => field.tabs.reduce((acc, tab) => { + return { ...acc, - ...tab.fields.reduce((subAcc, rowField: TabsField) => { - const getFieldSchema = fieldToSchemaMap[rowField.type]; - - if (getFieldSchema) { - const fieldSchema = getFieldSchema(rowField); - - return [ - ...subAcc, - fieldSchema, - ]; - } - - return subAcc; - }, []), - ]; - - return test; - }, []), + ...tab.fields.reduce((subFieldSchema, subField) => { + const addSubField = fieldToSchemaMap[subField.type]; + return addSubField(subFieldSchema, subField); + }, acc), + }; + }, inputObjectTypeConfig), }; - const fieldTypes = fields.reduce((schema, field: Field) => { - if (!fieldIsPresentationalOnly(field) && !field.hidden) { - const getFieldSchema: (field: Field) => { type: GraphQLType } = fieldToSchemaMap[field.type]; - - if (getFieldSchema) { - const fieldSchema = getFieldSchema(field); - - if (Array.isArray(fieldSchema)) { - let subFields: Field[] = []; - - if (fieldHasSubFields(field)) { - subFields = field.fields; - } - - if (field.type === 'tabs') { - subFields = field.tabs.reduce((flattenedFields, tab) => { - return [ - ...flattenedFields, - ...tab.fields, - ]; - }, []); - } - - if (subFields.length > 0) { - return fieldSchema.reduce((acc, subField, i) => { - const currentSubField = subFields[i]; - if (fieldAffectsData(currentSubField)) { - return { - ...acc, - [currentSubField.name]: subField, - }; - } - - return { - ...acc, - ...fieldSchema, - }; - }, schema); - } - } - - if (fieldAffectsData(field)) { - return { - ...schema, - [field.name]: fieldSchema, - }; - } - - return { - ...schema, - ...fieldSchema, - }; - } - } - - return schema; - }, {}); - const fieldName = formatName(name); return new GraphQLInputObjectType({ name: `mutation${fieldName}Input`, - fields: { - ...fieldTypes, - }, + fields: fields.reduce((inputObjectTypeConfig, field) => { + const fieldSchema = fieldToSchemaMap[field.type]; + + return { + ...inputObjectTypeConfig, + ...fieldSchema(inputObjectTypeConfig, field), + }; + }, {}), }); } diff --git a/src/graphql/schema/buildObjectType.ts b/src/graphql/schema/buildObjectType.ts index ba12b8e9d2..cc549703fe 100644 --- a/src/graphql/schema/buildObjectType.ts +++ b/src/graphql/schema/buildObjectType.ts @@ -5,6 +5,7 @@ import { GraphQLJSON } from 'graphql-type-json'; import { GraphQLBoolean, GraphQLEnumType, + GraphQLFieldConfig, GraphQLFloat, GraphQLInt, GraphQLList, @@ -14,11 +15,10 @@ import { GraphQLUnionType, } from 'graphql'; import { DateTimeResolver, EmailAddressResolver } from 'graphql-scalars'; -import { Field, RadioField, RelationshipField, SelectField, UploadField, ArrayField, GroupField, RichTextField, fieldAffectsData, NumberField, TextField, EmailField, TextareaField, CodeField, DateField, PointField, CheckboxField, BlockField, RowField, fieldIsPresentationalOnly } from '../../fields/config/types'; +import { Field, RadioField, RelationshipField, SelectField, UploadField, ArrayField, GroupField, RichTextField, fieldAffectsData, NumberField, TextField, EmailField, TextareaField, CodeField, DateField, PointField, CheckboxField, BlockField, RowField, fieldIsPresentationalOnly, CollapsibleField, TabsField } from '../../fields/config/types'; import formatName from '../utilities/formatName'; import combineParentName from '../utilities/combineParentName'; import withNullableType from './withNullableType'; -import { BaseFields } from '../../collections/graphql/types'; import { toWords } from '../../utilities/formatLabels'; import createRichTextRelationshipPromise from '../../fields/richText/relationshipPromise'; import formatOptions from '../utilities/formatOptions'; @@ -39,37 +39,65 @@ type LocaleInputType = { } } -function buildObjectType(payload: Payload, name: string, fields: Field[], parentName: string, baseFields: BaseFields = {}): GraphQLObjectType { - const fieldToSchemaMap = { - number: (field: NumberField) => ({ type: withNullableType(field, GraphQLFloat) }), - text: (field: TextField) => ({ type: withNullableType(field, GraphQLString) }), - email: (field: EmailField) => ({ type: withNullableType(field, EmailAddressResolver) }), - textarea: (field: TextareaField) => ({ type: withNullableType(field, GraphQLString) }), - code: (field: CodeField) => ({ type: withNullableType(field, GraphQLString) }), - date: (field: DateField) => ({ type: withNullableType(field, DateTimeResolver) }), - point: (field: PointField) => ({ type: withNullableType(field, new GraphQLList(GraphQLFloat)) }), - richText: (field: RichTextField) => ({ - type: withNullableType(field, GraphQLJSON), - async resolve(parent, args, context) { - if (args.depth > 0) { - await createRichTextRelationshipPromise({ - req: context.req, - siblingDoc: parent, - depth: args.depth, - field, - showHiddenFields: false, - }); - } +export type ObjectTypeConfig = { + [path: string]: GraphQLFieldConfig +} - return parent[field.name]; - }, - args: { - depth: { - type: GraphQLInt, +function buildObjectType(payload: Payload, name: string, fields: Field[], parentName: string, baseFields: ObjectTypeConfig = {}): GraphQLObjectType { + const fieldToSchemaMap = { + number: (objectTypeConfig: ObjectTypeConfig, field: NumberField) => ({ + ...objectTypeConfig, + [field.name]: { type: withNullableType(field, GraphQLFloat) }, + }), + text: (objectTypeConfig: ObjectTypeConfig, field: TextField) => ({ + ...objectTypeConfig, + [field.name]: { type: withNullableType(field, GraphQLString) }, + }), + email: (objectTypeConfig: ObjectTypeConfig, field: EmailField) => ({ + ...objectTypeConfig, + [field.name]: { type: withNullableType(field, EmailAddressResolver) }, + }), + textarea: (objectTypeConfig: ObjectTypeConfig, field: TextareaField) => ({ + ...objectTypeConfig, + [field.name]: { type: withNullableType(field, GraphQLString) }, + }), + code: (objectTypeConfig: ObjectTypeConfig, field: CodeField) => ({ + ...objectTypeConfig, + [field.name]: { type: withNullableType(field, GraphQLString) }, + }), + date: (objectTypeConfig: ObjectTypeConfig, field: DateField) => ({ + ...objectTypeConfig, + [field.name]: { type: withNullableType(field, DateTimeResolver) }, + }), + point: (objectTypeConfig: ObjectTypeConfig, field: PointField) => ({ + ...objectTypeConfig, + [field.name]: { type: withNullableType(field, new GraphQLList(GraphQLFloat)) }, + }), + richText: (objectTypeConfig: ObjectTypeConfig, field: RichTextField) => ({ + ...objectTypeConfig, + [field.name]: { + type: withNullableType(field, GraphQLJSON), + async resolve(parent, args, context) { + if (args.depth > 0) { + await createRichTextRelationshipPromise({ + req: context.req, + siblingDoc: parent, + depth: args.depth, + field, + showHiddenFields: false, + }); + } + + return parent[field.name]; + }, + args: { + depth: { + type: GraphQLInt, + }, }, }, }), - upload: (field: UploadField) => { + upload: (objectTypeConfig: ObjectTypeConfig, field: UploadField) => { const { relationTo, label } = field; const uploadName = combineParentName(parentName, label === false ? toWords(field.name, true) : label); @@ -149,19 +177,28 @@ function buildObjectType(payload: Payload, name: string, fields: Field[], parent ), }; - return upload; + return { + ...objectTypeConfig, + [field.name]: upload, + }; }, - radio: (field: RadioField) => ({ - type: withNullableType( - field, - new GraphQLEnumType({ - name: combineParentName(parentName, field.name), - values: formatOptions(field), - }), - ), + radio: (objectTypeConfig: ObjectTypeConfig, field: RadioField) => ({ + ...objectTypeConfig, + [field.name]: { + type: withNullableType( + field, + new GraphQLEnumType({ + name: combineParentName(parentName, field.name), + values: formatOptions(field), + }), + ), + }, }), - checkbox: (field: CheckboxField) => ({ type: withNullableType(field, GraphQLBoolean) }), - select: (field: SelectField) => { + checkbox: (objectTypeConfig: ObjectTypeConfig, field: CheckboxField) => ({ + ...objectTypeConfig, + [field.name]: { type: withNullableType(field, GraphQLBoolean) }, + }), + select: (objectTypeConfig: ObjectTypeConfig, field: SelectField) => { const fullName = combineParentName(parentName, field.name); let type: GraphQLType = new GraphQLEnumType({ @@ -172,9 +209,12 @@ function buildObjectType(payload: Payload, name: string, fields: Field[], parent type = field.hasMany ? new GraphQLList(type) : type; type = withNullableType(field, type); - return { type }; + return { + ...objectTypeConfig, + [field.name]: { type }, + }; }, - relationship: (field: RelationshipField) => { + relationship: (objectTypeConfig: ObjectTypeConfig, field: RelationshipField) => { const { relationTo, label } = field; const isRelatedToManyCollections = Array.isArray(relationTo); const hasManyValues = field.hasMany; @@ -388,22 +428,31 @@ function buildObjectType(payload: Payload, name: string, fields: Field[], parent }; } - return relationship; + return { + ...objectTypeConfig, + [field.name]: relationship, + }; }, - array: (field: ArrayField) => { + array: (objectTypeConfig: ObjectTypeConfig, field: ArrayField) => { const fullName = combineParentName(parentName, field.label === false ? toWords(field.name, true) : field.label); const type = buildObjectType(payload, fullName, field.fields, fullName); const arrayType = new GraphQLList(withNullableType(field, type)); - return { type: arrayType }; + return { + ...objectTypeConfig, + [field.name]: { type: arrayType }, + }; }, - group: (field: GroupField) => { + group: (objectTypeConfig: ObjectTypeConfig, field: GroupField) => { const fullName = combineParentName(parentName, field.label === false ? toWords(field.name, true) : field.label); const type = buildObjectType(payload, fullName, field.fields, fullName); - return { type }; + return { + ...objectTypeConfig, + [field.name]: { type }, + }; }, - blocks: (field: BlockField) => { + blocks: (objectTypeConfig: ObjectTypeConfig, field: BlockField) => { const blockTypes = field.blocks.map((block) => { buildBlockType(payload, block); return payload.types.blockTypes[block.slug]; @@ -417,72 +466,38 @@ function buildObjectType(payload: Payload, name: string, fields: Field[], parent resolveType: (data) => payload.types.blockTypes[data.blockType].name, })); - return { type }; + return { + ...objectTypeConfig, + [field.name]: { type }, + }; }, - row: (field) => field.fields.reduce((subFieldSchema, subField) => { - const buildSchemaType = fieldToSchemaMap[subField.type]; - - if (!fieldIsPresentationalOnly(subField) && buildSchemaType) { - return { - ...subFieldSchema, - [formatName(subField.name)]: buildSchemaType(subField), - }; - } - - return subFieldSchema; - }, {}), - collapsible: (field) => field.fields.reduce((subFieldSchema, subField) => { - const buildSchemaType = fieldToSchemaMap[subField.type]; - - if (!fieldIsPresentationalOnly(subField) && buildSchemaType) { - return { - ...subFieldSchema, - [formatName(subField.name)]: buildSchemaType(subField), - }; - } - - return subFieldSchema; - }, {}), - tabs: (field) => field.tabs.reduce((tabSchema, tab) => { + row: (objectTypeConfig: ObjectTypeConfig, field: RowField) => field.fields.reduce((objectTypeConfigWithRowFields, subField) => { + const addSubField = fieldToSchemaMap[subField.type]; + return addSubField(objectTypeConfigWithRowFields, subField); + }, objectTypeConfig), + collapsible: (objectTypeConfig: ObjectTypeConfig, field: CollapsibleField) => field.fields.reduce((objectTypeConfigWithCollapsibleFields, subField) => { + const addSubField = fieldToSchemaMap[subField.type]; + return addSubField(objectTypeConfigWithCollapsibleFields, subField); + }, objectTypeConfig), + tabs: (objectTypeConfig: ObjectTypeConfig, field: TabsField) => field.tabs.reduce((tabSchema, tab) => { return { ...tabSchema, ...tab.fields.reduce((subFieldSchema, subField) => { - const buildSchemaType = fieldToSchemaMap[subField.type]; - - if (!fieldIsPresentationalOnly(subField) && buildSchemaType) { - return { - ...subFieldSchema, - [formatName(subField.name)]: buildSchemaType(subField), - }; - } - - return subFieldSchema; - }, {}), + const addSubField = fieldToSchemaMap[subField.type]; + return addSubField(subFieldSchema, subField); + }, tabSchema), }; - }, {}), + }, objectTypeConfig), }; const objectSchema = { name, - fields: () => fields.reduce((schema, field) => { - if (!fieldIsPresentationalOnly(field) && !field.hidden) { - const fieldSchema = fieldToSchemaMap[field.type]; - if (fieldSchema) { - if (fieldAffectsData(field)) { - return { - ...schema, - [formatName(field.name)]: fieldSchema(field), - }; - } - - return { - ...schema, - ...fieldSchema(field), - }; - } - } - - return schema; + fields: () => fields.reduce((objectTypeConfig, field) => { + const fieldSchema = fieldToSchemaMap[field.type]; + return { + ...objectTypeConfig, + ...fieldSchema(objectTypeConfig, field), + }; }, baseFields), }; diff --git a/src/graphql/schema/withNullableType.ts b/src/graphql/schema/withNullableType.ts index c4389c2b8c..4a8ea695ba 100644 --- a/src/graphql/schema/withNullableType.ts +++ b/src/graphql/schema/withNullableType.ts @@ -1,7 +1,6 @@ import { GraphQLNonNull, GraphQLType } from 'graphql'; import { NonPresentationalField } from '../../fields/config/types'; - const withNullableType = (field: NonPresentationalField, type: GraphQLType, forceNullable = false): GraphQLType => { const hasReadAccessControl = field.access && field.access.read; const condition = field.admin && field.admin.condition; diff --git a/src/utilities/flattenTopLevelFields.ts b/src/utilities/flattenTopLevelFields.ts deleted file mode 100644 index fbded5f4f0..0000000000 --- a/src/utilities/flattenTopLevelFields.ts +++ /dev/null @@ -1,15 +0,0 @@ -const flattenTopLevelFields = (fields) => fields.reduce((flattened, field) => { - if (!field.name && Array.isArray(field.fields)) { - return [ - ...flattened, - ...field.fields.filter((subField) => subField.name), - ]; - } - - return [ - ...flattened, - field, - ]; -}, []); - -export default flattenTopLevelFields;