mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
@@ -86,10 +86,12 @@
|
||||
"exif-reader": "^1.0.3",
|
||||
"express": "^4.17.1",
|
||||
"express-async-handler": "^1.1.4",
|
||||
"express-graphql": "^0.11.0",
|
||||
"express-pino-logger": "^5.0.0",
|
||||
"express-session": "^1.17.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"grant": "^5.3.0",
|
||||
"graphql": "^15.3.0",
|
||||
"icc": "^2.0.0",
|
||||
"inquirer": "^7.3.3",
|
||||
"joi": "^17.1.1",
|
||||
|
||||
@@ -34,6 +34,7 @@ import settingsRouter from './controllers/settings';
|
||||
import usersRouter from './controllers/users';
|
||||
import utilsRouter from './controllers/utils';
|
||||
import webhooksRouter from './controllers/webhooks';
|
||||
import graphqlRouter from './controllers/graphql';
|
||||
|
||||
import notFoundHandler from './controllers/not-found';
|
||||
import sanitizeQuery from './middleware/sanitize-query';
|
||||
@@ -98,6 +99,8 @@ app.use('/auth', authRouter, respond);
|
||||
app.use(authenticate);
|
||||
app.use(cache);
|
||||
|
||||
app.use('/graphql', graphqlRouter);
|
||||
|
||||
app.use('/activity', activityRouter, respond);
|
||||
app.use('/assets', assetsRouter, respond);
|
||||
app.use('/collections', collectionsRouter, respond);
|
||||
|
||||
16
api/src/controllers/graphql.ts
Normal file
16
api/src/controllers/graphql.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Router } from 'express';
|
||||
import { graphqlHTTP } from 'express-graphql';
|
||||
import { GraphQLService } from '../services';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(asyncHandler(async (req, res) => {
|
||||
const service = new GraphQLService({ accountability: req.accountability });
|
||||
const schema = await service.getSchema();
|
||||
|
||||
graphqlHTTP({ schema, graphiql: true })(req, res);
|
||||
}));
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -29,6 +29,7 @@ data:
|
||||
icon: supervised_user_circle
|
||||
- collection: directus_sessions
|
||||
- collection: directus_settings
|
||||
singleton: true
|
||||
- collection: directus_users
|
||||
archive_field: status
|
||||
archive_value: archived
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
389
api/src/services/graphql.ts
Normal file
389
api/src/services/graphql.ts
Normal file
@@ -0,0 +1,389 @@
|
||||
import Knex from 'knex';
|
||||
import database from '../database';
|
||||
import { AbstractServiceOptions, Accountability, Collection, Field, Relation, Query, AbstractService } from '../types';
|
||||
import { GraphQLString, GraphQLSchema, GraphQLObjectType, GraphQLList, GraphQLResolveInfo, GraphQLInputObjectType, ObjectFieldNode, GraphQLID, ValueNode, FieldNode, GraphQLFieldConfigMap, GraphQLInt, IntValueNode, StringValueNode, BooleanValueNode, ArgumentNode, GraphQLScalarType, GraphQLBoolean, ObjectValueNode } from 'graphql';
|
||||
import { getGraphQLType } from '../utils/get-graphql-type';
|
||||
import { RelationsService } from './relations';
|
||||
import { ItemsService } from './items';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { sanitizeQuery } from '../utils/sanitize-query';
|
||||
|
||||
import { ActivityService } from './activity';
|
||||
import { CollectionsService } from './collections';
|
||||
import { FieldsService } from './fields';
|
||||
import { FilesService } from './files';
|
||||
import { FoldersService } from './folders';
|
||||
import { PermissionsService } from './permissions';
|
||||
import { PresetsService } from './presets';
|
||||
import { RevisionsService } from './revisions';
|
||||
import { RolesService } from './roles';
|
||||
import { SettingsService } from './settings';
|
||||
import { UsersService } from './users';
|
||||
import { WebhooksService } from './webhooks';
|
||||
|
||||
export class GraphQLService {
|
||||
accountability: Accountability | null;
|
||||
knex: Knex;
|
||||
fieldsService: FieldsService;
|
||||
collectionsService: CollectionsService;
|
||||
relationsService: RelationsService;
|
||||
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
this.accountability = options?.accountability || null;
|
||||
this.knex = options?.knex || database;
|
||||
this.fieldsService = new FieldsService(options);
|
||||
this.collectionsService = new CollectionsService(options);
|
||||
this.relationsService = new RelationsService({ knex: this.knex });
|
||||
}
|
||||
|
||||
args = {
|
||||
sort: {
|
||||
type: GraphQLString
|
||||
},
|
||||
limit: {
|
||||
type: GraphQLInt,
|
||||
},
|
||||
offset: {
|
||||
type: GraphQLInt,
|
||||
},
|
||||
page: {
|
||||
type: GraphQLInt,
|
||||
},
|
||||
search: {
|
||||
type: GraphQLString,
|
||||
}
|
||||
}
|
||||
|
||||
async getSchema() {
|
||||
const collectionsInSystem = await this.collectionsService.readByQuery();
|
||||
const fieldsInSystem = await this.fieldsService.readAll();
|
||||
const relationsInSystem = await this.relationsService.readByQuery({}) as Relation[];
|
||||
|
||||
const schema = this.getGraphQLSchema(collectionsInSystem, fieldsInSystem, relationsInSystem);
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
getGraphQLSchema(collections: Collection[], fields: Field[], relations: Relation[]) {
|
||||
const filterTypes = this.getFilterArgs(collections, fields, relations);
|
||||
const schema: any = { items: {} };
|
||||
|
||||
for (const collection of collections) {
|
||||
const systemCollection = collection.collection.startsWith('directus_');
|
||||
|
||||
const schemaSection: any = {
|
||||
type: new GraphQLObjectType({
|
||||
name: collection.collection,
|
||||
description: collection.meta?.note,
|
||||
fields: () => {
|
||||
const fieldsObject: GraphQLFieldConfigMap<any, any> = {};
|
||||
const fieldsInCollection = fields.filter((field) => field.collection === collection.collection);
|
||||
|
||||
for (const field of fieldsInCollection) {
|
||||
const relationForField = relations.find((relation) => {
|
||||
return relation.many_collection === collection.collection && relation.many_field === field.field ||
|
||||
relation.one_collection === collection.collection && relation.one_field === field.field;
|
||||
});
|
||||
|
||||
if (relationForField) {
|
||||
const isM2O = relationForField.many_collection === collection.collection && relationForField.many_field === field.field;
|
||||
|
||||
if (isM2O) {
|
||||
const relatedIsSystem = relationForField.one_collection.startsWith('directus_');
|
||||
const relatedType = relatedIsSystem ? schema[relationForField.one_collection.substring(9)].type : schema.items[relationForField.one_collection].type;
|
||||
|
||||
fieldsObject[field.field] = {
|
||||
type: relatedType,
|
||||
}
|
||||
} else {
|
||||
const relatedIsSystem = relationForField.many_collection.startsWith('directus_');
|
||||
const relatedType = relatedIsSystem ? schema[relationForField.many_collection.substring(9)].type : schema.items[relationForField.many_collection].type;
|
||||
|
||||
fieldsObject[field.field] = {
|
||||
type: new GraphQLList(relatedType),
|
||||
args: {
|
||||
...this.args,
|
||||
filter: {
|
||||
type: filterTypes[relationForField.many_collection],
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fieldsObject[field.field] = {
|
||||
type: field.schema?.is_primary_key ? GraphQLID : getGraphQLType(field.type),
|
||||
}
|
||||
}
|
||||
|
||||
fieldsObject[field.field].description = field.meta?.note;
|
||||
}
|
||||
|
||||
return fieldsObject;
|
||||
},
|
||||
}),
|
||||
resolve: (source: any, args: any, context: any, info: GraphQLResolveInfo) => this.resolve(info),
|
||||
args: {
|
||||
...this.args,
|
||||
filter: {
|
||||
name: `${collection.collection}_filter`,
|
||||
type: filterTypes[collection.collection],
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (systemCollection) {
|
||||
schema[collection.collection.substring(9)] = schemaSection;
|
||||
} else {
|
||||
schema.items[collection.collection] = schemaSection;
|
||||
}
|
||||
}
|
||||
|
||||
const schemaWithLists = cloneDeep(schema);
|
||||
|
||||
for (const collection of collections) {
|
||||
if (collection.meta?.singleton !== true) {
|
||||
const systemCollection = collection.collection.startsWith('directus_');
|
||||
|
||||
if (systemCollection) {
|
||||
schemaWithLists[collection.collection.substring(9)].type = new GraphQLList(schemaWithLists[collection.collection.substring(9)].type);
|
||||
} else {
|
||||
schemaWithLists.items[collection.collection].type = new GraphQLList(schemaWithLists.items[collection.collection].type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
schemaWithLists.items = {
|
||||
type: new GraphQLObjectType({
|
||||
name: 'items',
|
||||
fields: schemaWithLists.items,
|
||||
}),
|
||||
resolve: () => ({}),
|
||||
};
|
||||
|
||||
return new GraphQLSchema({
|
||||
query: new GraphQLObjectType({
|
||||
name: 'Directus',
|
||||
fields: schemaWithLists,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
getFilterArgs(collections: Collection[], fields: Field[], relations: Relation[]) {
|
||||
const filterTypes: any = {};
|
||||
|
||||
for (const collection of collections) {
|
||||
filterTypes[collection.collection] = new GraphQLInputObjectType({
|
||||
name: `${collection.collection}_filter`,
|
||||
fields: () => {
|
||||
const filterFields: any = {
|
||||
_and: {
|
||||
type: new GraphQLList(filterTypes[collection.collection])
|
||||
},
|
||||
_or: {
|
||||
type: new GraphQLList(filterTypes[collection.collection])
|
||||
},
|
||||
};
|
||||
|
||||
const fieldsInCollection = fields.filter((field) => field.collection === collection.collection);
|
||||
|
||||
for (const field of fieldsInCollection) {
|
||||
const relationForField = relations.find((relation) => {
|
||||
return relation.many_collection === collection.collection && relation.many_field === field.field ||
|
||||
relation.one_collection === collection.collection && relation.one_field === field.field;
|
||||
});
|
||||
|
||||
if (relationForField) {
|
||||
const isM2O = relationForField.many_collection === collection.collection && relationForField.many_field === field.field;
|
||||
|
||||
if (isM2O) {
|
||||
const relatedType = filterTypes[relationForField.one_collection];
|
||||
|
||||
filterFields[field.field] = {
|
||||
type: relatedType,
|
||||
}
|
||||
} else {
|
||||
const relatedType = filterTypes[relationForField.many_collection];
|
||||
|
||||
filterFields[field.field] = {
|
||||
type: relatedType
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const fieldType = field.schema?.is_primary_key ? GraphQLID : getGraphQLType(field.type);
|
||||
|
||||
filterFields[field.field] = {
|
||||
type: new GraphQLInputObjectType({
|
||||
name: `${collection.collection}_${field.field}_filter_operators`,
|
||||
fields: {
|
||||
/* @todo make this a little smarter by only including filters that work with current type */
|
||||
_eq: {
|
||||
type: fieldType,
|
||||
},
|
||||
_neq: {
|
||||
type: fieldType
|
||||
},
|
||||
_contains: {
|
||||
type: fieldType,
|
||||
},
|
||||
_ncontains: {
|
||||
type: fieldType,
|
||||
},
|
||||
_in: {
|
||||
type: new GraphQLList(fieldType),
|
||||
},
|
||||
_nin: {
|
||||
type: new GraphQLList(fieldType),
|
||||
},
|
||||
_gt: {
|
||||
type: fieldType,
|
||||
},
|
||||
_gte: {
|
||||
type: fieldType,
|
||||
},
|
||||
_lt: {
|
||||
type: fieldType,
|
||||
},
|
||||
_lte: {
|
||||
type: fieldType,
|
||||
},
|
||||
_null: {
|
||||
type: GraphQLBoolean,
|
||||
},
|
||||
_nnull: {
|
||||
type: GraphQLBoolean,
|
||||
},
|
||||
_empty: {
|
||||
type: GraphQLBoolean,
|
||||
},
|
||||
_nempty: {
|
||||
type: GraphQLBoolean,
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filterFields;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return filterTypes
|
||||
}
|
||||
|
||||
async resolve(info: GraphQLResolveInfo) {
|
||||
const systemField = info.path.prev?.key !== 'items';
|
||||
|
||||
const collection = systemField ? `directus_${info.fieldName}` : info.fieldName;
|
||||
const selections = info.fieldNodes[0]?.selectionSet?.selections?.filter((node) => node.kind === 'Field') as FieldNode[] | undefined;
|
||||
if (!selections) return null;
|
||||
|
||||
return await this.getData(collection, selections, info.fieldNodes[0].arguments);
|
||||
}
|
||||
|
||||
async getData(collection: string, selections: FieldNode[], argsArray?: readonly ArgumentNode[]) {
|
||||
const args: Record<string, any> = this.parseArgs(argsArray);
|
||||
|
||||
const query: Query = sanitizeQuery(args, this.accountability);
|
||||
|
||||
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> = this.parseArgs(selection.arguments);
|
||||
query.deep[current] = sanitizeQuery(args, this.accountability);
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
query.fields = parseFields(selections.filter((selection) => selection.kind === 'Field') as FieldNode[]);
|
||||
|
||||
let service: ItemsService;
|
||||
|
||||
switch (collection) {
|
||||
case 'directus_activity':
|
||||
service = new ActivityService({ knex: this.knex, accountability: this.accountability });
|
||||
// case 'directus_collections':
|
||||
// service = new CollectionsService({ knex: this.knex, accountability: this.accountability });
|
||||
// case 'directus_fields':
|
||||
// service = new FieldsService({ knex: this.knex, accountability: this.accountability });
|
||||
case 'directus_files':
|
||||
service = new FilesService({ knex: this.knex, accountability: this.accountability });
|
||||
case 'directus_folders':
|
||||
service = new FoldersService({ knex: this.knex, accountability: this.accountability });
|
||||
case 'directus_folders':
|
||||
service = new FoldersService({ knex: this.knex, accountability: this.accountability });
|
||||
case 'directus_permissions':
|
||||
service = new PermissionsService({ knex: this.knex, accountability: this.accountability });
|
||||
case 'directus_presets':
|
||||
service = new PresetsService({ knex: this.knex, accountability: this.accountability });
|
||||
case 'directus_relations':
|
||||
service = new RelationsService({ knex: this.knex, accountability: this.accountability });
|
||||
case 'directus_revisions':
|
||||
service = new RevisionsService({ knex: this.knex, accountability: this.accountability });
|
||||
case 'directus_roles':
|
||||
service = new RolesService({ knex: this.knex, accountability: this.accountability });
|
||||
case 'directus_settings':
|
||||
service = new SettingsService({ knex: this.knex, accountability: this.accountability });
|
||||
case 'directus_users':
|
||||
service = new UsersService({ knex: this.knex, accountability: this.accountability });
|
||||
case 'directus_webhooks':
|
||||
service = new WebhooksService({ knex: this.knex, accountability: this.accountability });
|
||||
default:
|
||||
service = new ItemsService(collection, { knex: this.knex, accountability: this.accountability });
|
||||
}
|
||||
|
||||
const collectionInfo = await this.knex.select('singleton').from('directus_collections').where({ collection: collection }).first();
|
||||
const result = collectionInfo?.singleton === true ? await service.readSingleton(query) : await service.readByQuery(query);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
parseArgs(args?: readonly ArgumentNode[] | readonly ObjectFieldNode[]): Record<string, any> {
|
||||
if (!args) return {};
|
||||
|
||||
const parseObjectValue = (arg: ObjectValueNode) => {
|
||||
return this.parseArgs(arg.fields);
|
||||
}
|
||||
|
||||
const argsObject: any = {};
|
||||
|
||||
for (const argument of args) {
|
||||
if (argument.value.kind === 'ObjectValue') {
|
||||
argsObject[argument.name.value] = parseObjectValue(argument.value);
|
||||
} else if (argument.value.kind === 'ListValue') {
|
||||
const values: any = [];
|
||||
|
||||
for (const valueNode of argument.value.values) {
|
||||
if (valueNode.kind === 'ObjectValue') {
|
||||
values.push(this.parseArgs(valueNode.fields));
|
||||
} else {
|
||||
values.push((valueNode as any).value);
|
||||
}
|
||||
}
|
||||
|
||||
argsObject[argument.name.value] = values;
|
||||
} else {
|
||||
argsObject[argument.name.value] = (argument.value as IntValueNode | StringValueNode | BooleanValueNode).value;
|
||||
}
|
||||
}
|
||||
|
||||
return argsObject;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export * from './collections';
|
||||
export * from './fields';
|
||||
export * from './files';
|
||||
export * from './folders';
|
||||
export * from './graphql';
|
||||
export * from './items';
|
||||
export * from './meta';
|
||||
export * from './payload';
|
||||
|
||||
@@ -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;
|
||||
|
||||
17
api/src/utils/get-graphql-type.ts
Normal file
17
api/src/utils/get-graphql-type.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { GraphQLBoolean, GraphQLFloat, GraphQLInt, GraphQLString } from 'graphql';
|
||||
import { types } from '../types';
|
||||
|
||||
export function getGraphQLType(localType: typeof types[number]) {
|
||||
switch (localType) {
|
||||
case 'boolean':
|
||||
return GraphQLBoolean;
|
||||
case 'bigInteger':
|
||||
case 'integer':
|
||||
return GraphQLInt;
|
||||
case 'decimal':
|
||||
case 'float':
|
||||
return GraphQLFloat;
|
||||
default:
|
||||
return GraphQLString;
|
||||
}
|
||||
}
|
||||
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];
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import Joi from 'joi';
|
||||
|
||||
const schema = Joi.alternatives().try(
|
||||
Joi.object({
|
||||
name: Joi.string().required(),
|
||||
age: Joi.number()
|
||||
}),
|
||||
Joi.string(),
|
||||
).match('all');
|
||||
|
||||
const value = {
|
||||
age: 25
|
||||
};
|
||||
|
||||
const { error } = schema.validate(value);
|
||||
|
||||
console.log(JSON.stringify(error, null, 2));
|
||||
Reference in New Issue
Block a user