From 89251faad7f4267abf1353931ae0a66fd88a4e57 Mon Sep 17 00:00:00 2001 From: William Luke Date: Tue, 23 Apr 2019 18:34:52 +0300 Subject: [PATCH] feat: Added Upload Middleware --- package.json | 2 + src/gqlMiddleware/index.ts | 10 ++ src/gqlMiddleware/upload.ts | 261 ++++++++++++++++++++++++++++++++++++ src/index.ts | 4 +- src/server.ts | 7 +- 5 files changed, 276 insertions(+), 8 deletions(-) create mode 100644 src/gqlMiddleware/index.ts create mode 100644 src/gqlMiddleware/upload.ts diff --git a/package.json b/package.json index c696a70..ff524ef 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "express": "^4.16.4", "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0", "graphql-middleware": "^3.0.2", + "graphql-shield": "^5.3.4", + "graphql-upload": "^8.0.5", "inquirer": "^6.3.1", "inquirer-path": "^1.0.0-beta5", "js-yaml": "^3.13.1", diff --git a/src/gqlMiddleware/index.ts b/src/gqlMiddleware/index.ts new file mode 100644 index 0000000..6d3d9bd --- /dev/null +++ b/src/gqlMiddleware/index.ts @@ -0,0 +1,10 @@ +import * as shield from "graphql-shield"; +import { upload } from "./upload"; +import { applyMiddleware } from "graphql-middleware"; + +const gqlMiddleware = { + upload, + shield, + applyMiddleware +} +export default gqlMiddleware \ No newline at end of file diff --git a/src/gqlMiddleware/upload.ts b/src/gqlMiddleware/upload.ts new file mode 100644 index 0000000..424bc2a --- /dev/null +++ b/src/gqlMiddleware/upload.ts @@ -0,0 +1,261 @@ +// https://github.com/maticzav/graphql-middleware-apollo-upload-server/blob/master/src/index.ts +import { + GraphQLResolveInfo, + GraphQLArgument, + GraphQLField, + getNamedType, + GraphQLType, +} from 'graphql' +import { GraphQLUpload } from 'graphql-upload' +import { IMiddlewareFunction } from 'graphql-middleware' + +// GraphQL ------------------------------------------------------------------- + +type Maybe = T | null + +/** + * + * @param info + * + * Returns GraphQLField type of the current resolver. + * + */ +function getResolverField( + info: GraphQLResolveInfo, +): GraphQLField { + const { fieldName, parentType } = info + const typeFields = parentType.getFields() + + return typeFields[fieldName] +} + +/** + * + * @param field + * + * Returns arguments that certain field accepts. + * + */ +function getFieldArguments( + field: GraphQLField, +): GraphQLArgument[] { + return field.args +} + +/** + * + * @param f + * @param xs + * + * Maps an array of functions and filters out the values + * which converted to null. + * + */ +function filterMap(f: (x: T) => Maybe, xs: T[]): U[] { + return xs.reduce((acc, x) => { + const res = f(x) + if (res !== null) { + return [res, ...acc] + } else { + return acc + } + }, [] as any) +} + +/** + * + * @param args + * @param arg + * + * Finds the value of argument from provided argument values and + * argument definition. + * + */ +function getArgumentValue(args: { [key: string]: any }, arg: GraphQLArgument) { + return args[arg.name] +} + +/** + * + * @param f + * @param info + * @param args + * + * Executes a funcition on all arguments of a particular field + * and filters out the results which returned null. + * + */ +export function filterMapFieldArguments( + f: (definition: GraphQLArgument, arg: any) => Maybe, + info: GraphQLResolveInfo, + args: { [key: string]: any }, +): T[] { + const field = getResolverField(info) + const fieldArguments = getFieldArguments(field) + + const fWithArguments = (arg: any) => f(arg, getArgumentValue(args, arg)) + + return filterMap(fWithArguments, fieldArguments) +} + +/** + * + * @param type + * @param x + * + * Checks whether a certain non-nullable, list or regular type + * is of predicted type. + * + */ +export function isGraphQLArgumentType( + type: GraphQLType, + argument: GraphQLArgument, +): boolean { + return getNamedType(argument.type).name === getNamedType(type).name +} + +// Upload -------------------------------------------------------------------- + +export interface IUpload { + stream: string + filename: string + mimetype: string + encoding: string +} + +interface IUploadArgument { + argumentName: string + upload: Promise | Promise[] +} + +interface IProcessedUploadArgument { + argumentName: string + upload: T | T[] +} + +declare type IUploadHandler = (upload: IUpload) => Promise + +interface IConfig { + uploadHandler: IUploadHandler +} + +/** + * + * @param def + * @param value + * + * Funciton used to identify GraphQLUpload arguments. + * + */ +export function uploadTypeIdentifier( + def: GraphQLArgument, + value: any, +): IUploadArgument | null { + if (isGraphQLArgumentType(GraphQLUpload, def)) { + return { + argumentName: def.name, + upload: value, + } + } else { + return null + } +} + +/** + * + * @param args + * @param info + * + * Function used to extract GraphQLUpload argumetns from a field. + * + */ +function extractUploadArguments( + args: { [key: string]: any }, + info: GraphQLResolveInfo, +): IUploadArgument[] { + return filterMapFieldArguments(uploadTypeIdentifier, info, args) +} + +/** + * + * @param arr + * + * Converts an array of processed uploads to one object which can + * be later used as arguments definition. + * + */ +export function normaliseArguments( + args: IProcessedUploadArgument[], +): { [key: string]: T } { + return args.reduce((acc, val) => { + return { + ...acc, + [val.argumentName]: val.upload, + } + }, {}) +} + +/** + * + * @param uploadHandler + * + * Function used to process file uploads. + * + */ +export function processor(uploadHandler: IUploadHandler) { + return function({ + argumentName, + upload, + }: IUploadArgument): Maybe>> | any { + if (Array.isArray(upload)) { + const uploads = upload.reduce((acc, file) => { + if (file !== undefined && file !== null && file.then) { + return [...acc, file.then(uploadHandler)] + } else { + return acc + } + }, [] as any) + + return Promise.all(uploads).then(res => ({ + argumentName: argumentName, + upload: res, + })) + } else if (upload !== undefined && upload !== null && upload.then) { + return upload.then(uploadHandler).then(res => ({ + argumentName: argumentName, + upload: res, + })) + } else { + return null + } + } +} + +/** + * + * @param config + * + * Exposed upload function which handles file upload in resolvers. + * Internally, it returns a middleware function which is later processed + * by GraphQL Middleware. + * The first step is to extract upload arguments using identifier + * which can be found above. + * Once we found all the GraphQLUpload arguments we check whether they + * carry a value or not and return a Promise to resolve them. + * Once Promises get processed we normalise outputs and merge them + * with old arguments to replace the old values with the new ones. + * + */ +export function upload({ uploadHandler }: IConfig): IMiddlewareFunction { + return async (resolve, parent, args, ctx, info) => { + const uploadArguments = extractUploadArguments(args, info) + const uploads = filterMap(processor(uploadHandler), uploadArguments) + + const uploaded = await Promise.all(uploads) + const argsUploaded = normaliseArguments(uploaded) + + const argsWithUploads = { ...args, ...argsUploaded } + + return resolve(parent, argsWithUploads, ctx, info) + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 499bf4a..f540f94 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,10 +5,10 @@ import * as Http from 'http' import * as logger from './logger' import { InputConfig as YogaConfig, MaybePromise, Yoga } from './types' import { injectCustomEnvironmentVariables } from './config' -import * as middleware from 'graphql-middleware' export * from 'nexus' export * from 'nexus-prisma' -export { ApolloServer, express, logger, middleware } +import gqlMiddleware from './gqlMiddleware'; +export { ApolloServer, express, logger, gqlMiddleware } injectCustomEnvironmentVariables() diff --git a/src/server.ts b/src/server.ts index 0ea886c..08ca22e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -38,14 +38,9 @@ export async function watch(env?: string): Promise { logger.info('Starting development server...') let info = importYogaConfig({ env }) - let filesToWatch = [path.join(info.projectDir, 'src', '*.ts')] + let filesToWatch = [path.join(info.projectDir, 'src', '**','*.ts')] logger.info(`Watching ${JSON.stringify(filesToWatch)}`) - if (info.prismaClientDir && info.datamodelInfoDir) { - filesToWatch.push(info.prismaClientDir) - filesToWatch.push(info.datamodelInfoDir) - } - let oldServer: any | undefined = await start(info, true) let filesToReloadBatched = [] as string[] const options = {