1- import { IMiddleware } from 'graphql-middleware'
1+ import {
2+ GraphQLResolveInfo ,
3+ GraphQLArgument ,
4+ GraphQLField ,
5+ getNamedType ,
6+ } from 'graphql'
27import { GraphQLUpload } from 'apollo-upload-server'
3- import { GraphQLResolveInfo , GraphQLArgument , GraphQLField } from 'graphql'
8+ import { IMiddlewareFunction } from 'graphql-middleware '
49
5- // GraphQL
10+ // GraphQL -------------------------------------------------------------------
611
712type Maybe < T > = T | null
813
14+ /**
15+ *
16+ * @param info
17+ *
18+ * Returns GraphQLField type of the current resolver.
19+ *
20+ */
921function getResolverField (
1022 info : GraphQLResolveInfo ,
1123) : GraphQLField < any , any , { [ key : string ] : any } > {
@@ -15,12 +27,28 @@ function getResolverField(
1527 return typeFields [ fieldName ]
1628}
1729
30+ /**
31+ *
32+ * @param field
33+ *
34+ * Returns arguments that certain field accepts.
35+ *
36+ */
1837function getFieldArguments < TSource , TContext , TArgs > (
1938 field : GraphQLField < TSource , TContext , TArgs > ,
2039) : GraphQLArgument [ ] {
2140 return field . args
2241}
2342
43+ /**
44+ *
45+ * @param f
46+ * @param xs
47+ *
48+ * Maps an array of functions and filters out the values
49+ * which converted to null.
50+ *
51+ */
2452function filterMap < T , U > ( f : ( x : T ) => Maybe < U > , xs : T [ ] ) : U [ ] {
2553 return xs . reduce ( ( acc , x ) => {
2654 const res = f ( x )
@@ -32,11 +60,30 @@ function filterMap<T, U>(f: (x: T) => Maybe<U>, xs: T[]): U[] {
3260 } , [ ] )
3361}
3462
63+ /**
64+ *
65+ * @param args
66+ * @param arg
67+ *
68+ * Finds the value of argument from provided argument values and
69+ * argument definition.
70+ *
71+ */
3572function getArgumentValue ( args : { [ key : string ] : any } , arg : GraphQLArgument ) {
3673 return args . get ( arg . name )
3774}
3875
39- export function executeWithArgumentType < T > (
76+ /**
77+ *
78+ * @param f
79+ * @param info
80+ * @param args
81+ *
82+ * Executes a funcition on all arguments of a particular field
83+ * and filters out the results which returned null.
84+ *
85+ */
86+ export function filterMapFieldArguments < T > (
4087 f : ( definition : GraphQLArgument , arg : any ) => Maybe < T > ,
4188 info : GraphQLResolveInfo ,
4289 args : { [ key : string ] : any } ,
@@ -49,7 +96,23 @@ export function executeWithArgumentType<T>(
4996 return filterMap ( fWithArguments , fieldArguments )
5097}
5198
52- // Upload
99+ /**
100+ *
101+ * @param type
102+ * @param x
103+ *
104+ * Checks whether a certain non-nullable, list or regular type
105+ * is of predicted type.
106+ *
107+ */
108+ export function isGraphQLArgumentType (
109+ type ,
110+ argument : GraphQLArgument ,
111+ ) : boolean {
112+ return getNamedType ( argument . type ) instanceof type
113+ }
114+
115+ // Upload --------------------------------------------------------------------
53116
54117export interface IUpload {
55118 stream : string
@@ -58,34 +121,133 @@ export interface IUpload {
58121 encoding : string
59122}
60123
61- declare type IUploadHandler < T > = ( upload : Promise < IUpload > ) => Promise < T >
124+ interface IUploadArgument {
125+ argumentName : string
126+ upload : Promise < IUpload > | Promise < IUpload > [ ]
127+ }
128+
129+ interface IProcessedUploadArgument < T > {
130+ argumentName : string
131+ upload : T | T [ ]
132+ }
133+
134+ declare type IUploadHandler < T > = ( upload : IUpload ) => Promise < T >
62135
63136interface IConfig < T > {
64137 uploadHandler : IUploadHandler < T >
65138}
66139
67- function isGraphQLUploadArgument (
68- x : GraphQLArgument ,
69- ) : x is GraphQLArgument & { type : GraphQLUpload } {
70- return x . type instanceof GraphQLUpload
140+ /**
141+ *
142+ * @param def
143+ * @param value
144+ *
145+ * Funciton used to identify GraphQLUpload arguments.
146+ *
147+ */
148+ function uploadTypeIdentifier (
149+ def : GraphQLArgument ,
150+ value : any ,
151+ ) : IUploadArgument {
152+ if ( isGraphQLArgumentType ( GraphQLUpload , def ) ) {
153+ return {
154+ argumentName : def . name ,
155+ upload : value ,
156+ }
157+ } else {
158+ return null
159+ }
71160}
72161
73- // function processor<T>
162+ /**
163+ *
164+ * @param args
165+ * @param info
166+ *
167+ * Function used to extract GraphQLUpload argumetns from a field.
168+ *
169+ */
170+ function extractUploadArguments (
171+ args : { [ key : string ] : any } ,
172+ info : GraphQLResolveInfo ,
173+ ) : IUploadArgument [ ] {
174+ return filterMapFieldArguments ( uploadTypeIdentifier , info , args )
175+ }
74176
75- async function processor < T > ( uploadFunction : IUploadHandler < T > ) {
76- return async ( def , value ) => {
77- if ( isGraphQLUploadArgument ( def ) ) {
78- return ( value as Promise < IUpload > ) . then ( uploadFunction )
177+ /**
178+ *
179+ * @param arr
180+ *
181+ * Converts an array of processed uploads to one object which can
182+ * be later used as arguments definition.
183+ *
184+ */
185+ function normaliseArguments < T > (
186+ args : IProcessedUploadArgument < T > [ ] ,
187+ ) : { [ key : string ] : T } {
188+ return args . reduce ( ( acc , val ) => {
189+ return {
190+ ...acc ,
191+ [ val . argumentName ] : val . upload ,
192+ }
193+ } , { } )
194+ }
195+
196+ /**
197+ *
198+ * @param uploadHandler
199+ *
200+ * Function used to process file uploads.
201+ *
202+ */
203+ function processor < T > ( uploadHandler : IUploadHandler < T > ) {
204+ return function ( {
205+ argumentName,
206+ upload,
207+ } : IUploadArgument ) : Maybe < Promise < IProcessedUploadArgument < T > > > {
208+ if ( Array . isArray ( upload ) ) {
209+ const uploads = upload . map ( file => file . then ( uploadHandler ) )
210+
211+ return Promise . all ( uploads ) . then ( res => ( {
212+ argumentName : argumentName ,
213+ upload : res ,
214+ } ) )
215+ } else if ( upload !== undefined ) {
216+ return upload . then ( uploadHandler ) . then ( res => ( {
217+ argumentName : argumentName ,
218+ upload : res ,
219+ } ) )
79220 } else {
80221 return null
81222 }
82223 }
83224}
84225
85- export function upload < T > ( { uploadHandler } : IConfig < T > ) : IMiddleware {
86- return ( resolve , parent , args , ctx , info ) => {
87- const executed = executeWithArgumentType ( uploadHandler , info )
88-
89- return resolve ( )
226+ /**
227+ *
228+ * @param config
229+ *
230+ * Exposed upload function which handles file upload in resolvers.
231+ * Internally, it returns a middleware function which is later processed
232+ * by GraphQL Middleware.
233+ * The first step is to extract upload arguments using identifier
234+ * which can be found above.
235+ * Once we found all the GraphQLUpload arguments we check whether they
236+ * carry a value or not and return a Promise to resolve them.
237+ * Once Promises get processed we normalise outputs and merge them
238+ * with old arguments to replace the old values with the new ones.
239+ *
240+ */
241+ export function upload < T > ( { uploadHandler } : IConfig < T > ) : IMiddlewareFunction {
242+ return async ( resolve , parent , args , ctx , info ) => {
243+ const files = extractUploadArguments ( args , info )
244+ const uploads = filterMap ( processor ( uploadHandler ) , files )
245+
246+ const uploaded = await Promise . all ( uploads )
247+ const argsUploaded = normaliseArguments ( uploaded )
248+
249+ const argsWithUploads = { ...args , ...argsUploaded }
250+
251+ return resolve ( parent , argsWithUploads , ctx , info )
90252 }
91253}
0 commit comments