Add field-level validation (#12363)

* Add field validation column

* Add frontend config for validation

* Make it work

* Add regex to filter configuration

* Fix const/let

* Add custom validation message support

* Add custom validation message tooltip inline

* Fix custom names in validation errors up top

* Fix type error

* Nog eentje om het af te leren

* resolve unused import warnings
This commit is contained in:
Rijk van Zanten
2022-03-25 18:03:36 -04:00
committed by GitHub
parent 82d1ad3b94
commit 175fb849c4
20 changed files with 544 additions and 390 deletions

View File

@@ -0,0 +1,15 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_fields', (table) => {
table.json('validation');
table.text('validation_message');
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_fields', (table) => {
table.dropColumn('validation');
table.dropColumn('validation_message');
});
}

View File

@@ -1,22 +1,22 @@
import { Knex } from 'knex';
import { cloneDeep, merge, uniq, uniqWith, flatten, isNil, isArray } from 'lodash';
import getDatabase from '../database';
import { ForbiddenException } from '../exceptions';
import { FailedValidationException } from '@directus/shared/exceptions';
import { validatePayload } from '@directus/shared/utils';
import { AbstractServiceOptions, AST, FieldNode, Item, NestedCollectionNode, PrimaryKey } from '../types';
import {
Query,
Accountability,
Aggregate,
Filter,
Permission,
PermissionsAction,
Accountability,
Query,
SchemaOverview,
Filter,
} from '@directus/shared/types';
import { validatePayload } from '@directus/shared/utils';
import { Knex } from 'knex';
import { cloneDeep, flatten, isArray, isNil, merge, uniq, uniqWith } from 'lodash';
import getDatabase from '../database';
import { ForbiddenException } from '../exceptions';
import { AbstractServiceOptions, AST, FieldNode, Item, NestedCollectionNode, PrimaryKey } from '../types';
import { stripFunction } from '../utils/strip-function';
import { ItemsService } from './items';
import { PayloadService } from './payload';
import { stripFunction } from '../utils/strip-function';
export class AuthorizationService {
knex: Knex;
@@ -302,9 +302,15 @@ export class AuthorizationService {
const payloadWithPresets = merge({}, preset, payload);
const fieldValidationRules = Object.values(this.schema.collections[collection].fields)
.map((field) => field.validation)
.filter((v) => v) as Filter[];
const hasValidationRules =
isNil(permission.validation) === false && Object.keys(permission.validation ?? {}).length > 0;
const hasFieldValidationRules = fieldValidationRules && fieldValidationRules.length > 0;
const requiredColumns: SchemaOverview['collections'][string]['fields'][string][] = [];
for (const field of Object.values(this.schema.collections[collection].fields)) {
@@ -321,7 +327,7 @@ export class AuthorizationService {
}
}
if (hasValidationRules === false && requiredColumns.length === 0) {
if (hasValidationRules === false && hasFieldValidationRules === false && requiredColumns.length === 0) {
return payloadWithPresets;
}
@@ -345,6 +351,14 @@ export class AuthorizationService {
}
}
if (hasFieldValidationRules) {
if (permission.validation) {
permission.validation = { _and: [permission.validation, ...fieldValidationRules] };
} else {
permission.validation = { _and: fieldValidationRules };
}
}
const validationErrors: FailedValidationException[] = [];
validationErrors.push(

View File

@@ -1,17 +1,17 @@
import SchemaInspector from '@directus/schema';
import { Accountability, SchemaOverview, Filter } from '@directus/shared/types';
import { toArray } from '@directus/shared/utils';
import { Knex } from 'knex';
import { mapValues } from 'lodash';
import { getCache, setSystemCache } from '../cache';
import getDatabase from '../database';
import { systemCollectionRows } from '../database/system-data/collections';
import { systemFieldRows } from '../database/system-data/fields';
import env from '../env';
import logger from '../logger';
import { RelationsService } from '../services';
import { Accountability, SchemaOverview } from '@directus/shared/types';
import { toArray } from '@directus/shared/utils';
import getDefaultValue from './get-default-value';
import getLocalType from './get-local-type';
import getDatabase from '../database';
import { getCache, setSystemCache } from '../cache';
import env from '../env';
export async function getSchema(options?: {
accountability?: Accountability;
@@ -106,6 +106,7 @@ async function getDatabaseSchema(
scale: column.numeric_scale || null,
special: [],
note: null,
validation: null,
alias: false,
};
}),
@@ -114,13 +115,16 @@ async function getDatabaseSchema(
const fields = [
...(await database
.select<{ id: number; collection: string; field: string; special: string; note: string | null }[]>(
'id',
'collection',
'field',
'special',
'note'
)
.select<
{
id: number;
collection: string;
field: string;
special: string;
note: string | null;
validation: string | Record<string, any> | null;
}[]
>('id', 'collection', 'field', 'special', 'note', 'validation')
.from('directus_fields')),
...systemFieldRows,
].filter((field) => (field.special ? toArray(field.special) : []).includes('no-data') === false);
@@ -132,6 +136,9 @@ async function getDatabaseSchema(
const column = schemaOverview[field.collection].columns[field.field];
const special = field.special ? toArray(field.special) : [];
const type = (existing && getLocalType(column, { special })) || 'alias';
let validation = field.validation ?? null;
if (validation && typeof validation === 'string') validation = JSON.parse(validation);
result.collections[field.collection].fields[field.field] = {
field: field.field,
@@ -145,6 +152,7 @@ async function getDatabaseSchema(
special: special,
note: field.note,
alias: existing?.alias ?? true,
validation: (field.validation as Filter) ?? null,
};
}