Skip to content

Commit ebb8bc6

Browse files
committed
getting the file uploads stuff more in line with the actual way the API expects things now
1 parent 4366a5b commit ebb8bc6

File tree

8 files changed

+145
-169
lines changed

8 files changed

+145
-169
lines changed

README.md

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,11 @@ Creates an Etch Packet and optionally sends it to the first signer.
117117

118118
### Class Methods
119119

120-
##### prepareGraphQLFile(pathOrStreamOrBuffer[, options])
120+
##### prepareGraphQLFile(pathOrStreamLikeThing[, options])
121121
A nice helper to prepare a Stream-backed or Buffer-backed file upload for use with our GraphQL API.
122-
* `pathOrStream` (String | Stream | Buffer) - An existing `Stream` OR an existing `Buffer` OR a string representing a fully resolved path to a file to be read into a new `Stream`.
123-
* `options` (Object) - [UploadOptions](#uploadoptions) for the resulting object.
122+
* `pathOrStreamLikeThing` (String | Stream | Buffer) - An existing `Stream`, `Buffer` or other Stream-like thing supported by [FormData.append](https://github.com/form-data/form-data#void-append-string-field-mixed-value--mixed-options-) OR a string representing a fully resolved path to a file to be read into a new `Stream`.
123+
* `options` (Object) - Anything supported by [FormData.append](https://github.com/form-data/form-data#void-append-string-field-mixed-value--mixed-options-). Likely required when providing a non-standard stream. From the `form-data` docs:
124+
> Form-Data can recognize and fetch all the required information from common types of streams (fs.readStream, http.response and mikeal's request), for some other types of streams you'd need to provide "file"-related information manually
124125
* Returns an `Object` that is properly formatted to be coerced by the client for use against our GraphQL API wherever an `Upload` type is required.
125126

126127
### Types
@@ -135,16 +136,6 @@ Options for the Anvil Client. Defaults are shown after each option key.
135136
}
136137
```
137138

138-
##### UploadOptions
139-
140-
Options for the upload preparation class methods.
141-
```js
142-
{
143-
filename: <filename>, // String
144-
mimetype: <mimetype> // String
145-
}
146-
```
147-
148139
### Rate Limits
149140

150141
Our API has request rate limits in place. This API client handles `429 Too Many Requests` errors by waiting until it can retry again, then retrying the request. The client attempts to avoid `429` errors by throttling requests after the number of requests within the specified time period has been reached.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
"form-data": "^3.0.0",
5656
"limiter": "^1.1.5",
5757
"lodash.get": "^4.4.2",
58-
"mime-types": "^2.1.27",
5958
"node-fetch": "^2.6.0"
6059
},
6160
"resolutions": {

src/UploadWithOptions.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
class UploadWithOptions {
2+
constructor (streamLikeThing, formDataAppendOptions) {
3+
this.streamLikeThing = streamLikeThing
4+
this.formDataAppendOptions = formDataAppendOptions
5+
}
6+
7+
get options () {
8+
return this.formDataAppendOptions
9+
}
10+
11+
get file () {
12+
return this.streamLikeThing
13+
}
14+
15+
appendToForm (form, fieldName) {
16+
form.append(fieldName, this.streamLikeThing, this.formDataAppendOptions)
17+
}
18+
}
19+
20+
module.exports = UploadWithOptions

src/index.js

Lines changed: 26 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
const fs = require('fs')
2-
const path = require('path')
32

43
const get = require('lodash.get')
54
const fetch = require('node-fetch')
65
const FormData = require('form-data')
7-
const Mime = require('mime-types')
86
const AbortController = require('abort-controller')
97
const { extractFiles } = require('extract-files')
108
const { RateLimiter } = require('limiter')
119

10+
const UploadWithOptions = require('./UploadWithOptions')
1211
const { version, description } = require('../package.json')
1312

1413
const {
@@ -63,12 +62,25 @@ class Anvil {
6362
this.limiter = new RateLimiter(this.requestLimit, this.requestLimitMS, true)
6463
}
6564

66-
static prepareGraphQLFile (pathOrStreamOrBuffer, options) {
67-
if (pathOrStreamOrBuffer instanceof Buffer) {
68-
return this._prepareGraphQLBuffer(pathOrStreamOrBuffer, options)
65+
/**
66+
* Perform some handy/necessary things for a GraphQL file upload to make it work
67+
* with this client and with our backend
68+
*
69+
* @param {string|Buffer|Stream-like-thing} pathOrStreamLikeThing - Either a string path to a file,
70+
* a Buffer, or a Stream-like thing that is compatible with form-data as an append.
71+
* @param {object} formDataAppendOptions - User can specify options to be passed to the form-data.append
72+
* call. This should be done if a stream-like thing is not one of the common types that
73+
* form-data can figure out on its own.
74+
*
75+
* @return {UploadWithOptions} - A class that wraps the stream-like-thing and any options
76+
* up together nicely in a way that we can also tell that it was us who did it.
77+
*/
78+
static prepareGraphQLFile (pathOrStreamLikeThing, options) {
79+
if (typeof pathOrStreamLikeThing === 'string') {
80+
pathOrStreamLikeThing = fs.createReadStream(pathOrStreamLikeThing)
6981
}
7082

71-
return this._prepareGraphQLStream(pathOrStreamOrBuffer, options)
83+
return new UploadWithOptions(pathOrStreamLikeThing, options)
7284
}
7385

7486
fillPDF (pdfTemplateID, payload, clientOptions = {}) {
@@ -145,17 +157,22 @@ class Anvil {
145157

146158
i = 0
147159
filesMap.forEach((paths, file) => {
148-
// If this is a Stream, will attach a listener to the 'error' event so that we
160+
let appendOptions = {}
161+
if (file instanceof UploadWithOptions) {
162+
appendOptions = file.formDataAppendOptions
163+
file = file.file
164+
}
165+
// If this is a stream-like thing, attach a listener to the 'error' event so that we
149166
// can cancel the API call if something goes wrong
150-
if (file instanceof fs.ReadStream) {
167+
if (typeof file.on === 'function') {
151168
file.on('error', (err) => {
152169
console.warn(err)
153170
abortController.abort()
154171
})
155172
}
173+
156174
// Pass in some things explicitly to the form.append so that we get the
157175
// desired/expected filename and mimetype, etc
158-
const appendOptions = extractFormAppendOptions({ paths, object: originalOperation })
159176
form.append(`${++i}`, file, appendOptions)
160177
})
161178

@@ -321,22 +338,6 @@ class Anvil {
321338
})
322339
}
323340

324-
static _prepareGraphQLStream (pathOrStream, options) {
325-
if (typeof pathOrStream === 'string') {
326-
pathOrStream = fs.createReadStream(pathOrStream)
327-
}
328-
329-
return this._prepareGraphQLStreamOrBuffer(pathOrStream, options)
330-
}
331-
332-
static _prepareGraphQLBuffer (pathOrBuffer, options) {
333-
if (typeof pathOrBuffer === 'string') {
334-
pathOrBuffer = fs.readFileSync(pathOrBuffer)
335-
}
336-
337-
return this._prepareGraphQLStreamOrBuffer(pathOrBuffer, options)
338-
}
339-
340341
static _prepareGraphQLBase64 (data, options = {}) {
341342
const { filename, mimetype } = options
342343
if (!filename) {
@@ -357,65 +358,6 @@ class Anvil {
357358
mimetype,
358359
}
359360
}
360-
361-
static _prepareGraphQLStreamOrBuffer (streamOrBuffer, options) {
362-
const filename = this._getFilename(streamOrBuffer, options)
363-
const mimetype = this._getMimetype(streamOrBuffer, options)
364-
return {
365-
name: filename,
366-
mimetype,
367-
file: streamOrBuffer,
368-
}
369-
}
370-
371-
static _getFilename (thing, options = {}) {
372-
// Very heavily influenced by:
373-
// https://github.com/form-data/form-data/blob/55d90ce4a4c22b0ea0647991d85cb946dfb7395b/lib/form_data.js#L217
374-
375-
if (typeof options.filepath === 'string') {
376-
// custom filepath for relative paths
377-
return path.normalize(options.filepath).replace(/\\/g, '/')
378-
}
379-
if (options.filename || thing.name || thing.path) {
380-
// custom filename take precedence
381-
// formidable and the browser add a name property
382-
// fs- and request- streams have path property
383-
return path.basename(options.filename || thing.name || thing.path)
384-
}
385-
if (thing.readable && Object.prototype.hasOwnProperty.call(thing, 'httpVersion')) {
386-
// or try http response
387-
return path.basename(thing.client._httpMessage.path || '')
388-
}
389-
390-
throw new Error('Unable to determine file name for this upload. Please pass it via options.filename.')
391-
}
392-
393-
static _getMimetype (thing, options = {}) {
394-
// Very heavily influenced by:
395-
// https://github.com/form-data/form-data/blob/55d90ce4a4c22b0ea0647991d85cb946dfb7395b/lib/form_data.js#L243
396-
397-
// use custom content-type above all
398-
if (typeof options.mimetype === 'string') {
399-
return options.mimetype
400-
}
401-
402-
// or try `name` from formidable, browser
403-
if (thing.name || thing.path) {
404-
return Mime.lookup(thing.name || thing.path)
405-
}
406-
407-
// or if it's http-reponse
408-
if (thing.readable && Object.prototype.hasOwnProperty.call(thing, 'httpVersion')) {
409-
return thing.headers['content-type'] || thing.headers['Content-Type']
410-
}
411-
412-
// or guess it from the filepath or filename
413-
if ((options.filepath || options.filename)) {
414-
Mime.lookup(options.filepath || options.filename)
415-
}
416-
417-
throw new Error('Unable to determine mime type for this upload. Please pass it via options.mimetype.')
418-
}
419361
}
420362

421363
function getRetryMS (retryAfterSeconds) {
@@ -428,23 +370,4 @@ function sleep (ms) {
428370
})
429371
}
430372

431-
function extractFormAppendOptions ({ paths, object }) {
432-
const length = paths.length
433-
if (length !== 1) {
434-
if (length === 0) {
435-
throw new Error('No file map paths received')
436-
}
437-
console.warn(`WARNING: received ${length} file map paths. Expected exactly 1.`)
438-
}
439-
const path = paths[0].split('.')
440-
path.pop()
441-
442-
const parent = get(object, path.join('.'))
443-
444-
return {
445-
filename: parent.name,
446-
contentType: parent.mimetype,
447-
}
448-
}
449-
450373
module.exports = Anvil

src/validation.js

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
const fs = require('fs')
22

3+
const UploadWithOptions = require('./UploadWithOptions')
4+
35
// https://www.npmjs.com/package/extract-files/v/6.0.0#type-extractablefilematcher
46
function isFile (value) {
5-
return value instanceof fs.ReadStream || value instanceof Buffer
7+
return value instanceof UploadWithOptions || value instanceof fs.ReadStream || value instanceof Buffer
68
}
79

810
function graphQLUploadSchemaIsValid (schema, parent, key) {
911
if (typeof schema === 'undefined') {
1012
return true
1113
}
1214

13-
// There is not a great/easy/worthwhile way to determine if a string is base64-encoded data,
14-
// so our best proxy is to check the keyname
15-
if (key !== 'base64File') {
15+
if (key !== 'file') {
1616
if (schema instanceof Array) {
1717
return schema.every((subSchema) => graphQLUploadSchemaIsValid(subSchema, schema))
1818
}
@@ -21,35 +21,26 @@ function graphQLUploadSchemaIsValid (schema, parent, key) {
2121
return Object.entries(schema).every(([key, subSchema]) => graphQLUploadSchemaIsValid(subSchema, schema, key))
2222
}
2323

24-
if (!isFile(schema)) {
25-
return true
26-
}
24+
return !isFile(schema)
2725
}
2826

27+
// OK, the key is 'file'
28+
2929
// All flavors should be nested, and not top-level
30-
if (!parent) {
30+
if (!(parent && parent.file === schema)) {
3131
return false
3232
}
3333

34-
// File Upload
35-
if (key === 'file') {
36-
if (parent.file !== schema) {
37-
return false
38-
}
39-
40-
return ['name', 'mimetype'].every((requiredKey) => parent[requiredKey])
41-
}
42-
4334
// Base64 Upload
44-
if (key === 'base64File') {
45-
if (parent.base64File !== schema) {
46-
return false
47-
}
48-
49-
return ['filename', 'mimetype'].every((requiredKey) => schema[requiredKey])
35+
if (schema.data) {
36+
// Must be a string and also have the provided keys
37+
return (
38+
typeof schema.data === 'string' &&
39+
['filename', 'mimetype'].every((requiredKey) => schema[requiredKey])
40+
)
5041
}
5142

52-
return false
43+
return isFile(schema)
5344
}
5445

5546
module.exports = {

test/assets/dummy.pdf

13 KB
Binary file not shown.

0 commit comments

Comments
 (0)