@@ -135,9 +135,22 @@ def parse_body(self):
135
135
elif content_type == 'application/json' :
136
136
return load_json_body (request .data .decode ('utf8' ))
137
137
138
- elif content_type in ( 'application/x-www-form-urlencoded' , 'multipart/form-data' ) :
138
+ elif content_type in 'application/x-www-form-urlencoded' :
139
139
return request .form
140
140
141
+ elif content_type == 'multipart/form-data' :
142
+ # --------------------------------------------------------
143
+ # See spec: https://github.com/jaydenseric/graphql-multipart-request-spec
144
+ #
145
+ # When processing multipart/form-data, we need to take
146
+ # files (from "parts") and place them in the "operations"
147
+ # data structure (list or dict) according to the "map".
148
+ # --------------------------------------------------------
149
+ operations = load_json_body (request .form ['operations' ])
150
+ files_map = load_json_body (request .form ['map' ])
151
+ return place_files_in_operations (
152
+ operations , files_map , request .files )
153
+
141
154
return {}
142
155
143
156
def should_display_graphiql (self ):
@@ -152,3 +165,80 @@ def request_wants_html(self):
152
165
return best == 'text/html' and \
153
166
request .accept_mimetypes [best ] > \
154
167
request .accept_mimetypes ['application/json' ]
168
+
169
+
170
+ def place_files_in_operations (operations , files_map , files ):
171
+ """Place files from multipart reuqests inside operations.
172
+
173
+ Args:
174
+
175
+ operations:
176
+ Either a dict or a list of dicts, containing GraphQL
177
+ operations to be run.
178
+
179
+ files_map:
180
+ A dictionary defining the mapping of files into "paths"
181
+ inside the operations data structure.
182
+
183
+ Keys are file names from the "files" dict, values are
184
+ lists of dotted paths describing where files should be
185
+ placed.
186
+
187
+ files:
188
+ A dictionary mapping file names to FileStorage instances.
189
+
190
+ Returns:
191
+
192
+ A structure similar to operations, but with FileStorage
193
+ instances placed appropriately.
194
+ """
195
+
196
+ # operations: dict or list
197
+ # files_map: {filename: [path, path, ...]}
198
+ # files: {filename: FileStorage}
199
+
200
+ fmap = []
201
+ for key , values in files_map .items ():
202
+ for val in values :
203
+ path = val .split ('.' )
204
+ fmap .append ((path , key ))
205
+
206
+ return _place_files_in_operations (operations , fmap , files )
207
+
208
+
209
+ def _place_files_in_operations (ops , fmap , fobjs ):
210
+ for path , fkey in fmap :
211
+ ops = _place_file_in_operations (ops , path , fobjs [fkey ])
212
+ return ops
213
+
214
+
215
+ def _place_file_in_operations (ops , path , obj ):
216
+
217
+ if len (path ) == 0 :
218
+ return obj
219
+
220
+ if isinstance (ops , list ):
221
+ key = int (path [0 ])
222
+ sub = _place_file_in_operations (ops [key ], path [1 :], obj )
223
+ return _insert_in_list (ops , key , sub )
224
+
225
+ if isinstance (ops , dict ):
226
+ key = path [0 ]
227
+ sub = _place_file_in_operations (ops [key ], path [1 :], obj )
228
+ return _insert_in_dict (ops , key , sub )
229
+
230
+ raise TypeError ('Expected ops to be list or dict' )
231
+
232
+
233
+ def _insert_in_dict (dct , key , val ):
234
+ new_dict = dct .copy ()
235
+ new_dict [key ] = val
236
+ return new_dict
237
+
238
+
239
+ def _insert_in_list (lst , key , val ):
240
+ new_list = []
241
+ new_list .extend (lst [:key ])
242
+ new_list .append (val )
243
+ new_list .extend (lst [key + 1 :])
244
+ return new_list
0 commit comments