Allow deep query

This commit is contained in:
rijkvanzanten
2020-09-30 22:22:02 -04:00
parent e3dc72b779
commit 537fe8f8dd
4 changed files with 207 additions and 166 deletions

View File

@@ -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];
}

View File

@@ -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;
}
}));
}
}
}

View File

@@ -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;

View 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];
}