Skip to content

Commit

Permalink
feat: Added Upload Middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
William Luke committed Apr 23, 2019
1 parent dc1c586 commit 89251fa
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 8 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions src/gqlMiddleware/index.ts
Original file line number Diff line number Diff line change
@@ -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
261 changes: 261 additions & 0 deletions src/gqlMiddleware/upload.ts
Original file line number Diff line number Diff line change
@@ -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> = T | null

/**
*
* @param info
*
* Returns GraphQLField type of the current resolver.
*
*/
function getResolverField(
info: GraphQLResolveInfo,
): GraphQLField<any, any, { [key: string]: any }> {
const { fieldName, parentType } = info
const typeFields = parentType.getFields()

return typeFields[fieldName]
}

/**
*
* @param field
*
* Returns arguments that certain field accepts.
*
*/
function getFieldArguments<TSource, TContext, TArgs>(
field: GraphQLField<TSource, TContext, TArgs>,
): GraphQLArgument[] {
return field.args
}

/**
*
* @param f
* @param xs
*
* Maps an array of functions and filters out the values
* which converted to null.
*
*/
function filterMap<T, U>(f: (x: T) => Maybe<U>, 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<T>(
f: (definition: GraphQLArgument, arg: any) => Maybe<T>,
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<IUpload> | Promise<IUpload>[]
}

interface IProcessedUploadArgument<T> {
argumentName: string
upload: T | T[]
}

declare type IUploadHandler<T> = (upload: IUpload) => Promise<T>

interface IConfig<T> {
uploadHandler: IUploadHandler<T>
}

/**
*
* @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<T>(
args: IProcessedUploadArgument<T>[],
): { [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<T>(uploadHandler: IUploadHandler<T>) {
return function({
argumentName,
upload,
}: IUploadArgument): Maybe<Promise<IProcessedUploadArgument<T>>> | 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<T>({ uploadHandler }: IConfig<T>): 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)
}
}
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 1 addition & 6 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,9 @@ export async function watch(env?: string): Promise<void> {
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 = {
Expand Down

0 comments on commit 89251fa

Please sign in to comment.