UNPKG

@directus/api

Version:

Directus is a real-time API and App dashboard for managing SQL database content

232 lines (231 loc) 8.11 kB
import { useEnv } from '@directus/env'; import { InvalidQueryError } from '@directus/errors'; import { parseFilter, parseJSON } from '@directus/utils'; import { flatten, get, isPlainObject, merge, set } from 'lodash-es'; import getDatabase from '../database/index.js'; import { useLogger } from '../logger/index.js'; import { fetchPolicies } from '../permissions/lib/fetch-policies.js'; import { contextHasDynamicVariables } from '../permissions/modules/process-ast/utils/context-has-dynamic-variables.js'; import { extractRequiredDynamicVariableContext } from '../permissions/utils/extract-required-dynamic-variable-context.js'; import { fetchDynamicVariableData } from '../permissions/utils/fetch-dynamic-variable-data.js'; import { Meta } from '../types/index.js'; /** * Sanitize the query parameters and parse them where necessary. */ export async function sanitizeQuery(rawQuery, schema, accountability) { const env = useEnv(); const query = {}; const hasMaxLimit = 'QUERY_LIMIT_MAX' in env && Number(env['QUERY_LIMIT_MAX']) >= 0 && !Number.isNaN(Number(env['QUERY_LIMIT_MAX'])) && Number.isFinite(Number(env['QUERY_LIMIT_MAX'])); if (rawQuery['limit'] !== undefined) { const limit = sanitizeLimit(rawQuery['limit']); if (typeof limit === 'number') { query.limit = limit === -1 && hasMaxLimit ? Number(env['QUERY_LIMIT_MAX']) : limit; } } else if (hasMaxLimit) { query.limit = Math.min(Number(env['QUERY_LIMIT_DEFAULT']), Number(env['QUERY_LIMIT_MAX'])); } 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 = await sanitizeFilter(rawQuery['filter'], schema, accountability || null); } if (rawQuery['offset'] !== undefined) { query.offset = sanitizeOffset(rawQuery['offset']); } if (rawQuery['page']) { query.page = sanitizePage(rawQuery['page']); } if (rawQuery['meta']) { query.meta = sanitizeMeta(rawQuery['meta']); } if (rawQuery['search'] && typeof rawQuery['search'] === 'string') { query.search = rawQuery['search']; } if (rawQuery['version']) { query.version = rawQuery['version']; // whether or not to merge the relational results query.versionRaw = Boolean('versionRaw' in rawQuery && (rawQuery['versionRaw'] === '' || rawQuery['versionRaw'] === 'true')); } if (rawQuery['export']) { query.export = rawQuery['export']; } if (rawQuery['deep']) { if (!query.deep) query.deep = {}; query.deep = await sanitizeDeep(rawQuery['deep'], schema, accountability); } if (rawQuery['alias']) { query.alias = sanitizeAlias(rawQuery['alias']); } if ('backlink' in rawQuery) { query.backlink = sanitizeBacklink(rawQuery['backlink']); } return query; } function sanitizeFields(rawFields) { if (!rawFields) return null; let fields = []; if (typeof rawFields === 'string') fields = rawFields.split(','); else if (Array.isArray(rawFields)) fields = rawFields; // 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) { let fields = []; if (typeof rawSort === 'string') fields = rawSort.split(','); else if (Array.isArray(rawSort)) fields = rawSort; fields = fields.map((field) => field.trim()); return fields; } function sanitizeAggregate(rawAggregate) { const logger = useLogger(); let aggregate = rawAggregate; if (typeof rawAggregate === 'string') { try { aggregate = parseJSON(rawAggregate); } catch { logger.warn('Invalid value passed for aggregate query parameter.'); } } for (const [operation, fields] of Object.entries(aggregate)) { if (typeof fields === 'string') aggregate[operation] = fields.split(','); else if (Array.isArray(fields)) aggregate[operation] = fields; } return aggregate; } async function sanitizeFilter(rawFilter, schema, accountability) { let filters = rawFilter; if (typeof filters === 'string') { try { filters = parseJSON(filters); } catch { throw new InvalidQueryError({ reason: 'Invalid JSON for filter object' }); } } try { let filterContext; if (accountability) { const dynamicVariableContext = extractRequiredDynamicVariableContext(filters); if (contextHasDynamicVariables(dynamicVariableContext)) { const context = { schema, knex: getDatabase(), }; const policies = await fetchPolicies(accountability, context); context.accountability = accountability; filterContext = await fetchDynamicVariableData({ dynamicVariableContext, accountability, policies, }, context); } } return parseFilter(filters, accountability, filterContext); } catch { throw new InvalidQueryError({ reason: 'Invalid filter object' }); } } function sanitizeLimit(rawLimit) { if (rawLimit === undefined || rawLimit === null) return null; return Number(rawLimit); } function sanitizeOffset(rawOffset) { return Number(rawOffset); } function sanitizePage(rawPage) { return Number(rawPage); } function sanitizeMeta(rawMeta) { if (rawMeta === '*') { return Object.values(Meta); } if (rawMeta.includes(',')) { return rawMeta.split(','); } if (Array.isArray(rawMeta)) { return rawMeta; } return [rawMeta]; } function sanitizeBacklink(rawBacklink) { return rawBacklink !== false && rawBacklink !== 'false'; } async function sanitizeDeep(deep, schema, accountability) { const logger = useLogger(); const result = {}; if (typeof deep === 'string') { try { deep = parseJSON(deep); } catch { logger.warn('Invalid value passed for deep query parameter.'); } } await parse(deep); return result; async function parse(level, path = []) { const subQuery = {}; const parsedLevel = {}; for (const [key, value] of Object.entries(level)) { if (!key) break; if (key.startsWith('_')) { // Collect all sub query parameters without the leading underscore subQuery[key.substring(1)] = value; } else if (isPlainObject(value)) { parse(value, [...path, key]); } } if (Object.keys(subQuery).length > 0) { // Sanitize the entire sub query const parsedSubQuery = await sanitizeQuery(subQuery, schema, accountability); for (const [parsedKey, parsedValue] of Object.entries(parsedSubQuery)) { parsedLevel[`_${parsedKey}`] = parsedValue; } } if (Object.keys(parsedLevel).length > 0) { set(result, path, merge({}, get(result, path, {}), parsedLevel)); } } } function sanitizeAlias(rawAlias) { const logger = useLogger(); let alias = rawAlias; if (typeof rawAlias === 'string') { try { alias = parseJSON(rawAlias); } catch { logger.warn('Invalid value passed for alias query parameter.'); } } return alias; }