From ad4f7a5fffc15c7fcea7177732b9101be0187170 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Sep 2022 20:07:02 -0700 Subject: [PATCH] chore: fixes and cleanup --- src/fields/config/types.ts | 7 ++-- src/fields/hooks/afterChange/promise.ts | 4 +- .../hooks/afterChange/traverseFields.ts | 4 +- src/fields/hooks/afterRead/promise.ts | 4 +- src/fields/hooks/afterRead/traverseFields.ts | 4 +- src/fields/hooks/beforeChange/promise.ts | 6 +-- .../hooks/beforeChange/traverseFields.ts | 4 +- src/fields/hooks/beforeValidate/promise.ts | 4 +- .../hooks/beforeValidate/traverseFields.ts | 4 +- src/graphql/schema/buildMutationInputType.ts | 39 +++++++++++++------ src/graphql/schema/withNullableType.ts | 4 +- src/mongoose/buildSchema.ts | 30 +++++++------- .../groupOrTabHasRequiredSubfield.ts | 15 +++++++ test/fields/collections/Tabs/index.ts | 12 ++++-- test/fields/int.spec.ts | 3 +- yarn.lock | 5 --- 16 files changed, 87 insertions(+), 62 deletions(-) create mode 100644 src/utilities/groupOrTabHasRequiredSubfield.ts diff --git a/src/fields/config/types.ts b/src/fields/config/types.ts index 3181e35d09..2ec3e8a0d8 100644 --- a/src/fields/config/types.ts +++ b/src/fields/config/types.ts @@ -201,7 +201,7 @@ export type TabsField = Omit & { admin?: TabsAdmin } -type TabAsField = Tab & { +export type TabAsField = Tab & { type: 'tab' }; @@ -370,7 +370,6 @@ export type Field = | RowField | CollapsibleField | TabsField - | TabAsField | UIField; export type FieldAffectingData = @@ -465,11 +464,11 @@ export function fieldHasMaxDepth(field: Field): field is FieldWithMaxDepth { return (field.type === 'upload' || field.type === 'relationship') && typeof field.maxDepth === 'number'; } -export function fieldIsPresentationalOnly(field: Field): field is UIField { +export function fieldIsPresentationalOnly(field: Field | TabAsField): field is UIField { return field.type === 'ui'; } -export function fieldAffectsData(field: Field): field is FieldAffectingData { +export function fieldAffectsData(field: Field | TabAsField): field is FieldAffectingData { return 'name' in field && !fieldIsPresentationalOnly(field); } diff --git a/src/fields/hooks/afterChange/promise.ts b/src/fields/hooks/afterChange/promise.ts index d0a5459fa2..615108b6a0 100644 --- a/src/fields/hooks/afterChange/promise.ts +++ b/src/fields/hooks/afterChange/promise.ts @@ -1,12 +1,12 @@ /* eslint-disable no-param-reassign */ import { PayloadRequest } from '../../../express/types'; -import { Field, fieldAffectsData, tabHasName } from '../../config/types'; +import { Field, fieldAffectsData, TabAsField, tabHasName } from '../../config/types'; import { traverseFields } from './traverseFields'; type Args = { data: Record doc: Record - field: Field + field: Field | TabAsField operation: 'create' | 'update' req: PayloadRequest siblingData: Record diff --git a/src/fields/hooks/afterChange/traverseFields.ts b/src/fields/hooks/afterChange/traverseFields.ts index ff9e56ec31..fcc93fd8cb 100644 --- a/src/fields/hooks/afterChange/traverseFields.ts +++ b/src/fields/hooks/afterChange/traverseFields.ts @@ -1,11 +1,11 @@ -import { Field } from '../../config/types'; +import { Field, TabAsField } from '../../config/types'; import { promise } from './promise'; import { PayloadRequest } from '../../../express/types'; type Args = { data: Record doc: Record - fields: Field[] + fields: (Field | TabAsField)[] operation: 'create' | 'update' req: PayloadRequest siblingData: Record diff --git a/src/fields/hooks/afterRead/promise.ts b/src/fields/hooks/afterRead/promise.ts index fbba713c69..d9d51009fa 100644 --- a/src/fields/hooks/afterRead/promise.ts +++ b/src/fields/hooks/afterRead/promise.ts @@ -1,5 +1,5 @@ /* eslint-disable no-param-reassign */ -import { Field, fieldAffectsData, tabHasName } from '../../config/types'; +import { Field, fieldAffectsData, TabAsField, tabHasName } from '../../config/types'; import { PayloadRequest } from '../../../express/types'; import { traverseFields } from './traverseFields'; import richTextRelationshipPromise from '../../richText/richTextRelationshipPromise'; @@ -9,7 +9,7 @@ type Args = { currentDepth: number depth: number doc: Record - field: Field + field: Field | TabAsField fieldPromises: Promise[] findMany: boolean flattenLocales: boolean diff --git a/src/fields/hooks/afterRead/traverseFields.ts b/src/fields/hooks/afterRead/traverseFields.ts index 687d011c8d..6e6aad4acd 100644 --- a/src/fields/hooks/afterRead/traverseFields.ts +++ b/src/fields/hooks/afterRead/traverseFields.ts @@ -1,4 +1,4 @@ -import { Field } from '../../config/types'; +import { Field, TabAsField } from '../../config/types'; import { promise } from './promise'; import { PayloadRequest } from '../../../express/types'; @@ -7,7 +7,7 @@ type Args = { depth: number doc: Record fieldPromises: Promise[] - fields: Field[] + fields: (Field | TabAsField)[] findMany: boolean flattenLocales: boolean populationPromises: Promise[] diff --git a/src/fields/hooks/beforeChange/promise.ts b/src/fields/hooks/beforeChange/promise.ts index 8f4ae44b3c..66e13f63f9 100644 --- a/src/fields/hooks/beforeChange/promise.ts +++ b/src/fields/hooks/beforeChange/promise.ts @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ import merge from 'deepmerge'; -import { Field, fieldAffectsData, tabHasName } from '../../config/types'; +import { Field, fieldAffectsData, TabAsField, tabHasName } from '../../config/types'; import { Operation } from '../../../types'; import { PayloadRequest } from '../../../express/types'; import getValueWithDefault from '../../getDefaultValue'; @@ -12,7 +12,7 @@ type Args = { doc: Record docWithLocales: Record errors: { message: string, field: string }[] - field: Field + field: Field | TabAsField id?: string | number mergeLocaleActions: (() => void)[] operation: Operation @@ -62,7 +62,7 @@ export const promise = async ({ siblingData[field.name] = siblingDoc[field.name]; } - // Otherwise compute default value + // Otherwise compute default value } else if (typeof field.defaultValue !== 'undefined') { siblingData[field.name] = await getValueWithDefault({ value: siblingData[field.name], diff --git a/src/fields/hooks/beforeChange/traverseFields.ts b/src/fields/hooks/beforeChange/traverseFields.ts index 7f8576e132..e96ea8154c 100644 --- a/src/fields/hooks/beforeChange/traverseFields.ts +++ b/src/fields/hooks/beforeChange/traverseFields.ts @@ -1,4 +1,4 @@ -import { Field } from '../../config/types'; +import { Field, TabAsField } from '../../config/types'; import { promise } from './promise'; import { Operation } from '../../../types'; import { PayloadRequest } from '../../../express/types'; @@ -8,7 +8,7 @@ type Args = { doc: Record docWithLocales: Record errors: { message: string, field: string }[] - fields: Field[] + fields: (Field | TabAsField)[] id?: string | number mergeLocaleActions: (() => void)[] operation: Operation diff --git a/src/fields/hooks/beforeValidate/promise.ts b/src/fields/hooks/beforeValidate/promise.ts index 3c154845c0..5e2cb2a6dc 100644 --- a/src/fields/hooks/beforeValidate/promise.ts +++ b/src/fields/hooks/beforeValidate/promise.ts @@ -1,12 +1,12 @@ /* eslint-disable no-param-reassign */ import { PayloadRequest } from '../../../express/types'; -import { Field, fieldAffectsData, tabHasName, valueIsValueWithRelation } from '../../config/types'; +import { Field, fieldAffectsData, TabAsField, tabHasName, valueIsValueWithRelation } from '../../config/types'; import { traverseFields } from './traverseFields'; type Args = { data: Record doc: Record - field: Field + field: Field | TabAsField id?: string | number operation: 'create' | 'update' overrideAccess: boolean diff --git a/src/fields/hooks/beforeValidate/traverseFields.ts b/src/fields/hooks/beforeValidate/traverseFields.ts index 2c0df254a2..7bd6bc17a1 100644 --- a/src/fields/hooks/beforeValidate/traverseFields.ts +++ b/src/fields/hooks/beforeValidate/traverseFields.ts @@ -1,11 +1,11 @@ import { PayloadRequest } from '../../../express/types'; -import { Field } from '../../config/types'; +import { Field, TabAsField } from '../../config/types'; import { promise } from './promise'; type Args = { data: Record doc: Record - fields: Field[] + fields: (Field | TabAsField)[] id?: string | number operation: 'create' | 'update' overrideAccess: boolean diff --git a/src/graphql/schema/buildMutationInputType.ts b/src/graphql/schema/buildMutationInputType.ts index fb2e86e2d3..b8344fa147 100644 --- a/src/graphql/schema/buildMutationInputType.ts +++ b/src/graphql/schema/buildMutationInputType.ts @@ -16,10 +16,11 @@ 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, fieldAffectsData, fieldIsPresentationalOnly, GroupField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TextareaField, TextField, UploadField, CollapsibleField, TabsField, CheckboxField, BlockField } from '../../fields/config/types'; +import { ArrayField, CodeField, DateField, EmailField, Field, fieldAffectsData, GroupField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TextareaField, TextField, UploadField, CollapsibleField, TabsField, CheckboxField, BlockField, tabHasName } from '../../fields/config/types'; import { toWords } from '../../utilities/formatLabels'; import { Payload } from '../../index'; import { SanitizedCollectionConfig } from '../../collections/config/types'; +import { groupOrTabHasRequiredSubfield } from '../../utilities/groupOrTabHasRequiredSubfield'; export const getCollectionIDType = (config: SanitizedCollectionConfig): GraphQLScalarType => { const idField = config.fields.find((field) => fieldAffectsData(field) && field.name === 'id'); @@ -163,7 +164,7 @@ function buildMutationInputType(payload: Payload, name: string, fields: Field[], }; }, group: (inputObjectTypeConfig: InputObjectTypeConfig, field: GroupField) => { - const requiresAtLeastOneField = field.fields.some((subField) => (!fieldIsPresentationalOnly(subField) && subField.required && !subField.localized)); + const requiresAtLeastOneField = groupOrTabHasRequiredSubfield(field); 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); @@ -186,16 +187,30 @@ function buildMutationInputType(payload: Payload, name: string, fields: Field[], if (addSubField) return addSubField(acc, subField); return acc; }, inputObjectTypeConfig), - tabs: (inputObjectTypeConfig: InputObjectTypeConfig, field: TabsField) => field.tabs.reduce((acc, tab) => { - return { - ...acc, - ...tab.fields.reduce((subFieldSchema, subField) => { - const addSubField = fieldToSchemaMap[subField.type]; - if (addSubField) return addSubField(subFieldSchema, subField); - return subFieldSchema; - }, acc), - }; - }, inputObjectTypeConfig), + tabs: (inputObjectTypeConfig: InputObjectTypeConfig, field: TabsField) => { + return field.tabs.reduce((acc, tab) => { + if (tabHasName(tab)) { + const fullName = combineParentName(parentName, toWords(tab.name, true)); + const requiresAtLeastOneField = groupOrTabHasRequiredSubfield(field); + let type: GraphQLType = buildMutationInputType(payload, fullName, tab.fields, fullName); + if (requiresAtLeastOneField) type = new GraphQLNonNull(type); + + return { + ...inputObjectTypeConfig, + [tab.name]: { type }, + }; + } + + return { + ...acc, + ...tab.fields.reduce((subFieldSchema, subField) => { + const addSubField = fieldToSchemaMap[subField.type]; + if (addSubField) return addSubField(subFieldSchema, subField); + return subFieldSchema; + }, acc), + }; + }, inputObjectTypeConfig); + }, }; const fieldName = formatName(name); diff --git a/src/graphql/schema/withNullableType.ts b/src/graphql/schema/withNullableType.ts index 4a8ea695ba..b1551cb324 100644 --- a/src/graphql/schema/withNullableType.ts +++ b/src/graphql/schema/withNullableType.ts @@ -1,7 +1,7 @@ import { GraphQLNonNull, GraphQLType } from 'graphql'; -import { NonPresentationalField } from '../../fields/config/types'; +import { FieldAffectingData } from '../../fields/config/types'; -const withNullableType = (field: NonPresentationalField, type: GraphQLType, forceNullable = false): GraphQLType => { +const withNullableType = (field: FieldAffectingData, type: GraphQLType, forceNullable = false): GraphQLType => { const hasReadAccessControl = field.access && field.access.read; const condition = field.admin && field.admin.condition; diff --git a/src/mongoose/buildSchema.ts b/src/mongoose/buildSchema.ts index 5fd6b92668..168d9ca5be 100644 --- a/src/mongoose/buildSchema.ts +++ b/src/mongoose/buildSchema.ts @@ -14,6 +14,7 @@ import { DateField, EmailField, Field, + FieldAffectingData, fieldAffectsData, fieldIsLocalized, fieldIsPresentationalOnly, GroupField, @@ -43,7 +44,7 @@ export type BuildSchemaOptions = { type FieldSchemaGenerator = (field: Field, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions) => void; -const formatBaseSchema = (field: NonPresentationalField, buildSchemaOptions: BuildSchemaOptions) => { +const formatBaseSchema = (field: FieldAffectingData, buildSchemaOptions: BuildSchemaOptions) => { const schema: SchemaTypeOptions = { unique: (!buildSchemaOptions.disableUnique && field.unique) || false, required: false, @@ -57,8 +58,8 @@ const formatBaseSchema = (field: NonPresentationalField, buildSchemaOptions: Bui return schema; }; -const localizeSchema = (field: NonPresentationalField | Tab, schema, localization) => { - if (fieldIsLocalized(field) && localization && Array.isArray(localization.locales)) { +const localizeSchema = (entity: NonPresentationalField | Tab, schema, localization) => { + if (fieldIsLocalized(entity) && localization && Array.isArray(localization.locales)) { return { type: localization.locales.reduce((localeSchema, locale) => ({ ...localeSchema, @@ -295,17 +296,18 @@ const fieldToSchemaMap: Record = { tabs: (field: TabsField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { field.tabs.forEach((tab) => { if (tabHasName(tab)) { - const formattedBaseSchema = formatBaseSchema(field, buildSchemaOptions); - const baseSchema = { - ...formattedBaseSchema, - type: buildSchema(config, tab.fields, { - options: { - _id: false, - id: false, + type: buildSchema( + config, + tab.fields, + { + options: { + _id: false, + id: false, + }, + disableUnique: buildSchemaOptions.disableUnique, }, - disableUnique: buildSchemaOptions.disableUnique, - }), + ), }; schema.add({ @@ -341,14 +343,10 @@ const fieldToSchemaMap: Record = { }); }, group: (field: GroupField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - let { required } = field; - if (field?.admin?.condition || field?.localized || field?.access?.create) required = false; - const formattedBaseSchema = formatBaseSchema(field, buildSchemaOptions); const baseSchema = { ...formattedBaseSchema, - required: required && field.fields.some((subField) => (!fieldIsPresentationalOnly(subField) && subField.required && !subField.localized && !subField?.admin?.condition && !subField?.access?.create)), type: buildSchema( config, field.fields, diff --git a/src/utilities/groupOrTabHasRequiredSubfield.ts b/src/utilities/groupOrTabHasRequiredSubfield.ts new file mode 100644 index 0000000000..773a63fab6 --- /dev/null +++ b/src/utilities/groupOrTabHasRequiredSubfield.ts @@ -0,0 +1,15 @@ +import { Field, fieldAffectsData, Tab } from '../fields/config/types'; + +export const groupOrTabHasRequiredSubfield = (entity: Field | Tab): boolean => { + if ('type' in entity && entity.type === 'group') { + return entity.fields.some((subField) => { + return (fieldAffectsData(subField) && subField.required) || groupOrTabHasRequiredSubfield(subField); + }); + } + + if ('fields' in entity && 'name' in entity) { + return (entity as Tab).fields.some((subField) => groupOrTabHasRequiredSubfield(subField)); + } + + return false; +}; diff --git a/test/fields/collections/Tabs/index.ts b/test/fields/collections/Tabs/index.ts index e82d8d0b38..2b36a99591 100644 --- a/test/fields/collections/Tabs/index.ts +++ b/test/fields/collections/Tabs/index.ts @@ -176,25 +176,29 @@ const TabsFields: CollectionConfig = { label: 'Hooks Tab', hooks: { beforeValidate: [ - ({ data }) => { + ({ data = {} }) => { + if (!data.hooksTab) data.hooksTab = {}; data.hooksTab.beforeValidate = true; return data.hooksTab; }, ], beforeChange: [ - ({ data }) => { + ({ data = {} }) => { + if (!data.hooksTab) data.hooksTab = {}; data.hooksTab.beforeChange = true; return data.hooksTab; }, ], afterChange: [ - ({ data }) => { + ({ data = {} }) => { + if (!data.hooksTab) data.hooksTab = {}; data.hooksTab.afterChange = true; return data.hooksTab; }, ], afterRead: [ - ({ data }) => { + ({ data = {} }) => { + if (!data.hooksTab) data.hooksTab = {}; data.hooksTab.afterRead = true; return data.hooksTab; }, diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index be3f613f47..85a8591d7b 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -5,7 +5,6 @@ import config from '../uploads/config'; import payload from '../../src'; import { pointDoc } from './collections/Point'; import type { ArrayField, GroupField, TabsField } from './payload-types'; -import type { ArrayField, GroupField } from './payload-types'; import { arrayFieldsSlug, arrayDefaultValue, arrayDoc } from './collections/Array'; import { groupFieldsSlug, groupDefaultChild, groupDefaultValue, groupDoc } from './collections/Group'; import { defaultText } from './collections/Text'; @@ -292,7 +291,7 @@ describe('Fields', () => { collection, id, locale: 'all', - }) as unknown as {localized: {en: unknown, es: unknown}}; + }) as unknown as { localized: { en: unknown, es: unknown } }; expect(enDoc.localized[0].text).toStrictEqual(enText); expect(esDoc.localized[0].text).toStrictEqual(esText); diff --git a/yarn.lock b/yarn.lock index 0c70a7a250..a2871676eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5814,11 +5814,6 @@ extract-files@^9.0.0: resolved "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a" integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ== -"falsey@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/falsey/-/falsey-1.0.0.tgz#71bdd775c24edad9f2f5c015ce8be24400bb5d7d" - integrity sha512-zMDNZ/Ipd8MY0+346CPvhzP1AsiVyNfTOayJza4reAIWf72xbkuFUDcJNxSAsQE1b9Bu0wijKb8Ngnh/a7fI5w== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"