mirror of
https://github.com/directus/directus.git
synced 2026-01-29 08:17:55 -05:00
* Remove advanced filter sidebar detail So long, and thanks for all the fish. * Remove filter conversion logic * Start replacing/removing old skool filters * Add inline mode for usages in search bar * Make filter work in header bar * Emit empty string as null in filter * Move shared filter types to shared * Upgrade use-items * Fix manual sort on tabular * Cleanup styling in search bar usage * Tweak styling * Fix filtering issues * Update cards * Remove activeFilterCount from tabular * Update maps to work with new filters * Update calendar to new filter/sort structure * Fix activity module nav/search * Fix no-results message * Update file library filtering * Finalize user search * Allow filtering in drawer-collection * Handle cancelled responses semi-gracefully * Add loading start state timeout * Replace sort type in api * Last commit before redoing a bunch * Finish new visual style * Remove unused rounded prop from v-menu * Tweak sizing * Enough size tweaking for now * Count all filter operators instead of top * Fix archive casting * Fix api build * Add merge filters util * Split filter in user vs system * Fix export sidebar detail * Show field label on permissions configuration * Add migration for filter/sort * Use filters in insights
224 lines
5.2 KiB
TypeScript
224 lines
5.2 KiB
TypeScript
import { flatten, get, merge, set } from 'lodash';
|
|
import logger from '../logger';
|
|
import { Meta } from '../types';
|
|
import { Query, Aggregate, Filter } from '@directus/shared/types';
|
|
import { Accountability } from '@directus/shared/types';
|
|
import { parseFilter, deepMap } from '@directus/shared/utils';
|
|
|
|
export function sanitizeQuery(rawQuery: Record<string, any>, accountability?: Accountability | null): Query {
|
|
const query: Query = {};
|
|
|
|
if (rawQuery.limit !== undefined) {
|
|
const limit = sanitizeLimit(rawQuery.limit);
|
|
|
|
if (typeof limit === 'number') {
|
|
query.limit = limit;
|
|
}
|
|
}
|
|
|
|
if (rawQuery.fields) {
|
|
query.fields = sanitizeFields(rawQuery.fields);
|
|
}
|
|
|
|
if (rawQuery.groupBy) {
|
|
query.group = sanitizeFields(rawQuery.groupBy);
|
|
}
|
|
|
|
if (rawQuery.aggregate) {
|
|
query.aggregate = sanitizeAggregate(rawQuery.aggregate);
|
|
}
|
|
|
|
if (rawQuery.sort) {
|
|
query.sort = sanitizeSort(rawQuery.sort);
|
|
}
|
|
|
|
if (rawQuery.filter) {
|
|
query.filter = sanitizeFilter(rawQuery.filter, accountability || null);
|
|
}
|
|
|
|
if (rawQuery.offset) {
|
|
query.offset = sanitizeOffset(rawQuery.offset);
|
|
}
|
|
|
|
if (rawQuery.page) {
|
|
query.page = sanitizePage(rawQuery.page);
|
|
}
|
|
|
|
if (rawQuery.meta) {
|
|
(query as any).meta = sanitizeMeta(rawQuery.meta);
|
|
}
|
|
|
|
if (rawQuery.search && typeof rawQuery.search === 'string') {
|
|
query.search = rawQuery.search;
|
|
}
|
|
|
|
if (rawQuery.export) {
|
|
query.export = rawQuery.export as 'json' | 'csv';
|
|
}
|
|
|
|
if (rawQuery.deep as Record<string, any>) {
|
|
if (!query.deep) query.deep = {};
|
|
|
|
query.deep = sanitizeDeep(rawQuery.deep, accountability);
|
|
}
|
|
|
|
if (rawQuery.alias) {
|
|
query.alias = sanitizeAlias(rawQuery.alias);
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
function sanitizeFields(rawFields: any) {
|
|
if (!rawFields) return;
|
|
|
|
let fields: string[] = [];
|
|
|
|
if (typeof rawFields === 'string') fields = rawFields.split(',');
|
|
else if (Array.isArray(rawFields)) fields = rawFields as string[];
|
|
|
|
// Case where array item includes CSV (fe fields[]=id,name):
|
|
fields = flatten(fields.map((field) => (field.includes(',') ? field.split(',') : field)));
|
|
|
|
fields = fields.map((field) => field.trim());
|
|
|
|
return fields;
|
|
}
|
|
|
|
function sanitizeSort(rawSort: any) {
|
|
let fields: string[] = [];
|
|
|
|
if (typeof rawSort === 'string') fields = rawSort.split(',');
|
|
else if (Array.isArray(rawSort)) fields = rawSort as string[];
|
|
|
|
return fields;
|
|
}
|
|
|
|
function sanitizeAggregate(rawAggregate: any): Aggregate {
|
|
let aggregate: Aggregate = rawAggregate;
|
|
|
|
if (typeof rawAggregate === 'string') {
|
|
try {
|
|
aggregate = JSON.parse(rawAggregate);
|
|
} catch {
|
|
logger.warn('Invalid value passed for filter query parameter.');
|
|
}
|
|
}
|
|
|
|
for (const [operation, fields] of Object.entries(aggregate)) {
|
|
if (typeof fields === 'string') aggregate[operation as keyof Aggregate] = (fields as string).split(',');
|
|
else if (Array.isArray(fields)) aggregate[operation as keyof Aggregate] = fields as string[];
|
|
}
|
|
|
|
return aggregate as Aggregate;
|
|
}
|
|
|
|
function sanitizeFilter(rawFilter: any, accountability: Accountability | null) {
|
|
let filters: Filter = rawFilter;
|
|
|
|
if (typeof rawFilter === 'string') {
|
|
try {
|
|
filters = JSON.parse(rawFilter);
|
|
} catch {
|
|
logger.warn('Invalid value passed for filter query parameter.');
|
|
}
|
|
}
|
|
|
|
filters = deepMap(filters, (val) => {
|
|
try {
|
|
const parsed = JSON.parse(val);
|
|
|
|
if (typeof parsed == 'number' && !Number.isSafeInteger(parsed)) return val;
|
|
|
|
return parsed;
|
|
} catch {
|
|
return val;
|
|
}
|
|
});
|
|
|
|
filters = parseFilter(filters, accountability);
|
|
|
|
return filters;
|
|
}
|
|
|
|
function sanitizeLimit(rawLimit: any) {
|
|
if (rawLimit === undefined || rawLimit === null) return null;
|
|
return Number(rawLimit);
|
|
}
|
|
|
|
function sanitizeOffset(rawOffset: any) {
|
|
return Number(rawOffset);
|
|
}
|
|
|
|
function sanitizePage(rawPage: any) {
|
|
return Number(rawPage);
|
|
}
|
|
|
|
function sanitizeMeta(rawMeta: any) {
|
|
if (rawMeta === '*') {
|
|
return Object.values(Meta);
|
|
}
|
|
|
|
if (rawMeta.includes(',')) {
|
|
return rawMeta.split(',');
|
|
}
|
|
|
|
if (Array.isArray(rawMeta)) {
|
|
return rawMeta;
|
|
}
|
|
|
|
return [rawMeta];
|
|
}
|
|
|
|
function sanitizeDeep(deep: Record<string, any>, accountability?: Accountability | null) {
|
|
const result: Record<string, any> = {};
|
|
|
|
if (typeof deep === 'string') {
|
|
try {
|
|
deep = JSON.parse(deep);
|
|
} catch {
|
|
logger.warn('Invalid value passed for deep query parameter.');
|
|
}
|
|
}
|
|
|
|
parse(deep);
|
|
|
|
return result;
|
|
|
|
function parse(level: Record<string, any>, path: string[] = []) {
|
|
const parsedLevel: Record<string, any> = {};
|
|
|
|
for (const [key, value] of Object.entries(level)) {
|
|
if (!key) break;
|
|
|
|
if (key.startsWith('_')) {
|
|
// Sanitize query only accepts non-underscore-prefixed query options
|
|
const parsedSubQuery = sanitizeQuery({ [key.substring(1)]: value }, accountability);
|
|
// ...however we want to keep them for the nested structure of deep, otherwise there's no
|
|
// way of knowing when to keep nesting and when to stop
|
|
parsedLevel[key] = Object.values(parsedSubQuery)[0];
|
|
} else {
|
|
parse(value, [...path, key]);
|
|
}
|
|
}
|
|
|
|
if (Object.keys(parsedLevel).length > 0) {
|
|
set(result, path, merge({}, get(result, path, {}), parsedLevel));
|
|
}
|
|
}
|
|
}
|
|
|
|
function sanitizeAlias(rawAlias: any) {
|
|
let alias: Record<string, string> = rawAlias;
|
|
|
|
if (typeof rawAlias === 'string') {
|
|
try {
|
|
alias = JSON.parse(rawAlias);
|
|
} catch (err) {
|
|
logger.warn('Invalid value passed for alias query parameter.');
|
|
}
|
|
}
|
|
|
|
return alias;
|
|
}
|