From 413d21fe4b7d37b53779f511ec239c4d99ae1988 Mon Sep 17 00:00:00 2001 From: Nitwel Date: Fri, 14 Oct 2022 20:38:53 +0200 Subject: [PATCH] treat empty array as null on relational field (#15958) * treat empty array as null on relational field * Run prettier * add unit test * Fix linter warnings Co-authored-by: rijkvanzanten --- .../insights/routes/panel-configuration.vue | 2 +- .../field-configuration.vue | 2 +- .../flows/components/operation-detail.vue | 2 +- .../modules/settings/routes/flows/flow.vue | 2 +- app/src/utils/validate-item.test.ts | 87 +++++++++++++++++++ app/src/utils/validate-item.ts | 16 +++- .../views/private/components/image-editor.vue | 2 +- 7 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 app/src/utils/validate-item.test.ts diff --git a/app/src/modules/insights/routes/panel-configuration.vue b/app/src/modules/insights/routes/panel-configuration.vue index f7ef7f0c32..69b927f048 100644 --- a/app/src/modules/insights/routes/panel-configuration.vue +++ b/app/src/modules/insights/routes/panel-configuration.vue @@ -100,7 +100,7 @@ import { useInsightsStore } from '@/stores/insights'; import { CreatePanel } from '@/stores/insights'; import { Panel } from '@directus/shared/types'; import { assign, clone, omitBy, isUndefined } from 'lodash'; -import { nanoid } from 'nanoid'; +import { nanoid } from 'nanoid/non-secure'; import { storeToRefs } from 'pinia'; import { computed, reactive, unref } from 'vue'; import { useI18n } from 'vue-i18n'; diff --git a/app/src/modules/settings/routes/data-model/field-detail/field-detail-simple/field-configuration.vue b/app/src/modules/settings/routes/data-model/field-detail/field-detail-simple/field-configuration.vue index b248008977..573a26d442 100644 --- a/app/src/modules/settings/routes/data-model/field-detail/field-detail-simple/field-configuration.vue +++ b/app/src/modules/settings/routes/data-model/field-detail/field-detail-simple/field-configuration.vue @@ -69,7 +69,7 @@ import { useFieldDetailStore, syncFieldDetailStoreProperty } from '../store/'; import { storeToRefs } from 'pinia'; import ExtensionOptions from '../shared/extension-options.vue'; import RelationshipConfiguration from './relationship-configuration.vue'; -import { nanoid } from 'nanoid'; +import { nanoid } from 'nanoid/non-secure'; export default defineComponent({ components: { ExtensionOptions, RelationshipConfiguration }, diff --git a/app/src/modules/settings/routes/flows/components/operation-detail.vue b/app/src/modules/settings/routes/flows/components/operation-detail.vue index d379fc37f3..47ddb41471 100644 --- a/app/src/modules/settings/routes/flows/components/operation-detail.vue +++ b/app/src/modules/settings/routes/flows/components/operation-detail.vue @@ -74,7 +74,7 @@ import { FlowRaw } from '@directus/shared/types'; import slugify from '@sindresorhus/slugify'; import { computed, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; -import { customAlphabet } from 'nanoid'; +import { customAlphabet } from 'nanoid/non-secure'; const generateSuffix = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 5); diff --git a/app/src/modules/settings/routes/flows/flow.vue b/app/src/modules/settings/routes/flows/flow.vue index 3e6ae07e04..f295f3d3f5 100644 --- a/app/src/modules/settings/routes/flows/flow.vue +++ b/app/src/modules/settings/routes/flows/flow.vue @@ -189,7 +189,7 @@ import { useEditsGuard } from '@/composables/use-edits-guard'; import { useShortcut } from '@/composables/use-shortcut'; import { isEmpty, merge, omit, cloneDeep } from 'lodash'; import { router } from '@/router'; -import { nanoid, customAlphabet } from 'nanoid'; +import { nanoid, customAlphabet } from 'nanoid/non-secure'; import SettingsNotFound from '../not-found.vue'; import SettingsNavigation from '../../components/navigation.vue'; diff --git a/app/src/utils/validate-item.test.ts b/app/src/utils/validate-item.test.ts new file mode 100644 index 0000000000..bef401bc1e --- /dev/null +++ b/app/src/utils/validate-item.test.ts @@ -0,0 +1,87 @@ +import { beforeEach, expect, test } from 'vitest'; + +import { validateItem } from '@/utils/validate-item'; +import { DeepPartial, Field } from '@directus/shared/types'; +import { createTestingPinia } from '@pinia/testing'; +import { setActivePinia } from 'pinia'; + +const fields: DeepPartial[] = [ + { + field: 'id', + collection: 'users', + type: 'integer', + name: 'ID', + meta: { + required: true, + }, + schema: null, + }, + { + field: 'name', + collection: 'users', + type: 'string', + name: 'Name', + meta: { + required: true, + }, + schema: null, + }, + { + field: 'email', + collection: 'users', + type: 'string', + name: 'Email', + schema: null, + }, + { + field: 'role', + collection: 'users', + type: 'integer', + name: 'Role', + meta: { + required: true, + }, + schema: null, + }, +]; + +beforeEach(() => { + setActivePinia( + createTestingPinia({ + createSpy: () => (_collection, field) => { + if (field === 'role') { + return [{ some: 'relation' }]; + } + return []; + }, + }) + ); +}); + +test('Required fields', () => { + let result = validateItem( + { + id: 1, + name: 'test', + email: 'test@test.com', + role: [1, 2], + }, + fields as Field[], + true + ); + + expect(result.length).toEqual(0); + + result = validateItem( + { + id: 1, + name: 'test', + email: 'test@test.com', + role: [], + }, + fields as Field[], + true + ); + + expect(result.length).toEqual(1); +}); diff --git a/app/src/utils/validate-item.ts b/app/src/utils/validate-item.ts index e2e08a52b7..4fef6cf846 100644 --- a/app/src/utils/validate-item.ts +++ b/app/src/utils/validate-item.ts @@ -1,10 +1,13 @@ +import { useRelationsStore } from '@/stores/relations'; import { FailedValidationException } from '@directus/shared/exceptions'; import { Field, LogicalFilterAND } from '@directus/shared/types'; import { validatePayload } from '@directus/shared/utils'; -import { flatten, isNil } from 'lodash'; +import { cloneDeep, flatten, isEmpty, isNil } from 'lodash'; import { applyConditions } from './apply-conditions'; export function validateItem(item: Record, fields: Field[], isNew: boolean) { + const relationsStore = useRelationsStore(); + const validationRules = { _and: [], } as LogicalFilterAND; @@ -13,6 +16,8 @@ export function validateItem(item: Record, fields: Field[], isNew: const requiredFields = fieldsWithConditions.filter((field) => field.meta?.required === true); + const updatedItem = cloneDeep(item); + for (const field of requiredFields) { if (isNew && isNil(field.schema?.default_value)) { validationRules._and.push({ @@ -22,6 +27,13 @@ export function validateItem(item: Record, fields: Field[], isNew: }); } + const relation = relationsStore.getRelationsForField(field.collection, field.field); + + // Check if we are dealing with a relational field that has an empty array as its value + if (relation.length > 0 && Array.isArray(updatedItem[field.field]) && isEmpty(updatedItem[field.field])) { + updatedItem[field.field] = null; + } + validationRules._and.push({ [field.field]: { _nnull: true, @@ -30,7 +42,7 @@ export function validateItem(item: Record, fields: Field[], isNew: } return flatten( - validatePayload(validationRules, item).map((error) => + validatePayload(validationRules, updatedItem).map((error) => error.details.map((details) => new FailedValidationException(details).extensions) ) ).map((error) => { diff --git a/app/src/views/private/components/image-editor.vue b/app/src/views/private/components/image-editor.vue index 49fffa2d7b..cdf9b0f36c 100644 --- a/app/src/views/private/components/image-editor.vue +++ b/app/src/views/private/components/image-editor.vue @@ -133,7 +133,7 @@ import { getRootPath } from '@/utils/get-root-path'; import { unexpectedError } from '@/utils/unexpected-error'; import Cropper from 'cropperjs'; import throttle from 'lodash/throttle'; -import { nanoid } from 'nanoid'; +import { nanoid } from 'nanoid/non-secure'; type Image = { type: string;