fix: more strict field typing

This commit is contained in:
James
2021-10-12 21:18:12 -04:00
parent 7d49302ffa
commit 84f6a9d659
32 changed files with 628 additions and 486 deletions

View File

@@ -28,8 +28,8 @@ module.exports = {
'plugin:@typescript-eslint/recommended',
],
rules: {
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
'no-shadow': 'off',
'@typescript-eslint/no-shadow': ['error'],
'import/no-unresolved': [
2,
{
@@ -38,18 +38,20 @@ module.exports = {
],
},
],
}
},
},
],
rules: {
'no-sparse-arrays': 'off',
'import/no-extraneous-dependencies': ["error", { "packageDir": "./" }],
'import/no-extraneous-dependencies': ['error', { packageDir: './' }],
'react/jsx-filename-extension': [2, { extensions: ['.js', '.jsx', '.ts', '.tsx'] }],
'import/prefer-default-export': 'off',
'react/prop-types': 'off',
'react/require-default-props': 'off',
'react/no-unused-prop-types': 'off',
'no-underscore-dangle': 'off',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': ['error'],
'import/extensions': [
'error',
'ignorePackages',

View File

@@ -248,19 +248,19 @@
"@types/webpack-dev-middleware": "4.0.0",
"@types/webpack-env": "^1.15.3",
"@types/webpack-hot-middleware": "2.25.3",
"@typescript-eslint/eslint-plugin": "^4.8.1",
"@typescript-eslint/parser": "4.0.1",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"babel-eslint": "^10.0.1",
"babel-plugin-ignore-html-and-css-imports": "^0.1.0",
"copyfiles": "^2.4.0",
"cross-env": "^7.0.2",
"eslint": "^6.8.0",
"eslint-plugin-import": "^2.20.0",
"eslint-plugin-jest": "^23.16.0",
"eslint-plugin-jest-dom": "^3.0.1",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-react": "^7.18.0",
"eslint-plugin-react-hooks": "^2.3.0",
"eslint": "^8.0.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-jest": "^25.0.5",
"eslint-plugin-jest-dom": "^3.9.2",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-react-hooks": "^4.2.0",
"form-data": "^3.0.0",
"graphql-request": "^3.4.0",
"mongodb": "^3.6.2",
@@ -270,7 +270,7 @@
"passport-strategy": "^1.0.0",
"release-it": "^14.2.2",
"rimraf": "^3.0.2",
"typescript": "^4.1.2"
"typescript": "^4.4.4"
},
"files": [
"bin.js",

View File

@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import AnimateHeight from 'react-animate-height';
import { fieldIsNamed } from '../../../../fields/config/types';
import SearchFilter from '../SearchFilter';
import ColumnSelector from '../ColumnSelector';
import WhereBuilder from '../WhereBuilder';
@@ -8,9 +9,10 @@ import Button from '../Button';
import { Props } from './types';
import { useSearchParams } from '../../utilities/SearchParams';
import './index.scss';
import validateWhereQuery from '../WhereBuilder/validateWhereQuery';
import './index.scss';
const baseClass = 'list-controls';
const ListControls: React.FC<Props> = (props) => {
@@ -34,17 +36,17 @@ const ListControls: React.FC<Props> = (props) => {
const params = useSearchParams();
const shouldInitializeWhereOpened = validateWhereQuery(params?.where);
const [titleField] = useState(() => fields.find((field) => field.name === useAsTitle));
const [titleField] = useState(() => fields.find((field) => fieldIsNamed(field) && field.name === useAsTitle));
const [visibleDrawer, setVisibleDrawer] = useState<'where' | 'sort' | 'columns'>(shouldInitializeWhereOpened ? 'where' : undefined);
return (
<div className={baseClass}>
<div className={`${baseClass}__wrap`}>
<SearchFilter
fieldName={titleField?.name}
fieldName={titleField && fieldIsNamed(titleField) ? titleField.name : undefined}
handleChange={handleWhereChange}
modifySearchQuery={modifySearchQuery}
fieldLabel={titleField?.label ? titleField?.label : undefined}
fieldLabel={titleField && titleField.label ? titleField.label : undefined}
/>
<div className={`${baseClass}__buttons`}>
<div className={`${baseClass}__buttons-wrap`}>

View File

@@ -4,9 +4,10 @@ import { useHistory } from 'react-router-dom';
import { Props } from './types';
import ReactSelect from '../ReactSelect';
import sortableFieldTypes from '../../../../fields/sortableFieldTypes';
import { useSearchParams } from '../../utilities/SearchParams';
import { fieldIsNamed } from '../../../../fields/config/types';
import './index.scss';
import { useSearchParams } from '../../utilities/SearchParams';
const baseClass = 'sort-complex';
@@ -23,7 +24,7 @@ const SortComplex: React.FC<Props> = (props) => {
const params = useSearchParams();
const [sortFields] = useState(() => collection.fields.reduce((fields, field) => {
if (field.name && sortableFieldTypes.indexOf(field.type) > -1) {
if (fieldIsNamed(field) && sortableFieldTypes.indexOf(field.type) > -1) {
return [
...fields,
{ label: field.label, value: field.name },

View File

@@ -13,6 +13,7 @@ import { Props } from './types';
import HiddenInput from '../field-types/HiddenInput';
import './index.scss';
import { fieldIsNamed } from '../../../../fields/config/types';
const baseClass = 'draggable-section';
@@ -111,7 +112,7 @@ const DraggableSection: React.FC<Props> = (props) => {
permissions={permissions?.fields}
fieldSchema={fieldSchema.map((field) => ({
...field,
path: `${parentPath}.${rowIndex}${field.name ? `.${field.name}` : ''}`,
path: `${parentPath}.${rowIndex}${fieldIsNamed(field) ? `.${field.name}` : ''}`,
}))}
/>
</NegativeFieldGutterProvider>

View File

@@ -1,5 +1,5 @@
import ObjectID from 'bson-objectid';
import { Field as FieldSchema } from '../../../../fields/config/types';
import { Field as FieldSchema, fieldIsNamed, NamedField } from '../../../../fields/config/types';
import { Fields, Field, Data } from './types';
const buildValidationPromise = async (fieldState: Field, field: FieldSchema) => {
@@ -44,13 +44,13 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data =
let initialData = data;
if (!field?.admin?.disabled) {
if (field.name && field.defaultValue && typeof initialData?.[field.name] === 'undefined') {
if (fieldIsNamed(field) && field.defaultValue && typeof initialData?.[field.name] === 'undefined') {
initialData = { [field.name]: field.defaultValue };
}
const passesCondition = Boolean((field?.admin?.condition ? field.admin.condition(fullData || {}, initialData || {}) : true) && parentPassesCondition);
if (field.name) {
if (fieldIsNamed(field)) {
if (field.type === 'relationship' && initialData?.[field.name] === null) {
initialData[field.name] = 'null';
}
@@ -135,10 +135,12 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data =
};
}
const namedField = field as NamedField;
// Handle normal fields
return {
...state,
[`${path}${field.name}`]: structureFieldState(field, passesCondition, data),
[`${path}${namedField.name}`]: structureFieldState(field, passesCondition, data),
};
}

View File

@@ -2,6 +2,7 @@ import React, { createContext, useEffect, useContext, useState } from 'react';
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
import useIntersect from '../../../hooks/useIntersect';
import { Props, Context } from './types';
import { fieldIsNamed } from '../../../../fields/config/types';
const baseClass = 'render-fields';
@@ -69,14 +70,16 @@ const RenderFields: React.FC<Props> = (props) => {
if ((filter && typeof filter === 'function' && filter(field)) || !filter) {
const FieldComponent = field?.admin?.hidden ? fieldTypes.hidden : fieldTypes[field.type];
const fieldPermissions = field?.name ? permissions?.[field.name] : permissions;
const isNamedField = fieldIsNamed(field);
const fieldPermissions = isNamedField ? permissions?.[field.name] : permissions;
let { admin: { readOnly } = {} } = field;
if (readOnlyOverride) readOnly = true;
if (permissions?.[field?.name]?.read?.permission !== false) {
if (permissions?.[field?.name]?.[operation]?.permission === false) {
if ((isNamedField && permissions?.[field?.name]?.read?.permission !== false) || !isNamedField) {
if (isNamedField && permissions?.[field?.name]?.[operation]?.permission === false) {
readOnly = true;
}
@@ -88,7 +91,7 @@ const RenderFields: React.FC<Props> = (props) => {
DefaultComponent={FieldComponent}
componentProps={{
...field,
path: field.path || field.name,
path: field.path || (isNamedField ? field.name : undefined),
fieldTypes,
admin: {
...(field.admin || {}),

View File

@@ -5,6 +5,7 @@ import FieldDescription from '../../FieldDescription';
import FieldTypeGutter from '../../FieldTypeGutter';
import { NegativeFieldGutterProvider } from '../../FieldTypeGutter/context';
import { Props } from './types';
import { fieldIsNamed } from '../../../../../fields/config/types';
import './index.scss';
@@ -41,7 +42,7 @@ const Group: React.FC<Props> = (props) => {
width,
}}
>
{ !hideGutter && (<FieldTypeGutter />) }
{!hideGutter && (<FieldTypeGutter />)}
<div className={`${baseClass}__content-wrapper`}>
{(label || description) && (
@@ -63,7 +64,7 @@ const Group: React.FC<Props> = (props) => {
fieldTypes={fieldTypes}
fieldSchema={fields.map((subField) => ({
...subField,
path: `${path}${subField.name ? `.${subField.name}` : ''}`,
path: `${path}${fieldIsNamed(subField) ? `.${subField.name}` : ''}`,
}))}
/>
</NegativeFieldGutterProvider>

View File

@@ -2,6 +2,7 @@ import React from 'react';
import RenderFields from '../../RenderFields';
import withCondition from '../../withCondition';
import { Props } from './types';
import { fieldIsNamed } from '../../../../../fields/config/types';
import './index.scss';
@@ -24,7 +25,7 @@ const Row: React.FC<Props> = (props) => {
fieldTypes={fieldTypes}
fieldSchema={fields.map((field) => ({
...field,
path: `${path ? `${path}.` : ''}${field.name}`,
path: `${path ? `${path}.` : ''}${fieldIsNamed(field) ? field.name : ''}`,
}))}
/>
);

View File

@@ -1,8 +1,8 @@
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
import { Field } from '../../../../../fields/config/types';
import { Field, fieldIsNamed } from '../../../../../fields/config/types';
const formatFields = (collection: SanitizedCollectionConfig, isEditing: boolean): Field[] => (isEditing
? collection.fields.filter(({ name }) => name !== 'id')
? collection.fields.filter((field) => fieldIsNamed(field) && field.name !== 'id')
: collection.fields);
export default formatFields;

View File

@@ -3,7 +3,7 @@ import Cell from './Cell';
import SortColumn from '../../../elements/SortColumn';
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
import { Column } from '../../../elements/Table/types';
import { fieldHasSubFields, Field } from '../../../../../fields/config/types';
import { fieldHasSubFields, Field, fieldIsNamed } from '../../../../../fields/config/types';
const buildColumns = (collection: SanitizedCollectionConfig, columns: string[]): Column[] => (columns || []).reduce((cols, col, colIndex) => {
let field = null;
@@ -28,13 +28,13 @@ const buildColumns = (collection: SanitizedCollectionConfig, columns: string[]):
];
fields.forEach((fieldToCheck) => {
if (fieldToCheck.name === col) {
if (fieldIsNamed(fieldToCheck) && fieldToCheck.name === col) {
field = fieldToCheck;
}
if (!fieldToCheck.name && fieldHasSubFields(fieldToCheck)) {
if (!fieldIsNamed(fieldToCheck) && fieldHasSubFields(fieldToCheck)) {
fieldToCheck.fields.forEach((subField) => {
if (subField.name === col) {
if (fieldIsNamed(subField) && subField.name === col) {
field = subField;
}
});

View File

@@ -1,8 +1,8 @@
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
import { Field } from '../../../../../fields/config/types';
import { Field, fieldIsNamed } from '../../../../../fields/config/types';
const formatFields = (config: SanitizedCollectionConfig): Field[] => {
const hasID = config.fields.findIndex(({ name }) => name === 'id') > -1;
const hasID = config.fields.findIndex((field) => fieldIsNamed(field) && field.name === 'id') > -1;
let fields: Field[] = config.fields.reduce((formatted, field) => {
if (field.hidden === true || field?.admin?.disabled === true) {
return formatted;

View File

@@ -1,4 +1,4 @@
import { Field, fieldHasSubFields } from '../../../../../fields/config/types';
import { Field, fieldHasSubFields, fieldIsNamed } from '../../../../../fields/config/types';
const getInitialColumnState = (fields: Field[], useAsTitle: string, defaultColumns: string[]): string[] => {
let initialColumns = [];
@@ -13,14 +13,23 @@ const getInitialColumnState = (fields: Field[], useAsTitle: string, defaultColum
}
const remainingColumns = fields.reduce((remaining, field) => {
if (field.name === useAsTitle) {
if (fieldIsNamed(field) && field.name === useAsTitle) {
return remaining;
}
if (!field.name && fieldHasSubFields(field)) {
if (!fieldIsNamed(field) && fieldHasSubFields(field)) {
return [
...remaining,
...field.fields.map((subField) => subField.name),
...field.fields.reduce((subFields, subField) => {
if (fieldIsNamed(subField)) {
return [
...subFields,
subField.name
];
}
return subFields;
}, []),
];
}

View File

@@ -5,7 +5,7 @@ import { PayloadRequest } from '../../express/types';
import getCookieExpiration from '../../utilities/getCookieExpiration';
import isLocked from '../isLocked';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
import { Field, fieldHasSubFields } from '../../fields/config/types';
import { Field, fieldHasSubFields, fieldIsNamed } from '../../fields/config/types';
import { User } from '../types';
import { Collection } from '../../collections/config/types';
@@ -108,15 +108,15 @@ async function login(incomingArgs: Arguments): Promise<Result> {
...signedFields,
};
if (!field.name && fieldHasSubFields(field)) {
if (!fieldIsNamed(field) && fieldHasSubFields(field)) {
field.fields.forEach((subField) => {
if (subField.saveToJWT) {
if (subField.saveToJWT && fieldIsNamed(subField)) {
result[subField.name] = user[subField.name];
}
});
}
if (field.saveToJWT) {
if (field.saveToJWT && fieldIsNamed(field)) {
result[field.name] = user[field.name];
}

View File

@@ -4,6 +4,7 @@ import { Collection } from '../../collections/config/types';
import { APIError } from '../../errors';
import getCookieExpiration from '../../utilities/getCookieExpiration';
import { UserDocument } from '../types';
import { fieldIsNamed } from '../../fields/config/types';
export type Result = {
token: string
@@ -51,7 +52,7 @@ async function resetPassword(args: Arguments): Promise<Result> {
await user.authenticate(data.password);
const fieldsToSign = collectionConfig.fields.reduce((signedFields, field) => {
if (field.saveToJWT) {
if (field.saveToJWT && fieldIsNamed(field)) {
return {
...signedFields,
[field.name]: user[field.name],

View File

@@ -1,4 +1,5 @@
import merge from 'deepmerge';
import { fieldIsNamed } from '../../fields/config/types';
import { SanitizedCollectionConfig, CollectionConfig } from './types';
import sanitizeFields from '../../fields/config/sanitize';
import toKebabCase from '../../utilities/toKebabCase';
@@ -75,7 +76,7 @@ const sanitizeCollection = (collections: CollectionConfig[], collection: Collect
let uploadFields = baseUploadFields;
if (sanitized.upload.mimeTypes) {
uploadFields.find((f) => f.name === 'mimeType').validate = mimeTypeValidator(sanitized.upload.mimeTypes);
uploadFields.find((field) => fieldIsNamed(field) && field.name === 'mimeType').validate = mimeTypeValidator(sanitized.upload.mimeTypes);
}
if (sanitized.upload.imageSizes && Array.isArray(sanitized.upload.imageSizes)) {

View File

@@ -19,6 +19,7 @@ import { PayloadRequest } from '../../express/types';
import { Document } from '../../types';
import { Payload } from '../..';
import saveBufferToFile from '../../uploads/saveBufferToFile';
import { fieldIsNamed } from '../../fields/config/types';
export type Arguments = {
collection: Collection
@@ -74,7 +75,7 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
// Custom id
// /////////////////////////////////////
const hasIdField = collectionConfig.fields.findIndex(({ name }) => name === 'id') > -1;
const hasIdField = collectionConfig.fields.findIndex((field) => fieldIsNamed(field) && field.name === 'id') > -1;
if (hasIdField) {
data = {
_id: data.id,

View File

@@ -7,6 +7,7 @@ import { SanitizedCollectionConfig } from '../collections/config/types';
import fieldSchema, { idField } from '../fields/config/schema';
import { SanitizedGlobalConfig } from '../globals/config/types';
import globalSchema from '../globals/config/schema';
import { fieldIsNamed } from '../fields/config/types';
const logger = Logger();
@@ -14,19 +15,19 @@ const validateFields = (context: string, entity: SanitizedCollectionConfig | San
const errors: string[] = [];
entity.fields.forEach((field) => {
let idResult: Partial<ValidationResult> = { error: null };
if (field.name === 'id') {
if (fieldIsNamed(field) && 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}`);
errors.push(`${context} "${entity.slug}" > Field${fieldIsNamed(field) ? `"${field.name}" >` : ''} ${message}`);
});
}
if (result.error) {
result.error.details.forEach(({ message }) => {
errors.push(`${context} "${entity.slug}" > Field "${field.name}" > ${message}`);
errors.push(`${context} "${entity.slug}" > Field${fieldIsNamed(field) ? `"${field.name}" >` : ''} ${message}`);
});
}
});

View File

@@ -1,9 +1,9 @@
import { Field } from '../fields/config/types';
import { Field, fieldIsNamed } from '../fields/config/types';
import APIError from './APIError';
class MissingFieldType extends APIError {
constructor(field: Field) {
super(`Field "${field.name}" is either missing a field type or it does not match an available field type`);
super(`Field${fieldIsNamed(field) ? ` "${field.name}"` : ''} is either missing a field type or it does not match an available field type`);
}
}

View File

@@ -1,5 +1,5 @@
import { Payload } from '..';
import { Field, HookName } from './config/types';
import { HookName, NamedField } from './config/types';
import relationshipPopulationPromise from './relationshipPopulationPromise';
import { Operation } from '../types';
import { PayloadRequest } from '../express/types';
@@ -8,7 +8,7 @@ type Arguments = {
data: Record<string, unknown>
fullData: Record<string, unknown>
originalDoc: Record<string, unknown>
field: Field
field: NamedField
operation: Operation
overrideAccess: boolean
req: PayloadRequest

View File

@@ -146,7 +146,6 @@ export type RowAdmin = Omit<Admin, 'description'> & {
};
export type RowField = Omit<FieldBase, 'admin' | 'name'> & {
name?: string;
admin?: RowAdmin;
type: 'row';
fields: Field[];
@@ -267,6 +266,24 @@ export type Field =
| PointField
| RowField;
export type NamedField =
TextField
| NumberField
| EmailField
| TextareaField
| CheckboxField
| DateField
| BlockField
| GroupField
| RadioField
| RelationshipField
| ArrayField
| RichTextField
| SelectField
| UploadField
| CodeField
| PointField
export type FieldWithPath = Field & {
path?: string
}
@@ -316,4 +333,8 @@ export function fieldHasMaxDepth(field: Field): field is FieldWithMaxDepth {
return (field.type === 'upload' || field.type === 'relationship') && typeof field.maxDepth === 'number';
}
export function fieldIsNamed(field: Field): field is NamedField {
return 'name' in field;
}
export type HookName = 'beforeChange' | 'beforeValidate' | 'afterChange' | 'afterRead';

View File

@@ -1,10 +1,10 @@
import { PayloadRequest } from '../express/types';
import { Operation } from '../types';
import { Field, HookName } from './config/types';
import { HookName, NamedField } from './config/types';
type Arguments = {
data: Record<string, unknown>
field: Field
field: NamedField
hook: HookName
req: PayloadRequest
operation: Operation

View File

@@ -1,5 +1,5 @@
import { PayloadRequest } from '../express/types';
import { Field, RelationshipField, fieldSupportsMany, fieldHasMaxDepth } from './config/types';
import { RelationshipField, fieldSupportsMany, fieldHasMaxDepth, UploadField } from './config/types';
import { Payload } from '..';
type PopulateArgs = {
@@ -9,7 +9,7 @@ type PopulateArgs = {
overrideAccess: boolean
dataReference: Record<string, any>
data: Record<string, unknown>
field: Field
field: RelationshipField | UploadField
index?: number
payload: Payload
}
@@ -27,12 +27,11 @@ const populate = async ({
}: PopulateArgs) => {
const dataToUpdate = dataReference;
const fieldAsRelationship = field as RelationshipField;
const relation = Array.isArray(fieldAsRelationship.relationTo) ? (data.relationTo as string) : fieldAsRelationship.relationTo;
const relation = Array.isArray(field.relationTo) ? (data.relationTo as string) : field.relationTo;
const relatedCollection = payload.collections[relation];
if (relatedCollection) {
let idString = Array.isArray(fieldAsRelationship.relationTo) ? data.value : data;
let idString = Array.isArray(field.relationTo) ? data.value : data;
if (typeof idString !== 'string' && typeof idString?.toString === 'function') {
idString = idString.toString();
@@ -55,12 +54,12 @@ const populate = async ({
// If populatedRelationship comes back, update value
if (populatedRelationship || populatedRelationship === null) {
if (typeof index === 'number') {
if (Array.isArray(fieldAsRelationship.relationTo)) {
if (Array.isArray(field.relationTo)) {
dataToUpdate[field.name][index].value = populatedRelationship;
} else {
dataToUpdate[field.name][index] = populatedRelationship;
}
} else if (Array.isArray(fieldAsRelationship.relationTo)) {
} else if (Array.isArray(field.relationTo)) {
dataToUpdate[field.name].value = populatedRelationship;
} else {
dataToUpdate[field.name] = populatedRelationship;
@@ -71,7 +70,7 @@ const populate = async ({
type PromiseArgs = {
data: Record<string, any>
field: Field
field: RelationshipField | UploadField
depth: number
currentDepth: number
req: PayloadRequest

View File

@@ -1,7 +1,7 @@
import validationPromise from './validationPromise';
import accessPromise from './accessPromise';
import hookPromise from './hookPromise';
import { Field, fieldHasSubFields, fieldIsArrayType, fieldIsBlockType, HookName } from './config/types';
import { Field, fieldHasSubFields, fieldIsArrayType, fieldIsBlockType, fieldIsNamed, HookName } from './config/types';
import { Operation } from '../types';
import { PayloadRequest } from '../express/types';
import { Payload } from '..';
@@ -82,7 +82,7 @@ const traverseFields = (args: Arguments): void => {
}
}
if (field.hidden && typeof data[field.name] !== 'undefined' && !showHiddenFields) {
if (field.hidden && fieldIsNamed(field) && typeof data[field.name] !== 'undefined' && !showHiddenFields) {
delete data[field.name];
}
@@ -112,7 +112,7 @@ const traverseFields = (args: Arguments): void => {
dataCopy[field.name] = parseFloat(data[field.name]);
}
if (field.name === 'id') {
if (fieldIsNamed(field) && field.name === 'id') {
if (field.type === 'number' && typeof data[field.name] === 'string') {
dataCopy[field.name] = parseFloat(data[field.name]);
}
@@ -151,7 +151,8 @@ const traverseFields = (args: Arguments): void => {
}
}
const hasLocalizedValue = (typeof data?.[field.name] === 'object' && data?.[field.name] !== null)
const hasLocalizedValue = fieldIsNamed(field)
&& (typeof data?.[field.name] === 'object' && data?.[field.name] !== null)
&& field.name
&& field.localized
&& locale !== 'all'
@@ -165,7 +166,7 @@ const traverseFields = (args: Arguments): void => {
dataCopy[field.name] = localizedValue;
}
if (field.localized && unflattenLocales) {
if (fieldIsNamed(field) && field.localized && unflattenLocales) {
unflattenLocaleActions.push(() => {
const localeData = payload.config.localization.locales.reduce((locales, localeID) => {
let valueToSet;
@@ -197,6 +198,7 @@ const traverseFields = (args: Arguments): void => {
});
}
if (fieldIsNamed(field)) {
accessPromises.push(() => accessPromise({
data,
fullData,
@@ -222,12 +224,14 @@ const traverseFields = (args: Arguments): void => {
fullOriginalDoc,
fullData,
}));
}
const passesCondition = (field.admin?.condition && hook === 'beforeChange') ? field.admin.condition(fullData, data) : true;
const skipValidationFromHere = skipValidation || !passesCondition;
if (fieldHasSubFields(field)) {
if (field.name === undefined) {
if (!fieldIsNamed(field)) {
traverseFields({
...args,
fields: field.fields,
@@ -284,7 +288,7 @@ const traverseFields = (args: Arguments): void => {
}
}
if (hook === 'beforeChange' && field.name) {
if (hook === 'beforeChange' && fieldIsNamed(field)) {
const updatedData = data;
if (data?.[field.name] === undefined && originalDoc?.[field.name] === undefined && field.defaultValue) {
@@ -296,7 +300,7 @@ const traverseFields = (args: Arguments): void => {
if (Array.isArray(dataCopy[field.name])) {
dataCopy[field.name].forEach((relatedDoc: {value: unknown, relationTo: string}, i) => {
const relatedCollection = payload.config.collections.find((collection) => collection.slug === relatedDoc.relationTo);
const relationshipIDField = relatedCollection.fields.find((collectionField) => collectionField.name === 'id');
const relationshipIDField = relatedCollection.fields.find((collectionField) => fieldIsNamed(collectionField) && collectionField.name === 'id');
if (relationshipIDField?.type === 'number') {
dataCopy[field.name][i] = { ...relatedDoc, value: parseFloat(relatedDoc.value as string) };
}
@@ -304,7 +308,7 @@ const traverseFields = (args: Arguments): void => {
}
if (field.type === 'relationship' && field.hasMany !== true && dataCopy[field.name]?.relationTo) {
const relatedCollection = payload.config.collections.find((collection) => collection.slug === dataCopy[field.name].relationTo);
const relationshipIDField = relatedCollection.fields.find((collectionField) => collectionField.name === 'id');
const relationshipIDField = relatedCollection.fields.find((collectionField) => fieldIsNamed(collectionField) && collectionField.name === 'id');
if (relationshipIDField?.type === 'number') {
dataCopy[field.name] = { ...dataCopy[field.name], value: parseFloat(dataCopy[field.name].value as string) };
}
@@ -313,7 +317,7 @@ const traverseFields = (args: Arguments): void => {
if (Array.isArray(dataCopy[field.name])) {
dataCopy[field.name].forEach((relatedDoc: unknown, i) => {
const relatedCollection = payload.config.collections.find((collection) => collection.slug === field.relationTo);
const relationshipIDField = relatedCollection.fields.find((collectionField) => collectionField.name === 'id');
const relationshipIDField = relatedCollection.fields.find((collectionField) => fieldIsNamed(collectionField) && collectionField.name === 'id');
if (relationshipIDField?.type === 'number') {
dataCopy[field.name][i] = parseFloat(relatedDoc as string);
}
@@ -321,7 +325,7 @@ const traverseFields = (args: Arguments): void => {
}
if (field.type === 'relationship' && field.hasMany !== true && dataCopy[field.name]) {
const relatedCollection = payload.config.collections.find((collection) => collection.slug === field.relationTo);
const relationshipIDField = relatedCollection.fields.find((collectionField) => collectionField.name === 'id');
const relationshipIDField = relatedCollection.fields.find((collectionField) => fieldIsNamed(collectionField) && collectionField.name === 'id');
if (relationshipIDField?.type === 'number') {
dataCopy[field.name] = parseFloat(dataCopy[field.name]);
}
@@ -364,7 +368,7 @@ const traverseFields = (args: Arguments): void => {
path,
skipValidation: skipValidationFromHere,
}));
} else {
} else if (fieldIsNamed(field)) {
validationPromises.push(() => validationPromise({
errors,
hook,

View File

@@ -1,8 +1,8 @@
import { Field, HookName } from './config/types';
import { HookName, NamedField } from './config/types';
type Arguments = {
hook: HookName
field: Field
field: NamedField
path: string
errors: {message: string, field: string}[]
newData: Record<string, unknown>

View File

@@ -15,13 +15,13 @@ import { GraphQLJSON } from 'graphql-type-json';
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 { ArrayField, CodeField, DateField, EmailField, Field, fieldHasSubFields, fieldIsNamed, GroupField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TextareaField, TextField, UploadField } from '../../fields/config/types';
import { toWords } from '../../utilities/formatLabels';
import payload from '../../index';
import { SanitizedCollectionConfig } from '../../collections/config/types';
export const getCollectionIDType = (config: SanitizedCollectionConfig): GraphQLScalarType => {
const idField = config.fields.find(({ name }) => name === 'id');
const idField = config.fields.find((field) => fieldIsNamed(field) && field.name === 'id');
if (!idField) return GraphQLString;
switch (idField.type) {
case 'number':
@@ -33,21 +33,19 @@ export const getCollectionIDType = (config: SanitizedCollectionConfig): GraphQLS
function buildMutationInputType(name: string, fields: Field[], parentName: string, forceNullable = false): GraphQLInputObjectType {
const fieldToSchemaMap = {
number: (field: Field) => {
number: (field: NumberField) => {
const type = field.name === 'id' ? GraphQLInt : GraphQLFloat;
return { type: withNullableType(field, type, forceNullable) };
},
text: (field: Field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
email: (field: Field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
textarea: (field: Field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
richText: (field: Field) => ({ type: withNullableType(field, GraphQLJSON, forceNullable) }),
code: (field: Field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
date: (field: Field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
upload: (field: Field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
'rich-text': (field: Field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
html: (field: Field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
radio: (field: Field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
point: (field: Field) => ({ type: withNullableType(field, GraphQLList(GraphQLFloat), 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) => {
const formattedName = `${combineParentName(parentName, field.name)}_MutationInput`;
@@ -148,16 +146,33 @@ function buildMutationInputType(name: string, fields: Field[], parentName: strin
if (getFieldSchema) {
const fieldSchema = getFieldSchema(field);
if (Array.isArray(fieldSchema)) {
return fieldSchema.reduce((acc, subField, i) => ({
if (fieldHasSubFields(field) && Array.isArray(fieldSchema)) {
return fieldSchema.reduce((acc, subField, i) => {
const currentSubField = field.fields[i];
if (fieldIsNamed(currentSubField)) {
return {
...acc,
[(field as FieldWithSubFields).fields[i].name]: subField,
}), schema);
[currentSubField.name]: subField,
};
}
return {
...acc,
...fieldSchema,
};
}, schema);
}
if (fieldIsNamed(field)) {
return {
...schema,
[field.name]: fieldSchema,
};
}
return {
...schema,
[field.name]: fieldSchema,
...fieldSchema,
};
}
}

View File

@@ -13,7 +13,7 @@ import {
GraphQLUnionType,
} from 'graphql';
import { DateTimeResolver, EmailAddressResolver } from 'graphql-scalars';
import { Field, RadioField, RelationshipField, SelectField, UploadField, optionIsObject, ArrayField, GroupField, RichTextField } from '../../fields/config/types';
import { Field, RadioField, RelationshipField, SelectField, UploadField, optionIsObject, ArrayField, GroupField, RichTextField, fieldIsNamed } from '../../fields/config/types';
import formatName from '../utilities/formatName';
import combineParentName from '../utilities/combineParentName';
import withNullableType from './withNullableType';
@@ -478,7 +478,7 @@ function buildObjectType(name: string, fields: Field[], parentName: string, base
if (!field.hidden) {
const fieldSchema = fieldToSchemaMap[field.type];
if (fieldSchema) {
if (field.name) {
if (fieldIsNamed(field)) {
return {
...schema,
[formatName(field.name)]: fieldSchema(field),

View File

@@ -32,6 +32,9 @@ import {
TextField,
UploadField,
PointField,
NamedField,
fieldIsNamed,
fieldHasSubFields,
} from '../../fields/config/types';
import formatName from '../utilities/formatName';
import combineParentName from '../utilities/combineParentName';
@@ -47,10 +50,10 @@ import withOperators from './withOperators';
const buildWhereInputType = (name: string, fields: Field[], parentName: string): GraphQLInputObjectType => {
// This is the function that builds nested paths for all
// field types with nested paths.
const recursivelyBuildNestedPaths = (field: FieldWithSubFields) => {
const recursivelyBuildNestedPaths = (field: FieldWithSubFields & NamedField) => {
const nestedPaths = field.fields.reduce((nestedFields, nestedField) => {
const getFieldSchema = fieldToSchemaMap[nestedField.type];
const nestedFieldName = `${field.name}__${nestedField.name}`;
const nestedFieldName = fieldIsNamed(nestedField) ? `${field.name}__${nestedField.name}` : undefined;
if (getFieldSchema) {
const fieldSchema = getFieldSchema({
@@ -294,7 +297,7 @@ const buildWhereInputType = (name: string, fields: Field[], parentName: string):
if (getFieldSchema) {
const rowFieldSchema = getFieldSchema(rowField);
if (Array.isArray(rowFieldSchema)) {
if (fieldHasSubFields(rowField)) {
return [
...rowSchema,
...rowFieldSchema,
@@ -321,7 +324,7 @@ const buildWhereInputType = (name: string, fields: Field[], parentName: string):
if (getFieldSchema) {
const fieldSchema = getFieldSchema(field);
if (Array.isArray(fieldSchema)) {
if (fieldHasSubFields(field)) {
return {
...schema,
...(fieldSchema.reduce((subFields, subField) => ({
@@ -343,7 +346,7 @@ const buildWhereInputType = (name: string, fields: Field[], parentName: string):
fieldTypes.id = {
type: withOperators(
{ name: 'id' } as Field,
{ name: 'id' } as NamedField,
GraphQLJSON,
parentName,
[...operators.equality, ...operators.contains],

View File

@@ -1,8 +1,8 @@
import { GraphQLBoolean, GraphQLInputObjectType, GraphQLList, GraphQLType } from 'graphql';
import { Field } from '../../fields/config/types';
import { NamedField } from '../../fields/config/types';
import combineParentName from '../utilities/combineParentName';
const withOperators = (field: Field, type: GraphQLType, parentName: string, operators: string[]): GraphQLInputObjectType => {
const withOperators = (field: NamedField, type: GraphQLType, parentName: string, operators: string[]): GraphQLInputObjectType => {
const name = `${combineParentName(parentName, field.name)}_operator`;
const listOperators = ['in', 'not_in', 'all'];

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-use-before-define */
import { Schema, SchemaDefinition, SchemaOptions } from 'mongoose';
import { SanitizedConfig } from '../config/types';
import { ArrayField, Block, BlockField, Field, GroupField, RadioField, RelationshipField, RowField, SelectField, UploadField } from '../fields/config/types';
import { ArrayField, Block, BlockField, CheckboxField, CodeField, DateField, EmailField, Field, fieldIsNamed, GroupField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TextareaField, TextField, UploadField } from '../fields/config/types';
import sortableFieldTypes from '../fields/sortableFieldTypes';
type BuildSchemaOptions = {
@@ -76,12 +76,12 @@ const buildSchema = (config: SanitizedConfig, configFields: Field[], buildSchema
const indexFields = [];
if (!allowIDField) {
const idField = schemaFields.find(({ name }) => name === 'id');
const idField = schemaFields.find((field) => fieldIsNamed(field) && field.name === 'id');
if (idField) {
fields = {
_id: idField.type === 'number' ? Number : String,
};
schemaFields = schemaFields.filter(({ name }) => name !== 'id');
schemaFields = schemaFields.filter((field) => fieldIsNamed(field) && field.name !== 'id');
}
}
@@ -97,7 +97,7 @@ const buildSchema = (config: SanitizedConfig, configFields: Field[], buildSchema
indexFields.push(...fieldIndexMap[field.type](field, config));
}
if (config.indexSortableFields && !buildSchemaOptions.global && !field.index && !field.hidden && sortableFieldTypes.indexOf(field.type) > -1) {
if (config.indexSortableFields && !buildSchemaOptions.global && !field.index && !field.hidden && sortableFieldTypes.indexOf(field.type) > -1 && fieldIsNamed(field)) {
indexFields.push({ [field.name]: 1 });
}
});
@@ -113,7 +113,7 @@ const buildSchema = (config: SanitizedConfig, configFields: Field[], buildSchema
};
const fieldIndexMap = {
point: (field: Field, config: SanitizedConfig) => {
point: (field: PointField, config: SanitizedConfig) => {
if (field.localized) {
return config.localization.locales.map((locale) => ({ [`${field.name}.${locale}`]: field.index === false ? undefined : field.index || '2dsphere' }));
}
@@ -122,7 +122,7 @@ const fieldIndexMap = {
};
const fieldToSchemaMap = {
number: (field: Field, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
number: (field: NumberField, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Number };
return {
@@ -130,7 +130,7 @@ const fieldToSchemaMap = {
[field.name]: localizeSchema(field, baseSchema, config.localization.locales),
};
},
text: (field: Field, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
text: (field: TextField, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String };
return {
@@ -138,7 +138,7 @@ const fieldToSchemaMap = {
[field.name]: localizeSchema(field, baseSchema, config.localization.locales),
};
},
email: (field: Field, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
email: (field: EmailField, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String };
return {
@@ -146,7 +146,7 @@ const fieldToSchemaMap = {
[field.name]: localizeSchema(field, baseSchema, config.localization.locales),
};
},
textarea: (field: Field, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
textarea: (field: TextareaField, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String };
return {
@@ -154,7 +154,7 @@ const fieldToSchemaMap = {
[field.name]: localizeSchema(field, baseSchema, config.localization.locales),
};
},
richText: (field: Field, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
richText: (field: RichTextField, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Schema.Types.Mixed };
return {
@@ -162,7 +162,7 @@ const fieldToSchemaMap = {
[field.name]: localizeSchema(field, baseSchema, config.localization.locales),
};
},
code: (field: Field, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
code: (field: CodeField, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String };
return {
@@ -170,7 +170,7 @@ const fieldToSchemaMap = {
[field.name]: localizeSchema(field, baseSchema, config.localization.locales),
};
},
point: (field: Field, fields: SchemaDefinition, config: SanitizedConfig): SchemaDefinition => {
point: (field: PointField, fields: SchemaDefinition, config: SanitizedConfig): SchemaDefinition => {
const baseSchema = {
type: {
type: String,
@@ -205,7 +205,7 @@ const fieldToSchemaMap = {
[field.name]: localizeSchema(field, baseSchema, config.localization.locales),
};
},
checkbox: (field: Field, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
checkbox: (field: CheckboxField, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Boolean };
return {
@@ -213,7 +213,7 @@ const fieldToSchemaMap = {
[field.name]: localizeSchema(field, baseSchema, config.localization.locales),
};
},
date: (field: Field, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
date: (field: DateField, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Date };
return {
@@ -294,7 +294,7 @@ const fieldToSchemaMap = {
field.fields.forEach((rowField: Field) => {
const fieldSchemaMap: FieldSchemaGenerator = fieldToSchemaMap[rowField.type];
if (fieldSchemaMap) {
if (fieldSchemaMap && fieldIsNamed(rowField)) {
const fieldSchema = fieldSchemaMap(rowField, fields, config, buildSchemaOptions);
newFields[rowField.name] = fieldSchema[rowField.name];
}

View File

@@ -38,6 +38,7 @@
"build",
"tests",
"**/*.spec.js",
"node_modules"
"node_modules",
".eslintrc.js"
]
}

741
yarn.lock

File diff suppressed because it is too large Load Diff