mirror of
https://github.com/directus/directus.git
synced 2026-01-11 01:38:06 -05:00
Support _contains operation for CSV type (#22002)
Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
This commit is contained in:
5
.changeset/slimy-paws-cough.md
Normal file
5
.changeset/slimy-paws-cough.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/utils': patch
|
||||
---
|
||||
|
||||
Fixed "contains" operators (used for validation / conditions) to work with arrays and "icontains" to respect case insensitivity
|
||||
@@ -219,78 +219,6 @@ describe(`generateJoi`, () => {
|
||||
expect(generateJoi(mockFieldFilter).describe()).toStrictEqual(mockSchema);
|
||||
});
|
||||
|
||||
it(`returns the correct schema for an _ncontains contain match`, () => {
|
||||
const mockFieldFilter = { field: { _ncontains: 'field' } } as FieldFilter;
|
||||
|
||||
const mockSchema = Joi.object({
|
||||
field: (Joi.string() as StringSchema).ncontains('field'),
|
||||
})
|
||||
.unknown()
|
||||
.describe();
|
||||
|
||||
expect(generateJoi(mockFieldFilter).describe()).toStrictEqual(mockSchema);
|
||||
});
|
||||
|
||||
it(`returns the correct schema for an _ncontains with null value`, () => {
|
||||
const mockFieldFilter = { field: { _ncontains: null } } as FieldFilter;
|
||||
|
||||
const mockSchema = Joi.object({
|
||||
field: Joi.any().equal(true),
|
||||
})
|
||||
.unknown()
|
||||
.describe();
|
||||
|
||||
expect(generateJoi(mockFieldFilter).describe()).toStrictEqual(mockSchema);
|
||||
});
|
||||
|
||||
it(`returns the correct schema for an _contains contain match`, () => {
|
||||
const mockFieldFilter = { field: { _contains: 'field' } } as FieldFilter;
|
||||
|
||||
const mockSchema = Joi.object({
|
||||
field: (Joi.string() as StringSchema).contains('field'),
|
||||
})
|
||||
.unknown()
|
||||
.describe();
|
||||
|
||||
expect(generateJoi(mockFieldFilter).describe()).toStrictEqual(mockSchema);
|
||||
});
|
||||
|
||||
it(`returns the correct schema for an _contains with null value`, () => {
|
||||
const mockFieldFilter = { field: { _contains: null } } as FieldFilter;
|
||||
|
||||
const mockSchema = Joi.object({
|
||||
field: Joi.any().equal(true),
|
||||
})
|
||||
.unknown()
|
||||
.describe();
|
||||
|
||||
expect(generateJoi(mockFieldFilter).describe()).toStrictEqual(mockSchema);
|
||||
});
|
||||
|
||||
it(`returns the correct schema for an _icontains contain match`, () => {
|
||||
const mockFieldFilter = { field: { _icontains: 'field' } } as FieldFilter;
|
||||
|
||||
const mockSchema = Joi.object({
|
||||
field: (Joi.string() as StringSchema).contains('field'),
|
||||
})
|
||||
.unknown()
|
||||
.describe();
|
||||
|
||||
expect(generateJoi(mockFieldFilter).describe()).toStrictEqual(mockSchema);
|
||||
});
|
||||
|
||||
it(`returns the correct schema for an _icontains with null value`, () => {
|
||||
const mockFieldFilter = { field: { _icontains: null } } as FieldFilter;
|
||||
|
||||
const mockSchema = Joi.object({
|
||||
field: Joi.any().equal(true),
|
||||
})
|
||||
.unknown()
|
||||
.describe();
|
||||
|
||||
expect(generateJoi(mockFieldFilter).describe()).toStrictEqual(mockSchema);
|
||||
});
|
||||
|
||||
it(`returns a value if the substring is included in the value`, () => {
|
||||
expect(() => {
|
||||
Joi.assert('testfield', (Joi.string() as StringSchema).contains('field'));
|
||||
|
||||
@@ -5,6 +5,7 @@ import { escapeRegExp, merge } from 'lodash-es';
|
||||
|
||||
export interface StringSchema extends BaseStringSchema {
|
||||
contains(substring: string): this;
|
||||
icontains(substring: string): this;
|
||||
ncontains(substring: string): this;
|
||||
}
|
||||
|
||||
@@ -13,6 +14,7 @@ export const Joi: typeof BaseJoi = BaseJoi.extend({
|
||||
base: BaseJoi.string(),
|
||||
messages: {
|
||||
'string.contains': '{{#label}} must contain [{{#substring}}]',
|
||||
'string.icontains': '{{#label}} must contain case insensitive [{{#substring}}]',
|
||||
'string.ncontains': "{{#label}} can't contain [{{#substring}}]",
|
||||
},
|
||||
rules: {
|
||||
@@ -36,6 +38,26 @@ export const Joi: typeof BaseJoi = BaseJoi.extend({
|
||||
return value;
|
||||
},
|
||||
},
|
||||
icontains: {
|
||||
args: [
|
||||
{
|
||||
name: 'substring',
|
||||
ref: true,
|
||||
assert: (val) => typeof val === 'string',
|
||||
message: 'must be a string',
|
||||
},
|
||||
],
|
||||
method(substring) {
|
||||
return this.$_addRule({ name: 'icontains', args: { substring } });
|
||||
},
|
||||
validate(value: string, helpers, { substring }) {
|
||||
if (value.toLowerCase().includes(substring.toLowerCase()) === false) {
|
||||
return helpers.error('string.icontains', { substring });
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
ncontains: {
|
||||
args: [
|
||||
{
|
||||
@@ -135,7 +157,10 @@ export function generateJoi(filter: FieldFilter | null, options?: JoiOptions): A
|
||||
if (compareValue === null || compareValue === undefined || typeof compareValue !== 'string') {
|
||||
schema[key] = Joi.any().equal(true);
|
||||
} else {
|
||||
schema[key] = getStringSchema().contains(compareValue);
|
||||
schema[key] = Joi.alternatives().try(
|
||||
getStringSchema().contains(compareValue),
|
||||
Joi.array().items(getStringSchema().contains(compareValue).required(), Joi.any()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +168,10 @@ export function generateJoi(filter: FieldFilter | null, options?: JoiOptions): A
|
||||
if (compareValue === null || compareValue === undefined || typeof compareValue !== 'string') {
|
||||
schema[key] = Joi.any().equal(true);
|
||||
} else {
|
||||
schema[key] = getStringSchema().contains(compareValue);
|
||||
schema[key] = Joi.alternatives().try(
|
||||
getStringSchema().icontains(compareValue),
|
||||
Joi.array().items(getStringSchema().icontains(compareValue).required(), Joi.any()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +179,10 @@ export function generateJoi(filter: FieldFilter | null, options?: JoiOptions): A
|
||||
if (compareValue === null || compareValue === undefined || typeof compareValue !== 'string') {
|
||||
schema[key] = Joi.any().equal(true);
|
||||
} else {
|
||||
schema[key] = getStringSchema().ncontains(compareValue);
|
||||
schema[key] = Joi.alternatives().try(
|
||||
getStringSchema().ncontains(compareValue),
|
||||
Joi.array().items(getStringSchema().contains(compareValue).forbidden()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { Filter } from '@directus/types';
|
||||
import { describe, expect, it, test } from 'vitest';
|
||||
import { validatePayload } from './validate-payload.js';
|
||||
|
||||
describe('validatePayload', () => {
|
||||
@@ -87,4 +87,117 @@ describe('validatePayload', () => {
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0]!.message).toBe(`"field" is required`);
|
||||
});
|
||||
|
||||
describe('validates operator: _contains', () => {
|
||||
const mockFilter = {
|
||||
_and: [
|
||||
{
|
||||
value: {
|
||||
_contains: 'MATCH-EXACT',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = { requireAll: true };
|
||||
|
||||
test('string values', () => {
|
||||
expect(validatePayload(mockFilter, { value: 'MATCH-EXACT' }, options)).toHaveLength(0);
|
||||
expect(validatePayload(mockFilter, { value: 'substring-MATCH-EXACT' }, options)).toHaveLength(0);
|
||||
|
||||
expect(validatePayload(mockFilter, { value: 'match-exact' }, options)).toHaveLength(1);
|
||||
expect(validatePayload(mockFilter, { value: 'mismatch' }, options)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('array values', () => {
|
||||
expect(validatePayload(mockFilter, { value: [123, 'MATCH-EXACT'] }, options)).toHaveLength(0);
|
||||
|
||||
expect(validatePayload(mockFilter, { value: [123, 'match-exact'] }, options)).toHaveLength(1);
|
||||
expect(validatePayload(mockFilter, { value: [] }, options)).toHaveLength(1);
|
||||
expect(validatePayload(mockFilter, { value: ['mismatch'] }, options)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('other values', () => {
|
||||
expect(validatePayload(mockFilter, { value: null }, options)).toHaveLength(1);
|
||||
expect(validatePayload(mockFilter, { value: undefined }, options)).toHaveLength(1);
|
||||
expect(validatePayload(mockFilter, { value: 123 }, options)).toHaveLength(1);
|
||||
expect(validatePayload(mockFilter, { value: {} }, options)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validates operator: _icontains', () => {
|
||||
const mockFilter = {
|
||||
_and: [
|
||||
{
|
||||
value: {
|
||||
_icontains: 'match-insensitive',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = { requireAll: true };
|
||||
|
||||
test('string values', () => {
|
||||
expect(validatePayload(mockFilter, { value: 'MATCH-insensitive' }, options)).toHaveLength(0);
|
||||
expect(validatePayload(mockFilter, { value: 'match-insensitive' }, options)).toHaveLength(0);
|
||||
expect(validatePayload(mockFilter, { value: 'substring-match-insensitive' }, options)).toHaveLength(0);
|
||||
|
||||
expect(validatePayload(mockFilter, { value: 'mismatch' }, options)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('array values', () => {
|
||||
expect(validatePayload(mockFilter, { value: [123, 'match-insensitive'] }, options)).toHaveLength(0);
|
||||
expect(validatePayload(mockFilter, { value: [123, 'MATCH-insensitive'] }, options)).toHaveLength(0);
|
||||
expect(validatePayload(mockFilter, { value: [123, 'substring-MATCH-insensitive'] }, options)).toHaveLength(0);
|
||||
|
||||
expect(validatePayload(mockFilter, { value: [] }, options)).toHaveLength(1);
|
||||
expect(validatePayload(mockFilter, { value: ['mismatch'] }, options)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('other values', () => {
|
||||
expect(validatePayload(mockFilter, { value: null }, options)).toHaveLength(1);
|
||||
expect(validatePayload(mockFilter, { value: undefined }, options)).toHaveLength(1);
|
||||
expect(validatePayload(mockFilter, { value: 123 }, options)).toHaveLength(1);
|
||||
expect(validatePayload(mockFilter, { value: {} }, options)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validates operator: _ncontains', () => {
|
||||
const mockFilter = {
|
||||
_and: [
|
||||
{
|
||||
value: {
|
||||
_ncontains: 'match',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = { requireAll: true };
|
||||
|
||||
test('string values', () => {
|
||||
expect(validatePayload(mockFilter, { value: 'foo' }, options)).toHaveLength(0);
|
||||
expect(validatePayload(mockFilter, { value: 'MATCH' }, options)).toHaveLength(0);
|
||||
|
||||
expect(validatePayload(mockFilter, { value: 'match' }, options)).toHaveLength(1);
|
||||
expect(validatePayload(mockFilter, { value: 'substring-match' }, options)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('array values', () => {
|
||||
expect(validatePayload(mockFilter, { value: [] }, options)).toHaveLength(0);
|
||||
expect(validatePayload(mockFilter, { value: ['foo'] }, options)).toHaveLength(0);
|
||||
expect(validatePayload(mockFilter, { value: ['MATCH'] }, options)).toHaveLength(0);
|
||||
|
||||
expect(validatePayload(mockFilter, { value: ['foo', 'match'] }, options)).toHaveLength(1);
|
||||
expect(validatePayload(mockFilter, { value: ['substring-match'] }, options)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('other values', () => {
|
||||
expect(validatePayload(mockFilter, { value: null }, options)).toHaveLength(1);
|
||||
expect(validatePayload(mockFilter, { value: undefined }, options)).toHaveLength(1);
|
||||
expect(validatePayload(mockFilter, { value: 123 }, options)).toHaveLength(1);
|
||||
expect(validatePayload(mockFilter, { value: {} }, options)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user