diff --git a/apollo/package-lock.json b/apollo/package-lock.json index 3a12ca7..9ae80aa 100644 --- a/apollo/package-lock.json +++ b/apollo/package-lock.json @@ -12,10 +12,12 @@ "body-parser": "^1.20.2", "cors": "^2.8.5", "express": "^4.19.2", - "graphql-upload": "^16.0.2" + "graphql-upload": "^16.0.2", + "uuid": "^10.0.0" }, "devDependencies": { "@apollo/server": "^4.10.4", + "@types/graphql-upload": "^16.0.7", "@types/node": "^20.11.5", "graphql": "^16.9.0", "typescript": "^5.3.3" @@ -109,6 +111,19 @@ "graphql": "14.x || 15.x || 16.x" } }, + "node_modules/@apollo/server/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@apollo/usage-reporting-protobuf": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@apollo/usage-reporting-protobuf/-/usage-reporting-protobuf-4.1.1.tgz", @@ -392,6 +407,15 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "dev": true }, + "node_modules/@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "devOptional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -419,6 +443,24 @@ "@types/node": "*" } }, + "node_modules/@types/content-disposition": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.8.tgz", + "integrity": "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==", + "devOptional": true + }, + "node_modules/@types/cookies": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz", + "integrity": "sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==", + "devOptional": true, + "dependencies": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "4.17.14", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", @@ -442,6 +484,62 @@ "@types/range-parser": "*" } }, + "node_modules/@types/graphql-upload": { + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@types/graphql-upload/-/graphql-upload-16.0.7.tgz", + "integrity": "sha512-7vCoxIv2pVTvV8n+miYyfkINdguWsYomAkPlOfHoM6z/qzsiBAdfRb6lNc8PvEUhe7TXaxX4+LHubejw1og1DQ==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/koa": "*", + "@types/node": "*", + "fs-capacitor": "^8.0.0", + "graphql": "^16.3.0" + } + }, + "node_modules/@types/http-assert": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.5.tgz", + "integrity": "sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==", + "devOptional": true + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "devOptional": true + }, + "node_modules/@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "devOptional": true + }, + "node_modules/@types/koa": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", + "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", + "devOptional": true, + "dependencies": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "node_modules/@types/koa-compose": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.8.tgz", + "integrity": "sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==", + "devOptional": true, + "dependencies": { + "@types/koa": "*" + } + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -1326,10 +1424,13 @@ } }, "node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "dev": true, + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -1427,6 +1528,14 @@ "node-fetch": "^2.6.7", "uuid": "^9.0.0", "whatwg-mimetype": "^3.0.0" + }, + "dependencies": { + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true + } } }, "@apollo/server-gateway-interface": { @@ -1662,6 +1771,15 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "dev": true }, + "@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "devOptional": true, + "requires": { + "@types/node": "*" + } + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -1689,6 +1807,24 @@ "@types/node": "*" } }, + "@types/content-disposition": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.8.tgz", + "integrity": "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==", + "devOptional": true + }, + "@types/cookies": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz", + "integrity": "sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==", + "devOptional": true, + "requires": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, "@types/express": { "version": "4.17.14", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", @@ -1712,6 +1848,62 @@ "@types/range-parser": "*" } }, + "@types/graphql-upload": { + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@types/graphql-upload/-/graphql-upload-16.0.7.tgz", + "integrity": "sha512-7vCoxIv2pVTvV8n+miYyfkINdguWsYomAkPlOfHoM6z/qzsiBAdfRb6lNc8PvEUhe7TXaxX4+LHubejw1og1DQ==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/koa": "*", + "@types/node": "*", + "fs-capacitor": "^8.0.0", + "graphql": "^16.3.0" + } + }, + "@types/http-assert": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.5.tgz", + "integrity": "sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==", + "devOptional": true + }, + "@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "devOptional": true + }, + "@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "devOptional": true + }, + "@types/koa": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", + "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", + "devOptional": true, + "requires": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "@types/koa-compose": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.8.tgz", + "integrity": "sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==", + "devOptional": true, + "requires": { + "@types/koa": "*" + } + }, "@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -2364,10 +2556,9 @@ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "dev": true + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==" }, "vary": { "version": "1.1.2", diff --git a/apollo/package.json b/apollo/package.json index 507e474..dcabe30 100644 --- a/apollo/package.json +++ b/apollo/package.json @@ -12,6 +12,7 @@ "license": "ISC", "devDependencies": { "@apollo/server": "^4.10.4", + "@types/graphql-upload": "^16.0.7", "@types/node": "^20.11.5", "graphql": "^16.9.0", "typescript": "^5.3.3" @@ -20,6 +21,7 @@ "body-parser": "^1.20.2", "cors": "^2.8.5", "express": "^4.19.2", - "graphql-upload": "^16.0.2" + "graphql-upload": "^16.0.2", + "uuid": "^10.0.0" } } diff --git a/apollo/src/index.ts b/apollo/src/index.ts index 8bcb48c..705c911 100644 --- a/apollo/src/index.ts +++ b/apollo/src/index.ts @@ -6,9 +6,11 @@ import http from 'http' import cors from 'cors' import bodyParser from 'body-parser' import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs' -import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs' +import GraphQLUpload, { FileUpload } from 'graphql-upload/GraphQLUpload.mjs' import { GraphQLError } from 'graphql' import data from './data.json' assert { type: 'json' } +import type { Readable } from 'stream' +import { v4 as uuidv4 } from 'uuid' const BASIC_LOGGING: any = { requestDidStart(requestContext) { @@ -31,10 +33,28 @@ const BASIC_LOGGING: any = { }, } +type UploadedFile = { + id: string + name: string + content: string +} + +type FormSubmissionDocument = { + name: string + file: UploadedFile +} + +type FormSubmission = { + id: string + firstName: string + lastName: string + documents: FormSubmissionDocument[] +} + let users = [] let idIncrement = 0 -let files = [] -let formSubmissions = [] +let files: UploadedFile[] = [] +let formSubmissions: FormSubmission[] = [] function initState() { users = [...data] @@ -80,6 +100,25 @@ const typeDefs = `#graphql userById(id: ID!): User testFetchOptions: TestFetchOptions getError: Boolean + getSubmissions: [FormSubmission] + } + + type UploadedFile { + id: String! + name: String! + content: String! + } + + type FormSubmissionDocument { + name: String + file: UploadedFile! + } + + type FormSubmission { + id: String! + firstName: String + lastName: String + documents: [FormSubmissionDocument] } type Mutation { @@ -87,26 +126,39 @@ const typeDefs = `#graphql deleteUser(id: Int!): Boolean initState: Boolean! triggerError: Boolean - uploadFile(id: String!, file: Upload!): File! - submitForm(elements: [FormElement]): Boolean! + uploadFile(file: Upload): Boolean! + submitForm(input: FormSubmissionInput!): Boolean! } - input FormElement { + input FormSubmissionDocumentsInput { name: String file: Upload! } - type File { - id: String! - filename: String! + input FormSubmissionInput { + firstName: String + lastName: String + documents: [FormSubmissionDocumentsInput] } ` +function streamToString(stream: Readable): Promise { + const chunks = [] + return new Promise((resolve, reject) => { + stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + stream.on('error', (err) => reject(err)) + stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) + }) +} + const resolvers = { Query: { users: () => { return users }, + getSubmissions: () => { + return formSubmissions + }, userById: (_: any, args: any) => { const id = parseInt(args.id) return users.find((v) => v.id === id) @@ -153,26 +205,43 @@ const resolvers = { }) }, - uploadFile: async (_, { id, file }) => { + uploadFile: async (_, { file }) => { const { filename, createReadStream } = await file console.log(`Uploading ${filename}...`) const stream = createReadStream() - // Promisify the stream and store the file, then… - const newImage = { id, filename } - files.push(newImage) - return newImage + const content = await streamToString(stream) + files.push({ id: uuidv4(), name: filename, content }) + return true }, - submitForm: async (_, { elements }) => { - for (let i = 0; i < elements.length; i++) { - const element = elements[i] - const file = element.file + submitForm: async (_, { input }) => { + const firstName = input.firstName + const lastName = input.lastName + const documents: FormSubmissionDocument[] = [] + + for (let i = 0; i < input.documents.length; i++) { + const doc = input.documents[i] + const file: FileUpload = doc.file + const name = doc.name const { filename, createReadStream } = await file - console.log(`Uploading ${filename}...`) const stream = createReadStream() - // Promisify the stream and store the file, then… - formSubmissions.push({ name: element.name }) + const content = await streamToString(stream) + + documents.push({ + name, + file: { + id: uuidv4(), + name: filename, + content, + }, + }) } + formSubmissions.push({ + id: uuidv4(), + firstName, + lastName, + documents, + }) return true }, diff --git a/cypress/e2e/fileUploads.cy.ts b/cypress/e2e/fileUploads.cy.ts new file mode 100644 index 0000000..4fa5e89 --- /dev/null +++ b/cypress/e2e/fileUploads.cy.ts @@ -0,0 +1,70 @@ +describe('Mutations with file uploads', () => { + it('correctly saves a single file', () => { + cy.initState() + cy.visit('/test-upload') + cy.fixture('check.svg', null).as('check-svg') + cy.get('#file-single') + .selectFile('@check-svg') + .trigger('change') + .then(() => { + cy.wait(1000).then(() => { + cy.get('#file-single-upload').should('be.visible') + cy.get('#file-single-upload').trigger('click') + cy.get('#file-single-upload').click({ force: true }) + cy.get('#upload-success').should('contain.text', 'true') + }) + }) + }) + + it('correctly saves multiple files', () => { + cy.initState() + cy.visit('/test-upload/contact') + .wait(1000) + .then(() => { + cy.fixture('one.txt', null).as('one') + cy.fixture('two.txt', null).as('two') + cy.fixture('three.txt', null).as('three') + cy.fixture('four.txt', null).as('four') + + cy.get('#firstname').type('John') + cy.get('#lastname').type('Wayne') + + cy.get('#file-multiple') + .selectFile(['@one', '@four', '@two', '@three']) + .trigger('change') + .then(() => { + cy.wait(1000).then(() => { + cy.get('#submit').should('be.visible') + cy.get('#submit').trigger('click') + cy.get('#submit').click({ force: true }) + cy.get('#upload-success').should('contain.text', 'true') + + // Test that the submission was correct. + cy.get('#submissions-table tbody tr') + .eq(0) + .within(() => { + // Text values are correct. + cy.get('.firstName').should('have.text', 'John') + cy.get('.lastName').should('have.text', 'Wayne') + cy.get('.documents').within(() => { + // Size of file was used correctly as name. + // "FOUR" => 4 bytes + cy.get('li').eq(0).find('.docName').should('have.text', '4') + + // Check that the order of the uploaded documents was stored correctly. + const ORDER = ['ONE', 'FOUR', 'TWO', 'THREE'] + + for (let i = 0; i < ORDER.length; i++) { + const text = ORDER[i] + cy.get('li') + .eq(i) + .find('.content') + .should('have.text', text + '\n') + } + }) + }) + }) + }) + }) + }) +}) diff --git a/cypress/fixtures/check.svg b/cypress/fixtures/check.svg new file mode 100644 index 0000000..e422cbc --- /dev/null +++ b/cypress/fixtures/check.svg @@ -0,0 +1,2 @@ + + diff --git a/cypress/fixtures/four.txt b/cypress/fixtures/four.txt new file mode 100644 index 0000000..9c89f62 --- /dev/null +++ b/cypress/fixtures/four.txt @@ -0,0 +1 @@ +FOUR diff --git a/cypress/fixtures/one.txt b/cypress/fixtures/one.txt new file mode 100644 index 0000000..a2628c1 --- /dev/null +++ b/cypress/fixtures/one.txt @@ -0,0 +1 @@ +ONE diff --git a/cypress/fixtures/three.txt b/cypress/fixtures/three.txt new file mode 100644 index 0000000..0aa1a36 --- /dev/null +++ b/cypress/fixtures/three.txt @@ -0,0 +1 @@ +THREE diff --git a/cypress/fixtures/two.txt b/cypress/fixtures/two.txt new file mode 100644 index 0000000..6333d30 --- /dev/null +++ b/cypress/fixtures/two.txt @@ -0,0 +1 @@ +TWO diff --git a/playground/pages/test-upload/contact.vue b/playground/pages/test-upload/contact.vue new file mode 100644 index 0000000..913708a --- /dev/null +++ b/playground/pages/test-upload/contact.vue @@ -0,0 +1,96 @@ + + + diff --git a/playground/pages/test-upload/index.vue b/playground/pages/test-upload/index.vue index edabb64..f621998 100644 --- a/playground/pages/test-upload/index.vue +++ b/playground/pages/test-upload/index.vue @@ -1,23 +1,17 @@ diff --git a/playground/pages/test-upload/mutation.testFormSubmit.graphql b/playground/pages/test-upload/mutation.testFormSubmit.graphql index 8e3928f..7b20296 100644 --- a/playground/pages/test-upload/mutation.testFormSubmit.graphql +++ b/playground/pages/test-upload/mutation.testFormSubmit.graphql @@ -1,3 +1,3 @@ -mutation testFormSubmit($elements: [FormElement]) { - submitForm(elements: $elements) +mutation testFormSubmit($input: FormSubmissionInput!) { + submitForm(input: $input) } diff --git a/playground/pages/test-upload/mutation.testUpload.graphql b/playground/pages/test-upload/mutation.testUpload.graphql index ba75563..4536e30 100644 --- a/playground/pages/test-upload/mutation.testUpload.graphql +++ b/playground/pages/test-upload/mutation.testUpload.graphql @@ -1,6 +1,3 @@ -mutation testUpload($id: String!, $file: Upload!) { - uploadFile(id: $id, file: $file) { - id - filename - } +mutation testUpload($file: Upload!) { + uploadFile(file: $file) } diff --git a/playground/pages/test-upload/query.getSubmissions.graphql b/playground/pages/test-upload/query.getSubmissions.graphql new file mode 100644 index 0000000..6d9b5bb --- /dev/null +++ b/playground/pages/test-upload/query.getSubmissions.graphql @@ -0,0 +1,19 @@ +query getSubmissions { + getSubmissions { + ...formSubmission + } +} + +fragment formSubmission on FormSubmission { + id + firstName + lastName + documents { + name + file { + name + id + content + } + } +} diff --git a/playground/schema.graphql b/playground/schema.graphql index 2eca89c..922c51c 100644 --- a/playground/schema.graphql +++ b/playground/schema.graphql @@ -3,27 +3,41 @@ Indicates exactly one field must be supplied and this field must not be `null`. """ directive @oneOf on INPUT_OBJECT -type File { - filename: String! +type FormSubmission { + documents: [FormSubmissionDocument] + firstName: String id: String! + lastName: String } -input FormElement { +type FormSubmissionDocument { + file: UploadedFile! + name: String +} + +input FormSubmissionDocumentsInput { file: Upload! name: String } +input FormSubmissionInput { + documents: [FormSubmissionDocumentsInput] + firstName: String + lastName: String +} + type Mutation { createUser(user: UserData!): User! deleteUser(id: Int!): Boolean initState: Boolean! - submitForm(elements: [FormElement]): Boolean! + submitForm(input: FormSubmissionInput!): Boolean! triggerError: Boolean - uploadFile(file: Upload!, id: String!): File! + uploadFile(file: Upload!): Boolean! } type Query { getError: Boolean + getSubmissions: [FormSubmission] testFetchOptions: TestFetchOptions userById(id: ID!): User users: [User!]! @@ -37,6 +51,12 @@ type TestFetchOptions { """The `Upload` scalar type represents a file upload.""" scalar Upload +type UploadedFile { + content: String! + id: String! + name: String! +} + type User { dateOfBirth: String description: String