mirror of
https://github.com/directus/directus.git
synced 2026-01-23 03:08:08 -05:00
Merge branch 'main' into relational-updates
This commit is contained in:
@@ -78,9 +78,16 @@ function registerHooks(hooks: string[]) {
|
||||
|
||||
function registerHook(hook: string) {
|
||||
const hookPath = path.resolve(extensionsPath, 'hooks', hook, 'index.js');
|
||||
const register: HookRegisterFunction = require(hookPath);
|
||||
const events = register({ services, exceptions, env, database });
|
||||
const hookInstance: HookRegisterFunction | { default?: HookRegisterFunction } = require(hookPath);
|
||||
|
||||
let register: HookRegisterFunction = hookInstance as HookRegisterFunction;
|
||||
if (typeof hookInstance !== "function") {
|
||||
if (hookInstance.default) {
|
||||
register = hookInstance.default;
|
||||
}
|
||||
}
|
||||
|
||||
let events = register({ services, exceptions, env, database });
|
||||
for (const [event, handler] of Object.entries(events)) {
|
||||
emitter.on(event, handler);
|
||||
}
|
||||
@@ -101,7 +108,14 @@ function registerEndpoints(endpoints: string[], router: Router) {
|
||||
|
||||
function registerEndpoint(endpoint: string) {
|
||||
const endpointPath = path.resolve(extensionsPath, 'endpoints', endpoint, 'index.js');
|
||||
const register: EndpointRegisterFunction = require(endpointPath);
|
||||
const endpointInstance: EndpointRegisterFunction | { default?: EndpointRegisterFunction } = require(endpointPath);
|
||||
|
||||
let register: EndpointRegisterFunction = endpointInstance as EndpointRegisterFunction;
|
||||
if (typeof endpointInstance !== "function") {
|
||||
if (endpointInstance.default) {
|
||||
register = endpointInstance.default;
|
||||
}
|
||||
}
|
||||
|
||||
const scopedRouter = express.Router();
|
||||
router.use(`/${endpoint}/`, scopedRouter);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { RequestHandler } from 'express';
|
||||
import { sanitizeQuery } from '../utils/sanitize-query';
|
||||
import { validateQuery } from '../utils/validate-query';
|
||||
|
||||
const sanitizeQueryMiddleware: RequestHandler = (req, res, next) => {
|
||||
req.sanitizedQuery = {};
|
||||
@@ -13,15 +14,16 @@ const sanitizeQueryMiddleware: RequestHandler = (req, res, next) => {
|
||||
req.sanitizedQuery = sanitizeQuery(
|
||||
{
|
||||
fields: req.query.fields || '*',
|
||||
...req.query
|
||||
...req.query,
|
||||
},
|
||||
req.accountability || null
|
||||
);
|
||||
|
||||
Object.freeze(req.sanitizedQuery);
|
||||
|
||||
validateQuery(req.sanitizedQuery);
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
export default sanitizeQueryMiddleware;
|
||||
|
||||
|
||||
@@ -2,7 +2,12 @@ import { Filter, Accountability } from '../types';
|
||||
import { deepMap } from './deep-map';
|
||||
|
||||
export function parseFilter(filter: Filter, accountability: Accountability | null) {
|
||||
return deepMap(filter, (val: any) => {
|
||||
return deepMap(filter, (val: any, key: string) => {
|
||||
if (val === 'true') return true;
|
||||
if (val === 'false') return false;
|
||||
|
||||
if (key === '_in' || key === '_nin') return val.split(',').filter((val: any) => val);
|
||||
|
||||
if (val === '$NOW') return new Date();
|
||||
if (val === '$CURRENT_USER') return accountability?.user || null;
|
||||
if (val === '$CURRENT_ROLE') return accountability?.role || null;
|
||||
|
||||
@@ -2,7 +2,10 @@ import { Accountability, Query, Sort, Filter, Meta } from '../types';
|
||||
import logger from '../logger';
|
||||
import { parseFilter } from '../utils/parse-filter';
|
||||
|
||||
export function sanitizeQuery(rawQuery: Record<string, any>, accountability: Accountability | null) {
|
||||
export function sanitizeQuery(
|
||||
rawQuery: Record<string, any>,
|
||||
accountability: Accountability | null
|
||||
) {
|
||||
const query: Query = {};
|
||||
|
||||
if (rawQuery.limit !== undefined) {
|
||||
@@ -49,11 +52,7 @@ export function sanitizeQuery(rawQuery: Record<string, any>, accountability: Acc
|
||||
query.search = rawQuery.search;
|
||||
}
|
||||
|
||||
if (
|
||||
rawQuery.export &&
|
||||
typeof rawQuery.export === 'string' &&
|
||||
['json', 'csv'].includes(rawQuery.export)
|
||||
) {
|
||||
if (rawQuery.export) {
|
||||
query.export = rawQuery.export as 'json' | 'csv';
|
||||
}
|
||||
|
||||
|
||||
110
api/src/utils/validate-query.ts
Normal file
110
api/src/utils/validate-query.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Query } from '../types';
|
||||
import Joi from 'joi';
|
||||
import { InvalidQueryException } from '../exceptions';
|
||||
|
||||
const querySchema = Joi.object({
|
||||
fields: Joi.array().items(Joi.string()),
|
||||
sort: Joi.array().items(
|
||||
Joi.object({
|
||||
column: Joi.string(),
|
||||
order: Joi.string().valid('asc', 'desc'),
|
||||
})
|
||||
),
|
||||
filter: Joi.object({}).unknown(),
|
||||
limit: Joi.number(),
|
||||
offset: Joi.number(),
|
||||
page: Joi.number(),
|
||||
single: Joi.boolean(),
|
||||
meta: Joi.array().items(Joi.string().valid('total_count', 'result_count')),
|
||||
search: Joi.string(),
|
||||
export: Joi.string().valid('json', 'csv'),
|
||||
deep: Joi.link('#query'),
|
||||
}).id('query');
|
||||
|
||||
export function validateQuery(query: Query) {
|
||||
const { error } = querySchema.validate(query);
|
||||
|
||||
if (query.filter && Object.keys(query.filter).length > 0) {
|
||||
validateFilter(query.filter);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw new InvalidQueryException(error.message);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
function validateFilter(filter: Query['filter']) {
|
||||
if (!filter) throw new InvalidQueryException('Invalid filter object');
|
||||
|
||||
for (const [key, nested] of Object.entries(filter)) {
|
||||
if (key === '_and' || key === '_or') {
|
||||
nested.forEach(validateFilter);
|
||||
} else if (key.startsWith('_')) {
|
||||
const value = nested;
|
||||
|
||||
switch (key) {
|
||||
case '_eq':
|
||||
case '_neq':
|
||||
case '_contains':
|
||||
case '_ncontains':
|
||||
case '_gt':
|
||||
case '_gte':
|
||||
case '_lt':
|
||||
case '_lte':
|
||||
default:
|
||||
validateFilterPrimitive(value, key);
|
||||
break;
|
||||
case '_in':
|
||||
case '_nin':
|
||||
validateList(value, key);
|
||||
break;
|
||||
case '_null':
|
||||
case '_nnull':
|
||||
case '_empty':
|
||||
case '_nempty':
|
||||
validateBoolean(value, key);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
validateFilter(nested);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateFilterPrimitive(value: any, key: string) {
|
||||
if ((typeof value === 'string' || typeof value === 'number') === false) {
|
||||
throw new InvalidQueryException(
|
||||
`The filter value for "${key}" has to be a string or a number`
|
||||
);
|
||||
}
|
||||
|
||||
if (Number.isNaN(value)) {
|
||||
throw new InvalidQueryException(`The filter value for "${key}" is not a valid number`);
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.length === 0) {
|
||||
throw new InvalidQueryException(
|
||||
`You can't filter for an empty string in "${key}". Use "_empty" or "_nempty" instead`
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateList(value: any, key: string) {
|
||||
if (Array.isArray(value) === false || value.length === 0) {
|
||||
throw new InvalidQueryException(`"${key}" has to be an array of values`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateBoolean(value: any, key: string) {
|
||||
if (typeof value !== 'boolean') {
|
||||
throw new InvalidQueryException(`"${key}" has to be a boolean`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import { Filter } from '@/types/';
|
||||
import { set } from 'lodash';
|
||||
import { set, clone } from 'lodash';
|
||||
|
||||
export default function filtersToQuery(filters: readonly Filter[]) {
|
||||
const filterQuery: Record<string, any> = {};
|
||||
|
||||
for (const filter of filters) {
|
||||
const { field, operator, value } = filter;
|
||||
let { field, operator, value } = clone(filter) as any;
|
||||
|
||||
if (['empty', 'nempty', 'null', 'nnull'].includes(operator)) {
|
||||
value = true;
|
||||
}
|
||||
|
||||
if (!value) continue;
|
||||
|
||||
set(filterQuery, field, { [`_${operator}`]: value });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user