mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Allow deep query
This commit is contained in:
@@ -4,15 +4,13 @@
|
||||
*/
|
||||
|
||||
import { RequestHandler } from 'express';
|
||||
import { Accountability, Query, Sort, Filter, Meta } from '../types';
|
||||
import logger from '../logger';
|
||||
import { parseFilter } from '../utils/parse-filter';
|
||||
import { sanitizeQuery } from '../utils/sanitize-query';
|
||||
|
||||
const sanitizeQuery: RequestHandler = (req, res, next) => {
|
||||
const sanitizeQueryMiddleware: RequestHandler = (req, res, next) => {
|
||||
req.sanitizedQuery = {};
|
||||
if (!req.query) return;
|
||||
|
||||
req.sanitizedQuery = sanitize(
|
||||
req.sanitizedQuery = sanitizeQuery(
|
||||
{
|
||||
fields: req.query.fields || '*',
|
||||
...req.query
|
||||
@@ -25,143 +23,5 @@ const sanitizeQuery: RequestHandler = (req, res, next) => {
|
||||
return next();
|
||||
};
|
||||
|
||||
function sanitize(rawQuery: Record<string, any>, accountability: Accountability | null) {
|
||||
const query: Query = {};
|
||||
export default sanitizeQueryMiddleware;
|
||||
|
||||
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.sort) {
|
||||
query.sort = sanitizeSort(rawQuery.sort);
|
||||
}
|
||||
|
||||
if (rawQuery.filter) {
|
||||
query.filter = sanitizeFilter(rawQuery.filter, accountability || null);
|
||||
}
|
||||
|
||||
if (rawQuery.limit == '-1') {
|
||||
delete query.limit;
|
||||
}
|
||||
|
||||
if (rawQuery.offset) {
|
||||
query.offset = sanitizeOffset(rawQuery.offset);
|
||||
}
|
||||
|
||||
if (rawQuery.page) {
|
||||
query.page = sanitizePage(rawQuery.page);
|
||||
}
|
||||
|
||||
if (rawQuery.single) {
|
||||
query.single = sanitizeSingle(rawQuery.single);
|
||||
}
|
||||
|
||||
if (rawQuery.meta) {
|
||||
query.meta = sanitizeMeta(rawQuery.meta);
|
||||
}
|
||||
|
||||
if (rawQuery.search && typeof rawQuery.search === 'string') {
|
||||
query.search = rawQuery.search;
|
||||
}
|
||||
|
||||
if (
|
||||
rawQuery.export &&
|
||||
typeof rawQuery.export === 'string' &&
|
||||
['json', 'csv'].includes(rawQuery.export)
|
||||
) {
|
||||
query.export = rawQuery.export as 'json' | 'csv';
|
||||
}
|
||||
|
||||
if (rawQuery.deep as Record<string, any>) {
|
||||
if (!query.deep) query.deep = {};
|
||||
|
||||
for (const [field, deepRawQuery] of Object.entries(rawQuery.deep)) {
|
||||
query.deep[field] = sanitize(deepRawQuery as any, accountability);
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
export default sanitizeQuery;
|
||||
|
||||
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[];
|
||||
|
||||
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.map((field) => {
|
||||
const order = field.startsWith('-') ? 'desc' : 'asc';
|
||||
const column = field.startsWith('-') ? field.substring(1) : field;
|
||||
return { column, order } as Sort;
|
||||
});
|
||||
}
|
||||
|
||||
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 = 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 sanitizeSingle(rawSingle: any) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function sanitizeMeta(rawMeta: any) {
|
||||
if (rawMeta === '*') {
|
||||
return Object.values(Meta);
|
||||
}
|
||||
|
||||
if (rawMeta.includes(',')) {
|
||||
return rawMeta.split(',');
|
||||
}
|
||||
|
||||
if (Array.isArray(rawMeta)) {
|
||||
return rawMeta;
|
||||
}
|
||||
|
||||
return [rawMeta];
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import Knex from 'knex';
|
||||
import database from '../database';
|
||||
import { AbstractServiceOptions, Accountability, Collection, Field, Relation, Query } from '../types';
|
||||
import { GraphQLSchema, GraphQLObjectType, GraphQLList, GraphQLScalarType, GraphQLResolveInfo, GraphQLID, FieldNode, GraphQLFieldConfigMap, GraphQLObjectTypeConfig } from 'graphql';
|
||||
import { GraphQLString, GraphQLSchema, GraphQLObjectType, GraphQLList, GraphQLResolveInfo, GraphQLID, FieldNode, GraphQLFieldConfigMap, GraphQLInt, IntValueNode, StringValueNode, BooleanValueNode, } from 'graphql';
|
||||
import { CollectionsService } from './collections';
|
||||
import { FieldsService } from './fields';
|
||||
import { getGraphQLType } from '../utils/get-graphql-type';
|
||||
import { RelationsService } from './relations';
|
||||
import { ItemsService } from './items';
|
||||
import { clone, cloneDeep, flatten } from 'lodash';
|
||||
import { cloneDeep, flatten } from 'lodash';
|
||||
import { sanitizeQuery } from '../utils/sanitize-query';
|
||||
|
||||
export class GraphQLService {
|
||||
accountability: Accountability | null;
|
||||
@@ -24,6 +25,28 @@ export class GraphQLService {
|
||||
this.relationsService = new RelationsService({ knex: this.knex });
|
||||
}
|
||||
|
||||
args = {
|
||||
sort: {
|
||||
type: GraphQLString
|
||||
},
|
||||
limit: {
|
||||
type: GraphQLInt,
|
||||
},
|
||||
// filter: {
|
||||
// type: GraphQL,
|
||||
// },
|
||||
// @TODO research "any object input" arg type
|
||||
offset: {
|
||||
type: GraphQLInt,
|
||||
},
|
||||
page: {
|
||||
type: GraphQLInt,
|
||||
},
|
||||
search: {
|
||||
type: GraphQLString,
|
||||
}
|
||||
}
|
||||
|
||||
async getSchema() {
|
||||
const collectionsInSystem = await this.collectionsService.readByQuery();
|
||||
const fieldsInSystem = await this.fieldsService.readAll();
|
||||
@@ -61,6 +84,7 @@ export class GraphQLService {
|
||||
} else {
|
||||
fieldsObject[field.field] = {
|
||||
type: new GraphQLList(schema[relationForField.many_collection].type),
|
||||
args: this.args,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -73,14 +97,15 @@ export class GraphQLService {
|
||||
return fieldsObject;
|
||||
},
|
||||
}),
|
||||
resolve: (source: any, args: any, context: any, info: GraphQLResolveInfo) => this.resolve(info)
|
||||
args: this.args,
|
||||
resolve: (source: any, args: any, context: any, info: GraphQLResolveInfo) => this.resolve(info, args)
|
||||
}
|
||||
}
|
||||
|
||||
const schemaWithLists = cloneDeep(schema);
|
||||
|
||||
for (const collection of collections) {
|
||||
if (collection.meta?.single !== true) {
|
||||
if (collection.meta?.singleton !== true) {
|
||||
schemaWithLists[collection.collection].type = new GraphQLList(schemaWithLists[collection.collection].type);
|
||||
}
|
||||
}
|
||||
@@ -93,32 +118,46 @@ export class GraphQLService {
|
||||
});
|
||||
}
|
||||
|
||||
async resolve(info: GraphQLResolveInfo) {
|
||||
async resolve(info: GraphQLResolveInfo, args: Record<string, any>) {
|
||||
const collection = info.fieldName;
|
||||
const query: Query = {};
|
||||
const query: Query = sanitizeQuery(args, this.accountability);
|
||||
|
||||
const selections = info.fieldNodes[0]?.selectionSet?.selections;
|
||||
if (!selections) return null;
|
||||
|
||||
query.fields = getFields(selections.filter((selection) => selection.kind === 'Field') as FieldNode[]);
|
||||
const parseFields = (selections: FieldNode[], parent?: string): string[] => {
|
||||
const fields: string[] = [];
|
||||
|
||||
for (const selection of selections) {
|
||||
const current = parent ? `${parent}.${selection.name.value}` : selection.name.value;
|
||||
|
||||
if (selection.selectionSet === undefined) {
|
||||
fields.push(current);
|
||||
} else {
|
||||
const children = parseFields(selection.selectionSet.selections.filter((selection) => selection.kind === 'Field') as FieldNode[], current);
|
||||
fields.push(...children);
|
||||
}
|
||||
|
||||
if (selection.arguments && selection.arguments.length > 0) {
|
||||
if (!query.deep) query.deep = {};
|
||||
const args: Record<string, any> = {};
|
||||
|
||||
for (const argument of selection.arguments) {
|
||||
args[argument.name.value] = (argument.value as IntValueNode | StringValueNode | BooleanValueNode).value;
|
||||
}
|
||||
|
||||
query.deep[current] = sanitizeQuery(args, this.accountability);
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
query.fields = parseFields(selections.filter((selection) => selection.kind === 'Field') as FieldNode[]);
|
||||
|
||||
const service = new ItemsService(collection, { knex: this.knex, accountability: this.accountability });
|
||||
const result = await service.readByQuery(query);
|
||||
|
||||
return result;
|
||||
|
||||
function getFields(selections: FieldNode[], parent?: string): string[] {
|
||||
return flatten(selections!
|
||||
.map((selection) => {
|
||||
const current = parent ? `${parent}.${selection.name.value}` : selection.name.value;
|
||||
|
||||
if (selection.selectionSet === undefined) {
|
||||
return current;
|
||||
} else {
|
||||
const children = getFields(selection.selectionSet.selections.filter((selection) => selection.kind === 'Field') as FieldNode[], current);
|
||||
return children;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export type Collection = {
|
||||
collection: string;
|
||||
note: string | null;
|
||||
hidden: boolean;
|
||||
single: boolean;
|
||||
singleton: boolean;
|
||||
icon: string | null;
|
||||
translation: Record<string, string>;
|
||||
} | null;
|
||||
|
||||
142
api/src/utils/sanitize-query.ts
Normal file
142
api/src/utils/sanitize-query.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
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) {
|
||||
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.sort) {
|
||||
query.sort = sanitizeSort(rawQuery.sort);
|
||||
}
|
||||
|
||||
if (rawQuery.filter) {
|
||||
query.filter = sanitizeFilter(rawQuery.filter, accountability || null);
|
||||
}
|
||||
|
||||
if (rawQuery.limit == '-1') {
|
||||
delete query.limit;
|
||||
}
|
||||
|
||||
if (rawQuery.offset) {
|
||||
query.offset = sanitizeOffset(rawQuery.offset);
|
||||
}
|
||||
|
||||
if (rawQuery.page) {
|
||||
query.page = sanitizePage(rawQuery.page);
|
||||
}
|
||||
|
||||
if (rawQuery.single) {
|
||||
query.single = sanitizeSingle(rawQuery.single);
|
||||
}
|
||||
|
||||
if (rawQuery.meta) {
|
||||
query.meta = sanitizeMeta(rawQuery.meta);
|
||||
}
|
||||
|
||||
if (rawQuery.search && typeof rawQuery.search === 'string') {
|
||||
query.search = rawQuery.search;
|
||||
}
|
||||
|
||||
if (
|
||||
rawQuery.export &&
|
||||
typeof rawQuery.export === 'string' &&
|
||||
['json', 'csv'].includes(rawQuery.export)
|
||||
) {
|
||||
query.export = rawQuery.export as 'json' | 'csv';
|
||||
}
|
||||
|
||||
if (rawQuery.deep as Record<string, any>) {
|
||||
if (!query.deep) query.deep = {};
|
||||
|
||||
for (const [field, deepRawQuery] of Object.entries(rawQuery.deep)) {
|
||||
query.deep[field] = sanitizeQuery(deepRawQuery as any, accountability);
|
||||
}
|
||||
}
|
||||
|
||||
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[];
|
||||
|
||||
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.map((field) => {
|
||||
const order = field.startsWith('-') ? 'desc' : 'asc';
|
||||
const column = field.startsWith('-') ? field.substring(1) : field;
|
||||
return { column, order } as Sort;
|
||||
});
|
||||
}
|
||||
|
||||
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 = 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 sanitizeSingle(rawSingle: any) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function sanitizeMeta(rawMeta: any) {
|
||||
if (rawMeta === '*') {
|
||||
return Object.values(Meta);
|
||||
}
|
||||
|
||||
if (rawMeta.includes(',')) {
|
||||
return rawMeta.split(',');
|
||||
}
|
||||
|
||||
if (Array.isArray(rawMeta)) {
|
||||
return rawMeta;
|
||||
}
|
||||
|
||||
return [rawMeta];
|
||||
}
|
||||
Reference in New Issue
Block a user