├── .upignore ├── .gitignore ├── .babelrc ├── up.json ├── src ├── server.js └── schema.js ├── rollup.config.js ├── package.json └── README.md /.upignore: -------------------------------------------------------------------------------- 1 | * 2 | !app.js 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | app.js 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ['latest'], 3 | plugins: [ 4 | 'transform-runtime', 5 | 'transform-async-generator-functions', 6 | 'transform-object-rest-spread', 7 | ], 8 | } -------------------------------------------------------------------------------- /up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "star-wars", 3 | "lambda": { 4 | "memory": 512 5 | }, 6 | "hooks": { 7 | "build": "NODE_ENV=production npx rollup -c", 8 | "clean": "rm app.js" 9 | }, 10 | "cors": { "enable": true }, 11 | "proxy": { "command": "node app.js" } 12 | } 13 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { graphqlExpress, graphiqlExpress } from "apollo-server-express"; 3 | import bodyParser from "body-parser"; 4 | 5 | import { schema } from "./schema"; 6 | 7 | const PORT = process.env.PORT || 3000; 8 | const server = express(); 9 | 10 | // allow for any url to be GraphQL since this is a lambda function 11 | server.post("*", bodyParser.json(), graphqlExpress({ schema, tracing: true })); 12 | server.get("*", (...args) => { 13 | const [req] = args; 14 | return graphiqlExpress({ endpointURL: req.path })(...args); 15 | }); 16 | 17 | server.listen(PORT, () => { 18 | console.log(`GraphQL Server is now running on http://localhost:${PORT}`); 19 | }); 20 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | import commonjs from "rollup-plugin-commonjs"; 3 | // import uglify from "rollup-plugin-uglify"; 4 | import json from "rollup-plugin-json"; 5 | 6 | export default { 7 | input: "src/server.js", 8 | output: { 9 | file: "app.js", 10 | format: "cjs", 11 | sourcemap: false, 12 | }, 13 | plugins: [ 14 | json(), 15 | resolve({ preferBuiltins: true }), 16 | commonjs({ 17 | include: "node_modules/**", 18 | namedExports: { 19 | "node_modules/graphql-tools/dist/index.js": ["makeExecutableSchema"], 20 | }, 21 | }), 22 | // process.env.NODE_ENV === "production" && uglify(), 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "star_wars_schema_from_graphql_org", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "./src/server.js", 6 | "scripts": { 7 | "start": "nodemon ./src/server.js --exec babel-node -e js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "devDependencies": { 12 | "babel-cli": "^6.24.0", 13 | "babel-plugin-transform-async-generator-functions": "^6.24.1", 14 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 15 | "babel-plugin-transform-runtime": "^6.23.0", 16 | "babel-preset-latest": "^6.23.0", 17 | "nodemon": "^1.11.0", 18 | "rollup": "^0.55.3", 19 | "rollup-plugin-commonjs": "^8.3.0", 20 | "rollup-plugin-json": "^2.3.0", 21 | "rollup-plugin-node-resolve": "^3.0.2", 22 | "rollup-plugin-uglify": "^3.0.0" 23 | }, 24 | "dependencies": { 25 | "apollo-server-express": "1.3.2", 26 | "body-parser": "^1.17.1", 27 | "express": "^4.15.2", 28 | "graphql": "0.13.0", 29 | "graphql-tools": "2.20.2", 30 | "up": "^1.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Engine + Lambda example 2 | 3 | This is a simple GraphQL endpoint demoing using `Up` by Apex to deploy to AWS Lambda and using AWS Fargate to deploy Engine and link them together. 4 | 5 | ## Reproduction steps 6 | 7 | 1. `npm install` in this repo 8 | 2. Setup an IAM policy for up like [this](https://up.docs.apex.sh/#aws_credentials.iam_policy_for_up_cli) 9 | 3. run `npx up` in this directory to deploy and create a Lambda. The Lambda will be called `star-wars`. 10 | 4. Create a new service in [Engine](https://engine.apollographql.com/) 11 | 5. Create an IAM user and add the predefined AWSLambdaRole policy to it (this is overly wide policy here, could be more narrow). Create an access key ID and secret access key, which you'll use in the next step. 12 | 6. Create a config JSON like so with values from Engine, Lambda, and IAM credentials. The function ARN can be found at [the Lambda page for the function](https://console.aws.amazon.com/lambda/home?region=us-east-1#/functions/star-wars?tab=graph). 13 | ```json 14 | { 15 | "apiKey": "", 16 | "origins": [ 17 | { 18 | "lambda": { 19 | "functionArn":"", 20 | "awsAccessKeyId":"", 21 | "awsSecretAccessKey":"" 22 | } 23 | } 24 | ], 25 | "frontends": [ 26 | { 27 | "host": "0.0.0.0", 28 | "port": 80, 29 | "endpoints": ["/graphql", "/staging/graphql", "/production/graphql"] 30 | } 31 | ] 32 | } 33 | ``` 34 | 7. Stringify this so it can be used with Fargate (*hint* in chrome you can run `copy(JSON.stringify())` to copy it to your clipboard). It is worth saving this somewhere for future ease of use. 35 | 8. Go to [this link](https://console.aws.amazon.com/ecs/home?region=us-east-1#/firstRun) to start using Fargate for a service 36 | 9. Choose `custom` and enter the following information: 37 | ``` 38 | Container Name: Engine 39 | Image: gcr.io/mdg-public/engine:2018.02-50-gef2fc6d4e 40 | Port Mappings: 80 / TCP 41 | Advanced: 42 | Env Variables: 43 | ENGINE_CONFIG: 44 | 45 | ``` 46 | Then click Update. 47 | 10. Edit the task definition to set name to be engine-proxy and click next 48 | 11. Choose ALB (Application Load Balancer) and click next 49 | 12. Set cluster name to Engine and click next 50 | 13. Click create and wait until everything is ready 51 | 14. After the cluster is ready, click the service "Engine-service" and click into the target group link 52 | 15. Edit health check to be `/.well-known/apollo/engine-health` 53 | 16. Click back to Description and click the Load Balancer link 54 | 17. Copy the DNS name, and go to it in your web browser. You may get a "503 Service Temporarily Unavailable" error while the service is still starting up; if so, give it a few minutes. 55 | 18. Paste this query and hit go: 56 | ```graphql 57 | query Test { 58 | hero(episode:NEWHOPE){ 59 | name 60 | } 61 | } 62 | ``` 63 | 19. Profit (or replicate the above bugs) 64 | -------------------------------------------------------------------------------- /src/schema.js: -------------------------------------------------------------------------------- 1 | // This is the Star Wars schema used in all of the interactive GraphiQL 2 | // examples on GraphQL.org. License reproduced at the bottom. 3 | 4 | /** 5 | * Copyright (c) 2015, Facebook, Inc. 6 | * All rights reserved. 7 | * 8 | * This source code is licensed under the license found in the 9 | * LICENSE file in the root directory of this source tree. 10 | */ 11 | 12 | import { makeExecutableSchema } from "graphql-tools"; 13 | 14 | const schemaString = ` 15 | schema { 16 | query: Query 17 | mutation: Mutation 18 | } 19 | # The query type, represents all of the entry points into our object graph 20 | type Query { 21 | hero(episode: Episode): Character 22 | reviews(episode: Episode!): [Review] 23 | search(text: String): [SearchResult] 24 | character(id: ID!): Character 25 | droid(id: ID!): Droid 26 | human(id: ID!): Human 27 | starship(id: ID!): Starship 28 | environment: String 29 | } 30 | # The mutation type, represents all updates we can make to our data 31 | type Mutation { 32 | createReview(episode: Episode, review: ReviewInput!): Review 33 | } 34 | # The episodes in the Star Wars trilogy 35 | enum Episode { 36 | # Star Wars Episode IV: A New Hope, released in 1977. 37 | NEWHOPE 38 | # Star Wars Episode V: The Empire Strikes Back, released in 1980. 39 | EMPIRE 40 | # Star Wars Episode VI: Return of the Jedi, released in 1983. 41 | JEDI 42 | } 43 | # A character from the Star Wars universe 44 | interface Character { 45 | # The ID of the character 46 | id: ID! 47 | # The name of the character 48 | name: String! 49 | # The friends of the character, or an empty list if they have none 50 | friends: [Character] 51 | # The friends of the character exposed as a connection with edges 52 | friendsConnection(first: Int, after: ID): FriendsConnection! 53 | # The movies this character appears in 54 | appearsIn: [Episode]! 55 | } 56 | # Units of height 57 | enum LengthUnit { 58 | # The standard unit around the world 59 | METER 60 | # Primarily used in the United States 61 | FOOT 62 | } 63 | # A humanoid creature from the Star Wars universe 64 | type Human implements Character { 65 | # The ID of the human 66 | id: ID! 67 | # What this human calls themselves 68 | name: String! 69 | # Height in the preferred unit, default is meters 70 | height(unit: LengthUnit = METER): Float 71 | # Mass in kilograms, or null if unknown 72 | mass: Float 73 | # This human's friends, or an empty list if they have none 74 | friends: [Character] 75 | # The friends of the human exposed as a connection with edges 76 | friendsConnection(first: Int, after: ID): FriendsConnection! 77 | # The movies this human appears in 78 | appearsIn: [Episode]! 79 | # A list of starships this person has piloted, or an empty list if none 80 | starships: [Starship] 81 | } 82 | # An autonomous mechanical character in the Star Wars universe 83 | type Droid implements Character { 84 | # The ID of the droid 85 | id: ID! 86 | # What others call this droid 87 | name: String! 88 | # This droid's friends, or an empty list if they have none 89 | friends: [Character] 90 | # The friends of the droid exposed as a connection with edges 91 | friendsConnection(first: Int, after: ID): FriendsConnection! 92 | # The movies this droid appears in 93 | appearsIn: [Episode]! 94 | # This droid's primary function 95 | primaryFunction: String 96 | } 97 | # A connection object for a character's friends 98 | type FriendsConnection { 99 | # The total number of friends 100 | totalCount: Int 101 | # The edges for each of the character's friends. 102 | edges: [FriendsEdge] 103 | # A list of the friends, as a convenience when edges are not needed. 104 | friends: [Character] 105 | # Information for paginating this connection 106 | pageInfo: PageInfo! 107 | } 108 | # An edge object for a character's friends 109 | type FriendsEdge { 110 | # A cursor used for pagination 111 | cursor: ID! 112 | # The character represented by this friendship edge 113 | node: Character 114 | } 115 | # Information for paginating this connection 116 | type PageInfo { 117 | startCursor: ID 118 | endCursor: ID 119 | hasNextPage: Boolean! 120 | } 121 | # Represents a review for a movie 122 | type Review { 123 | # The number of stars this review gave, 1-5 124 | stars: Int! 125 | # Comment about the movie 126 | commentary: String 127 | } 128 | # The input object sent when someone is creating a new review 129 | input ReviewInput { 130 | # 0-5 stars 131 | stars: Int! 132 | # Comment about the movie, optional 133 | commentary: String 134 | } 135 | type Starship { 136 | # The ID of the starship 137 | id: ID! 138 | # The name of the starship 139 | name: String! 140 | # Length of the starship, along the longest axis 141 | length(unit: LengthUnit = METER): Float 142 | } 143 | union SearchResult = Human | Droid | Starship 144 | `; 145 | 146 | /** 147 | * This defines a basic set of data for our Star Wars Schema. 148 | * 149 | * This data is hard coded for the sake of the demo, but you could imagine 150 | * fetching this data from a backend service rather than from hardcoded 151 | * JSON objects in a more complex demo. 152 | */ 153 | 154 | const humans = [ 155 | { 156 | id: "1000", 157 | name: "Luke Skywalker", 158 | friends: ["1002", "1003", "2000", "2001"], 159 | appearsIn: ["NEWHOPE", "EMPIRE", "JEDI"], 160 | height: 1.72, 161 | mass: 77, 162 | starships: ["3001", "3003"], 163 | }, 164 | { 165 | id: "1001", 166 | name: "Darth Vader", 167 | friends: ["1004"], 168 | appearsIn: ["NEWHOPE", "EMPIRE", "JEDI"], 169 | height: 2.02, 170 | mass: 136, 171 | starships: ["3002"], 172 | }, 173 | { 174 | id: "1002", 175 | name: "Han Solo", 176 | friends: ["1000", "1003", "2001"], 177 | appearsIn: ["NEWHOPE", "EMPIRE", "JEDI"], 178 | height: 1.8, 179 | mass: 80, 180 | starships: ["3000", "3003"], 181 | }, 182 | { 183 | id: "1003", 184 | name: "Leia Organa", 185 | friends: ["1000", "1002", "2000", "2001"], 186 | appearsIn: ["NEWHOPE", "EMPIRE", "JEDI"], 187 | height: 1.5, 188 | mass: 49, 189 | starships: [], 190 | }, 191 | { 192 | id: "1004", 193 | name: "Wilhuff Tarkin", 194 | friends: ["1001"], 195 | appearsIn: ["NEWHOPE"], 196 | height: 1.8, 197 | mass: null, 198 | starships: [], 199 | }, 200 | ]; 201 | 202 | const humanData = {}; 203 | humans.forEach(ship => { 204 | humanData[ship.id] = ship; 205 | }); 206 | 207 | const droids = [ 208 | { 209 | id: "2000", 210 | name: "C-3PO", 211 | friends: ["1000", "1002", "1003", "2001"], 212 | appearsIn: ["NEWHOPE", "EMPIRE", "JEDI"], 213 | primaryFunction: "Protocol", 214 | }, 215 | { 216 | id: "2001", 217 | name: "R2-D2", 218 | friends: ["1000", "1002", "1003"], 219 | appearsIn: ["NEWHOPE", "EMPIRE", "JEDI"], 220 | primaryFunction: "Astromech", 221 | }, 222 | ]; 223 | 224 | const droidData = {}; 225 | droids.forEach(ship => { 226 | droidData[ship.id] = ship; 227 | }); 228 | 229 | const starships = [ 230 | { 231 | id: "3000", 232 | name: "Millenium Falcon", 233 | length: 34.37, 234 | }, 235 | { 236 | id: "3001", 237 | name: "X-Wing", 238 | length: 12.5, 239 | }, 240 | { 241 | id: "3002", 242 | name: "TIE Advanced x1", 243 | length: 9.2, 244 | }, 245 | { 246 | id: "3003", 247 | name: "Imperial shuttle", 248 | length: 20, 249 | }, 250 | ]; 251 | 252 | const starshipData = {}; 253 | starships.forEach(ship => { 254 | starshipData[ship.id] = ship; 255 | }); 256 | 257 | /** 258 | * Helper function to get a character by ID. 259 | */ 260 | function getCharacter(id) { 261 | // Returning a promise just to illustrate GraphQL.js's support. 262 | return Promise.resolve(humanData[id] || droidData[id]); 263 | } 264 | 265 | /** 266 | * Allows us to fetch the undisputed hero of the Star Wars trilogy, R2-D2. 267 | */ 268 | function getHero(episode) { 269 | if (episode === "EMPIRE") { 270 | // Luke is the hero of Episode V. 271 | return humanData["1000"]; 272 | } 273 | // Artoo is the hero otherwise. 274 | return droidData["2001"]; 275 | } 276 | 277 | /** 278 | * Allows us to query for the human with the given id. 279 | */ 280 | function getHuman(id) { 281 | return humanData[id]; 282 | } 283 | 284 | /** 285 | * Allows us to query for the droid with the given id. 286 | */ 287 | function getDroid(id) { 288 | return droidData[id]; 289 | } 290 | 291 | function getStarship(id) { 292 | return starshipData[id]; 293 | } 294 | 295 | function toCursor(str) { 296 | return Buffer("cursor" + str).toString("base64"); 297 | } 298 | 299 | function fromCursor(str) { 300 | return Buffer.from(str, "base64") 301 | .toString() 302 | .slice(6); 303 | } 304 | 305 | const resolvers = { 306 | Query: { 307 | environment: () => process.env.UP_STAGE || "local", 308 | hero: (root, { episode }) => getHero(episode), 309 | character: (root, { id }) => getCharacter(id), 310 | human: (root, { id }) => getHuman(id), 311 | droid: (root, { id }) => getDroid(id), 312 | starship: (root, { id }) => getStarship(id), 313 | reviews: () => null, 314 | search: (root, { text }) => { 315 | const re = new RegExp(text, "i"); 316 | 317 | const allData = [...humans, ...droids, ...starships]; 318 | 319 | return allData.filter(obj => re.test(obj.name)); 320 | }, 321 | }, 322 | Mutation: { 323 | createReview: (root, { episode, review }) => review, 324 | }, 325 | Character: { 326 | __resolveType(data, context, info) { 327 | if (humanData[data.id]) { 328 | return info.schema.getType("Human"); 329 | } 330 | if (droidData[data.id]) { 331 | return info.schema.getType("Droid"); 332 | } 333 | return null; 334 | }, 335 | }, 336 | Human: { 337 | height: ({ height }, { unit }) => { 338 | if (unit === "FOOT") { 339 | return height * 3.28084; 340 | } 341 | 342 | return height; 343 | }, 344 | friends: ({ friends }) => friends.map(getCharacter), 345 | friendsConnection: ({ friends }, { first, after }) => { 346 | first = first || friends.length; 347 | after = after ? parseInt(fromCursor(after), 10) : 0; 348 | const edges = friends 349 | .map((friend, i) => ({ 350 | cursor: toCursor(i + 1), 351 | node: getCharacter(friend), 352 | })) 353 | .slice(after, first + after); 354 | const slicedFriends = edges.map(({ node }) => node); 355 | return { 356 | edges, 357 | friends: slicedFriends, 358 | pageInfo: { 359 | startCursor: edges.length > 0 ? edges[0].cursor : null, 360 | hasNextPage: first + after < friends.length, 361 | endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null, 362 | }, 363 | totalCount: friends.length, 364 | }; 365 | }, 366 | starships: ({ starships }) => starships.map(getStarship), 367 | appearsIn: ({ appearsIn }) => appearsIn, 368 | }, 369 | Droid: { 370 | friends: ({ friends }) => friends.map(getCharacter), 371 | friendsConnection: ({ friends }, { first, after }) => { 372 | first = first || friends.length; 373 | after = after ? parseInt(fromCursor(after), 10) : 0; 374 | const edges = friends 375 | .map((friend, i) => ({ 376 | cursor: toCursor(i + 1), 377 | node: getCharacter(friend), 378 | })) 379 | .slice(after, first + after); 380 | const slicedFriends = edges.map(({ node }) => node); 381 | return { 382 | edges, 383 | friends: slicedFriends, 384 | pageInfo: { 385 | startCursor: edges.length > 0 ? edges[0].cursor : null, 386 | hasNextPage: first + after < friends.length, 387 | endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null, 388 | }, 389 | totalCount: friends.length, 390 | }; 391 | }, 392 | appearsIn: ({ appearsIn }) => appearsIn, 393 | }, 394 | FriendsConnection: { 395 | edges: ({ edges }) => edges, 396 | friends: ({ friends }) => friends, 397 | pageInfo: ({ pageInfo }) => pageInfo, 398 | totalCount: ({ totalCount }) => totalCount, 399 | }, 400 | FriendsEdge: { 401 | node: ({ node }) => node, 402 | cursor: ({ cursor }) => cursor, 403 | }, 404 | Starship: { 405 | length: ({ length }, { unit }) => { 406 | if (unit === "FOOT") { 407 | return length * 3.28084; 408 | } 409 | 410 | return length; 411 | }, 412 | }, 413 | SearchResult: { 414 | __resolveType(data, context, info) { 415 | if (humanData[data.id]) { 416 | return info.schema.getType("Human"); 417 | } 418 | if (droidData[data.id]) { 419 | return info.schema.getType("Droid"); 420 | } 421 | if (starshipData[data.id]) { 422 | return info.schema.getType("Starship"); 423 | } 424 | return null; 425 | }, 426 | }, 427 | }; 428 | 429 | /** 430 | * Finally, we construct our schema (whose starting query type is the query 431 | * type we defined above) and export it. 432 | */ 433 | export const schema = makeExecutableSchema({ 434 | typeDefs: [schemaString], 435 | resolvers, 436 | }); 437 | 438 | /* 439 | License from https://github.com/graphql/graphql.github.io/blob/source/LICENSE 440 | 441 | LICENSE AGREEMENT For graphql.org software 442 | 443 | Facebook, Inc. (“Facebook”) owns all right, title and interest, including all 444 | intellectual property and other proprietary rights, in and to the graphql.org 445 | software. Subject to your compliance with these terms, you are hereby granted a 446 | non-exclusive, worldwide, royalty-free copyright license to (1) use and copy the 447 | graphql.org software; and (2) reproduce and distribute the graphql.org software 448 | as part of your own software (“Your Software”). Facebook reserves all rights not 449 | expressly granted to you in this license agreement. 450 | 451 | THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS OR 452 | IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 453 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. IN NO 454 | EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICES, DIRECTORS OR EMPLOYEES BE 455 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 456 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 457 | GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 458 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 459 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 460 | THE USE OF THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 461 | 462 | You will include in Your Software (e.g., in the file(s), documentation or other 463 | materials accompanying your software): (1) the disclaimer set forth above; (2) 464 | this sentence; and (3) the following copyright notice: 465 | 466 | Copyright (c) 2015, Facebook, Inc. All rights reserved. 467 | */ 468 | --------------------------------------------------------------------------------