Skip to content

Commit

Permalink
feat: added support for nullable uploads in multipart request
Browse files Browse the repository at this point in the history
  • Loading branch information
BowlingX committed Apr 5, 2024
1 parent 440afd0 commit 02a1e9d
Show file tree
Hide file tree
Showing 10 changed files with 1,104 additions and 269 deletions.
412 changes: 412 additions & 0 deletions src/__tests__/requests/__snapshots__/multipartFormData.spec.ts.snap

Large diffs are not rendered by default.

71 changes: 69 additions & 2 deletions src/__tests__/requests/multipartFormData.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const schema = buildASTSchema(gql`
otherFiles: Uploads!
coverPicture: Upload!
): [String!]!
uploadOptionalFiles(id: String!, otherFiles: Uploads): [String!]!
uploadOptionalFilesAndFile(id: String!, otherFiles: Uploads, coverPicture: Upload): [String!]!
}
type Query {
Expand Down Expand Up @@ -95,6 +97,34 @@ const gqlSchema = addMocksToSchema({
}
return result
},
uploadOptionalFiles: async (
root: unknown,
{ otherFiles }: { otherFiles: AsyncGenerator<FileUpload> },
) => {
const result = []

for await (const file of otherFiles) {
result.push(await text(file.createReadStream()))
}
return result
},
uploadOptionalFilesAndFile: async (
root: unknown,
{
otherFiles,
coverPicture,
}: { otherFiles: AsyncGenerator<FileUpload>; coverPicture: Promise<FileUpload | null> },
) => {
const result = []

const picture = await coverPicture

assert(null === picture)
for await (const file of otherFiles) {
result.push(await text(file.createReadStream()))
}
return result
},
},
},
schema: makeExecutableSchema({
Expand Down Expand Up @@ -146,6 +176,19 @@ const bridge = createOpenAPIGraphQLBridge({
) @OAOperation(path: "/upload-arrays-and-single/{id}") {
uploadAnArrayOfFilesAndASingleFile(id: $id, otherFiles: $files, coverPicture: $coverImage)
}
mutation uploadOptionalFiles(
$id: String!
$files: Uploads @OABody(contentType: MULTIPART_FORM_DATA)
) @OAOperation(path: "/upload-optional-files/{id}") {
uploadOptionalFiles(id: $id, otherFiles: $files)
}
mutation uploadOptionalFilesAndFile(
$id: String!
$files: Uploads @OABody(contentType: MULTIPART_FORM_DATA)
$coverPicture: Upload @OABody(contentType: MULTIPART_FORM_DATA)
) @OAOperation(path: "/upload-optional-files-and-file/{id}") {
uploadOptionalFilesAndFile(id: $id, otherFiles: $files, coverPicture: $coverPicture)
}
`,
})
app.use(
Expand All @@ -159,15 +202,15 @@ app.use(
})
}
// Process our multipart request and make sure files resolve
const uploadRequest = await processRequest(request, response, {
const { operations: uploadRequest, parsedDocuments } = await processRequest(request, response, {
maxFiles: 5,
maxFileSize: 1000,
})
// we do not support batching requests, so we can safely assume a single request
assert(!Array.isArray(uploadRequest))
return execute({
schema: gqlSchema,
document: parse(uploadRequest.query),
document: parsedDocuments[0],
variableValues: {
...variables,
...uploadRequest.variables,
Expand Down Expand Up @@ -300,4 +343,28 @@ describe('FormData', () => {
.expect(500)
expect(result.body).toMatchSnapshot()
})
it('should allow optional files', async () => {
const result = await request(app)
.post('/upload-optional-files/5')
.field('some', 'value')
.set('Content-Type', 'multipart/form-data')
.expect(200)
expect(result.body).toMatchSnapshot()
})
it('should allow optional files and file in combination', async () => {
const result = await request(app)
.post('/upload-optional-files-and-file/5')
.field('some', 'value')
.set('Content-Type', 'multipart/form-data')
.expect(200)
expect(result.body).toMatchSnapshot()
})
it('should fail if a required file is missing', async () => {
const result = await request(app)
.post('/upload-file/10')
.field('some', 'value')
.set('Content-Type', 'multipart/form-data')
.expect(500)
expect(result.body).toMatchSnapshot()
})
})
8 changes: 4 additions & 4 deletions src/graphql-upload/Upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import { FileUpload } from './processRequest.js'
* {@link GraphQLUpload} derives it’s value from {@link Upload.promise}.
*/
export default class Upload {
promise: Promise<FileUpload>
resolve?: (file: FileUpload) => void
promise: Promise<FileUpload | null>
resolve?: (file: FileUpload | null) => void
reject?: (error: Error) => void
value?: FileUpload
value?: FileUpload | null

constructor() {
/**
Expand All @@ -23,7 +23,7 @@ export default class Upload {
* Resolves the upload promise with the file upload details. This should
* only be utilized by {@linkcode processRequest}.
*/
this.resolve = (file: FileUpload) => {
this.resolve = (file: FileUpload | null) => {
/**
* The file upload details, available when the
* {@linkcode Upload.promise} resolves. This should only be utilized by
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`graphql-upload-spec \`processRequest\` with a missing multipart form field file. 1`] = `"File missing in the request."`;

exports[`graphql-upload-spec \`processRequest\` with missing graphql document 1`] = `"Missing ‘query’ field on operations."`;
254 changes: 254 additions & 0 deletions src/graphql-upload/__tests__/__snapshots__/utils.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`getAllVariablesFromDocuments 1`] = `
{
"0.variables.var1": {
"defaultValue": undefined,
"directives": [],
"kind": "VariableDefinition",
"loc": {
"end": 34,
"start": 21,
},
"type": {
"kind": "NamedType",
"loc": {
"end": 34,
"start": 28,
},
"name": {
"kind": "Name",
"loc": {
"end": 34,
"start": 28,
},
"value": "Upload",
},
},
"variable": {
"kind": "Variable",
"loc": {
"end": 26,
"start": 21,
},
"name": {
"kind": "Name",
"loc": {
"end": 26,
"start": 22,
},
"value": "var1",
},
},
},
"0.variables.var2": {
"defaultValue": undefined,
"directives": [],
"kind": "VariableDefinition",
"loc": {
"end": 50,
"start": 36,
},
"type": {
"kind": "NonNullType",
"loc": {
"end": 50,
"start": 43,
},
"type": {
"kind": "NamedType",
"loc": {
"end": 49,
"start": 43,
},
"name": {
"kind": "Name",
"loc": {
"end": 49,
"start": 43,
},
"value": "Upload",
},
},
},
"variable": {
"kind": "Variable",
"loc": {
"end": 41,
"start": 36,
},
"name": {
"kind": "Name",
"loc": {
"end": 41,
"start": 37,
},
"value": "var2",
},
},
},
"0.variables.var3": {
"defaultValue": undefined,
"directives": [],
"kind": "VariableDefinition",
"loc": {
"end": 65,
"start": 52,
},
"type": {
"kind": "NamedType",
"loc": {
"end": 65,
"start": 59,
},
"name": {
"kind": "Name",
"loc": {
"end": 65,
"start": 59,
},
"value": "Upload",
},
},
"variable": {
"kind": "Variable",
"loc": {
"end": 57,
"start": 52,
},
"name": {
"kind": "Name",
"loc": {
"end": 57,
"start": 53,
},
"value": "var3",
},
},
},
"1.variables.var1": {
"defaultValue": undefined,
"directives": [],
"kind": "VariableDefinition",
"loc": {
"end": 34,
"start": 21,
},
"type": {
"kind": "NamedType",
"loc": {
"end": 34,
"start": 28,
},
"name": {
"kind": "Name",
"loc": {
"end": 34,
"start": 28,
},
"value": "Upload",
},
},
"variable": {
"kind": "Variable",
"loc": {
"end": 26,
"start": 21,
},
"name": {
"kind": "Name",
"loc": {
"end": 26,
"start": 22,
},
"value": "var1",
},
},
},
"1.variables.var2": {
"defaultValue": undefined,
"directives": [],
"kind": "VariableDefinition",
"loc": {
"end": 50,
"start": 36,
},
"type": {
"kind": "NonNullType",
"loc": {
"end": 50,
"start": 43,
},
"type": {
"kind": "NamedType",
"loc": {
"end": 49,
"start": 43,
},
"name": {
"kind": "Name",
"loc": {
"end": 49,
"start": 43,
},
"value": "Upload",
},
},
},
"variable": {
"kind": "Variable",
"loc": {
"end": 41,
"start": 36,
},
"name": {
"kind": "Name",
"loc": {
"end": 41,
"start": 37,
},
"value": "var2",
},
},
},
"1.variables.var3": {
"defaultValue": undefined,
"directives": [],
"kind": "VariableDefinition",
"loc": {
"end": 65,
"start": 52,
},
"type": {
"kind": "NamedType",
"loc": {
"end": 65,
"start": 59,
},
"name": {
"kind": "Name",
"loc": {
"end": 65,
"start": 59,
},
"value": "Upload",
},
},
"variable": {
"kind": "Variable",
"loc": {
"end": 57,
"start": 52,
},
"name": {
"kind": "Name",
"loc": {
"end": 57,
"start": 53,
},
"value": "var3",
},
},
},
}
`;
Loading

0 comments on commit 02a1e9d

Please sign in to comment.