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 <rijkvanzanten@me.com>
This commit is contained in:
Nitwel
2022-10-14 20:38:53 +02:00
committed by GitHub
parent 64f60a007a
commit 413d21fe4b
7 changed files with 106 additions and 7 deletions

View File

@@ -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';

View File

@@ -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 },

View File

@@ -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);

View File

@@ -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';

View File

@@ -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>[] = [
{
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);
});

View File

@@ -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<string, any>, fields: Field[], isNew: boolean) {
const relationsStore = useRelationsStore();
const validationRules = {
_and: [],
} as LogicalFilterAND;
@@ -13,6 +16,8 @@ export function validateItem(item: Record<string, any>, 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<string, any>, 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<string, any>, 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) => {

View File

@@ -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;