-
Notifications
You must be signed in to change notification settings - Fork 37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GraphQL Upload #30
Merged
Merged
GraphQL Upload #30
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# GraphQL Upload | ||
|
||
This example is based off of `combining-local-and-remote-schemas`. | ||
|
||
**This example demonstrates:** | ||
|
||
- Adding a locally-executable schema. | ||
- Adding a remote schema, fetched via introspection. | ||
- Adding GraphQL Upload | ||
|
||
## Setup | ||
|
||
```shell | ||
cd graphql-upload | ||
|
||
yarn install | ||
yarn start | ||
``` | ||
|
||
The following services are available for interactive queries: | ||
|
||
- **Stitched gateway:** http://localhost:4000/graphql | ||
- _Products subservice_: http://localhost:4001/graphql | ||
|
||
## Summary | ||
|
||
Visit the [stitched gateway](http://localhost:4000/graphql) and try running the following query: | ||
|
||
```graphql | ||
query { | ||
product(upc: "1") { | ||
upc | ||
name | ||
} | ||
} | ||
``` | ||
|
||
The results of this query are live-proxied from the underlying subschemas by the stitched gateway: | ||
|
||
- `product` comes from the remote Products server. This service is added into the stitched schema using introspection, i.e.: `introspectSchema` from the `@graphql-tools/wrap` package. Introspection is a tidy way to incorporate remote schemas, but be careful: not all GraphQL servers enable introspection, and those that do will not include custom directives. | ||
|
||
- `errorCodes` comes from a locally-executable schema running on the gateway server itself. This schema is built using `makeExecutableSchema` from the `@graphql-tools/schema` package, and then stitched directly into the combined schema. Note that this still operates as a standalone schema instance that is proxied by the top-level gateway schema. | ||
|
||
## Upload a File | ||
|
||
Run the following command from the terminal to upload the file `file.txt`. To learn more, visit [graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec) | ||
|
||
```bash | ||
curl localhost:4000/graphql \ | ||
-F operations='{ "query": "mutation($file: Upload!) { uploadFile(input: $file) { filename mimetype content } }", "variables": { "file": null } }' \ | ||
-F map='{ "0": ["variables.file"] }' \ | ||
-F 0=@graphql-upload/file.txt | ||
|
||
# output | ||
# {"data":{"uploadFile":{"filename":"file.txt","mimetype":"text/plain","content":"hello upload\n"}}} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
const { ApolloServer, gql } = require("apollo-server-express"); | ||
const { buildSchema } = require("graphql"); | ||
const { Upload } = require("graphql-upload/public"); | ||
const { Readable } = require("stream"); | ||
const { stitchSchemas } = require("@graphql-tools/stitch"); | ||
const { GraphQLUpload: GatewayGraphQLUpload } = require("@graphql-tools/links"); | ||
|
||
const makeRemoteExecutor = require("../lib/make_remote_executor"); | ||
const localSchema = require("../services/local/schema"); | ||
|
||
async function makeGatewaySchema() { | ||
// Make remote executors: | ||
// these are simple functions that query a remote GraphQL API for JSON. | ||
const productsExec = makeRemoteExecutor("http://localhost:4001/graphql"); | ||
|
||
return stitchSchemas({ | ||
subschemas: [ | ||
{ | ||
// 1. Introspect a remote schema. Simple, but there are caveats: | ||
// - Remote server must enable introspection. | ||
// - Custom directives are not included in introspection. | ||
schema: buildSchema(` | ||
type Product { | ||
name: String! | ||
price: Float! | ||
upc: ID! | ||
} | ||
|
||
type Query { | ||
product(upc: ID!): Product | ||
} | ||
`), | ||
executor: productsExec, | ||
}, | ||
{ | ||
// 4. Incorporate a locally-executable subschema. | ||
// No need for a remote executor! | ||
// Note that that the gateway still proxies through | ||
// to this same underlying executable schema instance. | ||
schema: localSchema, | ||
}, | ||
], | ||
resolvers: { | ||
Upload: GatewayGraphQLUpload, | ||
}, | ||
}); | ||
} | ||
|
||
async function createApolloserver() { | ||
const schema = await makeGatewaySchema(); | ||
|
||
const server = new ApolloServer({ | ||
schema, | ||
uploads: false, | ||
}); | ||
|
||
return server; | ||
} | ||
|
||
test("mutation", async () => { | ||
const THE_MUTATION = gql` | ||
mutation uploadFile($input: Upload!) { | ||
uploadFile(input: $input) { | ||
filename | ||
mimetype | ||
content | ||
} | ||
} | ||
`; | ||
|
||
const upload = new Upload(); | ||
const filename = "some_file.jpeg"; | ||
|
||
const buffer = Buffer.from('hello upload', 'utf-8'); | ||
const stream = Readable.from(buffer); | ||
upload.promise = new Promise(resolve => resolve({ | ||
createReadStream: () => stream, | ||
filename, | ||
mimetype: 'text/plain' | ||
})) | ||
|
||
const server = await createApolloserver(); | ||
const result = await server.executeOperation({ | ||
query: THE_MUTATION, | ||
variables: { | ||
input: upload, | ||
}, | ||
}); | ||
|
||
expect(result.errors).toBeUndefined(); | ||
expect(result.data).toMatchObject({ | ||
uploadFile: { | ||
filename: "some_file.jpeg", | ||
mimetype: "text/plain", | ||
content: "hello upload", | ||
}, | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
hello upload |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
const waitOn = require("wait-on"); | ||
const express = require("express"); | ||
const { introspectSchema } = require("@graphql-tools/wrap"); | ||
const { stitchSchemas } = require("@graphql-tools/stitch"); | ||
|
||
const { GraphQLUpload: GatewayGraphQLUpload } = require("@graphql-tools/links"); | ||
const { graphqlUploadExpress } = require("graphql-upload"); | ||
const { ApolloServer } = require("apollo-server-express"); | ||
|
||
const makeRemoteExecutor = require("./lib/make_remote_executor"); | ||
const localSchema = require("./services/local/schema"); | ||
|
||
async function makeGatewaySchema() { | ||
// Make remote executors: | ||
// these are simple functions that query a remote GraphQL API for JSON. | ||
const productsExec = makeRemoteExecutor("http://localhost:4001/graphql"); | ||
const adminContext = { authHeader: "Bearer my-app-to-app-token" }; | ||
|
||
return stitchSchemas({ | ||
subschemas: [ | ||
{ | ||
// 1. Introspect a remote schema. Simple, but there are caveats: | ||
// - Remote server must enable introspection. | ||
// - Custom directives are not included in introspection. | ||
schema: await introspectSchema(productsExec, adminContext), | ||
executor: productsExec, | ||
}, | ||
{ | ||
// 4. Incorporate a locally-executable subschema. | ||
// No need for a remote executor! | ||
// Note that that the gateway still proxies through | ||
// to this same underlying executable schema instance. | ||
schema: localSchema, | ||
}, | ||
], | ||
resolvers: { | ||
Upload: GatewayGraphQLUpload, | ||
}, | ||
}); | ||
} | ||
|
||
async function startApolloServer() { | ||
const schema = await makeGatewaySchema(); | ||
const server = new ApolloServer({ | ||
schema, | ||
uploads: false, | ||
}); | ||
await server.start(); | ||
const app = express(); | ||
|
||
// Additional middleware can be mounted at this point to run before Apollo. | ||
app.use( | ||
graphqlUploadExpress({ | ||
maxFileSize: 10000000, // 10 MB | ||
maxFiles: 5, | ||
}) | ||
); | ||
|
||
// Mount Apollo middleware here. | ||
server.applyMiddleware({ app, path: "/", cors: false }); | ||
|
||
await new Promise((resolve) => app.listen({ port: 4000 }, resolve)); | ||
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`); | ||
} | ||
|
||
waitOn({ resources: ["tcp:4001"] }, async () => { | ||
startApolloServer(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
const { fetch } = require('cross-fetch'); | ||
const { print } = require('graphql'); | ||
|
||
// Builds a remote schema executor function, | ||
// customize any way that you need (auth, headers, etc). | ||
// Expects to receive an object with "document" and "variable" params, | ||
// and asynchronously returns a JSON response from the remote. | ||
module.exports = function makeRemoteExecutor(url) { | ||
return async ({ document, variables, context }) => { | ||
const query = typeof document === 'string' ? document : print(document); | ||
const fetchResult = await fetch(url, { | ||
method: 'POST', | ||
headers: { | ||
'Authorization': context.authHeader, | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify({ query, variables }), | ||
}); | ||
return fetchResult.json(); | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
module.exports = class NotFoundError extends Error { | ||
constructor(message) { | ||
super(message || 'Record not found'); | ||
this.extensions = { code: 'NOT_FOUND' }; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
|
||
module.exports = function readFileSync(dir, filename) { | ||
return fs.readFileSync(path.join(dir, filename), 'utf8'); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{ | ||
"name": "combining-local-and-remote-schemas", | ||
"version": "0.0.0", | ||
"main": "index.js", | ||
"license": "MIT", | ||
"scripts": { | ||
"start-products": "nodemon --watch services/products services/products/index.js", | ||
"start-gateway": "nodemon index.js", | ||
"start": "concurrently \"yarn:start-*\"" | ||
}, | ||
"dependencies": { | ||
"@graphql-tools/links": "^7.1.0", | ||
"@graphql-tools/schema": "^7.0.0", | ||
"@graphql-tools/stitch": "^7.0.4", | ||
"@graphql-tools/wrap": "^7.0.1", | ||
"apollo-server-express": "^2.25.0", | ||
"concurrently": "^5.3.0", | ||
"cross-fetch": "^3.0.6", | ||
"express": "^4.17.1", | ||
"express-graphql": "^0.12.0", | ||
"graphql": "^15.4.0", | ||
"graphql-upload": "^12.0.0", | ||
"nodemon": "^2.0.6", | ||
"wait-on": "^5.2.1" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
const { makeExecutableSchema } = require("@graphql-tools/schema"); | ||
|
||
// does not work | ||
// const { GraphQLUpload } = require("graphql-upload"); | ||
|
||
// does work | ||
const { GraphQLUpload } =require("@graphql-tools/links"); | ||
|
||
module.exports = makeExecutableSchema({ | ||
typeDefs: ` | ||
scalar Upload | ||
type SomeFile { | ||
filename: String | ||
mimetype: String | ||
content: String | ||
} | ||
type Mutation { | ||
uploadFile(input: Upload!): SomeFile! | ||
} | ||
type Query { | ||
errorCodes: [String!]! | ||
} | ||
`, | ||
resolvers: { | ||
Upload: GraphQLUpload, | ||
Mutation: { | ||
uploadFile: async (_, { input }) => { | ||
const { createReadStream, filename, mimetype } = await input; | ||
const chunks = []; | ||
const stream = createReadStream(); | ||
for await (const chunk of stream) { | ||
chunks.push(chunk); | ||
} | ||
const buf = Buffer.concat(chunks); | ||
|
||
return { | ||
filename, | ||
mimetype, | ||
content: buf.toString(), | ||
}; | ||
}, | ||
}, | ||
Query: { | ||
errorCodes: () => [ | ||
"NOT_FOUND", | ||
"GRAPHQL_PARSE_FAILED", | ||
"GRAPHQL_VALIDATION_FAILED", | ||
], | ||
}, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
const express = require('express'); | ||
const { graphqlHTTP } = require('express-graphql'); | ||
const schema = require('./schema'); | ||
|
||
const app = express(); | ||
app.use('/graphql', graphqlHTTP({ schema, graphiql: true })); | ||
app.listen(4001, () => console.log('products running at http://localhost:4001/graphql')); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
type Product { | ||
name: String! | ||
price: Float! | ||
upc: ID! | ||
} | ||
|
||
type Query { | ||
product(upc: ID!): Product | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
const { makeExecutableSchema } = require('@graphql-tools/schema'); | ||
const NotFoundError = require('../../lib/not_found_error'); | ||
const readFileSync = require('../../lib/read_file_sync'); | ||
const typeDefs = readFileSync(__dirname, 'schema.graphql'); | ||
|
||
// data fixtures | ||
const products = [ | ||
{ upc: '1', name: 'Cookbook', price: 15.99 }, | ||
{ upc: '2', name: 'Toothbrush', price: 3.99 }, | ||
]; | ||
|
||
module.exports = makeExecutableSchema({ | ||
typeDefs, | ||
resolvers: { | ||
Query: { | ||
product: (root, { upc }) => products.find(p => p.upc === upc) || new NotFoundError() | ||
} | ||
} | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yaacovCR this is the issue I was having in this thread ardatan/graphql-tools#671 (comment). The original
GraphQLUpload
throws an error.