├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── README.md ├── backend ├── todo-api │ ├── .eslintrc │ ├── .gitignore │ ├── output-schema.js │ ├── package.json │ ├── src │ │ ├── index.js │ │ ├── models │ │ │ └── todo │ │ │ │ ├── todo.mutations.js │ │ │ │ ├── todo.queries.js │ │ │ │ ├── todo.schema.js │ │ │ │ └── todo.subscriptions.js │ │ ├── root.mutation.js │ │ ├── root.query.js │ │ ├── root.schema.js │ │ └── root.subscription.js │ ├── webpack.config.js │ └── yarn.lock ├── todo-backend.yaml ├── todo-subscription-pruner │ ├── .eslintrc │ ├── .gitignore │ ├── package.json │ ├── src │ │ └── index.js │ ├── webpack.config.js │ └── yarn.lock ├── todo-subscription-publisher │ ├── .eslintrc │ ├── .gitignore │ ├── package.json │ ├── src │ │ └── index.js │ ├── webpack.config.js │ └── yarn.lock └── yarn.lock ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── schema.json ├── src ├── app │ ├── apollo │ │ ├── app-apollo.module.ts │ │ ├── credentials.model.ts │ │ └── get-credentials.ts │ ├── app-routing │ │ └── app.routing.module.ts │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ └── todos │ │ ├── todo-create │ │ ├── todo-create.component.css │ │ ├── todo-create.component.html │ │ └── todo-create.component.ts │ │ ├── todo-home │ │ ├── todo-home.component.html │ │ └── todo-home.component.ts │ │ ├── todo-list-item │ │ ├── todo-list-item.component.html │ │ └── todo-list-item.component.ts │ │ ├── todo-list │ │ ├── todo-list.component.html │ │ └── todo-list.component.ts │ │ └── todo-team │ │ ├── todo-team.component.css │ │ ├── todo-team.component.html │ │ └── todo-team.component.ts ├── assets │ └── .gitkeep ├── enviornment │ └── environment.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── graphql │ ├── CreateTodo.graphql │ ├── TeamTodoAdded.graphql │ ├── TeamTodos.graphql │ └── schema.ts ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "serverless-graphql-subscriptions", 5 | "ejected": true 6 | }, 7 | "apps": [ 8 | { 9 | "root": "src", 10 | "outDir": "dist", 11 | "assets": [ 12 | "assets", 13 | "favicon.ico" 14 | ], 15 | "index": "index.html", 16 | "main": "main.ts", 17 | "polyfills": "polyfills.ts", 18 | "test": "test.ts", 19 | "tsconfig": "tsconfig.app.json", 20 | "testTsconfig": "tsconfig.spec.json", 21 | "prefix": "app", 22 | "styles": [ 23 | "styles.css" 24 | ], 25 | "scripts": [], 26 | "environmentSource": "environments/environment.ts", 27 | "environments": { 28 | "dev": "environments/environment.ts", 29 | "prod": "environments/environment.prod.ts" 30 | } 31 | } 32 | ], 33 | "e2e": { 34 | "protractor": { 35 | "config": "./protractor.conf.js" 36 | } 37 | }, 38 | "lint": [ 39 | { 40 | "project": "src/tsconfig.app.json", 41 | "exclude": "**/node_modules/**" 42 | }, 43 | { 44 | "project": "src/tsconfig.spec.json", 45 | "exclude": "**/node_modules/**" 46 | }, 47 | { 48 | "project": "e2e/tsconfig.e2e.json", 49 | "exclude": "**/node_modules/**" 50 | } 51 | ], 52 | "test": { 53 | "karma": { 54 | "config": "./karma.conf.js" 55 | } 56 | }, 57 | "defaults": { 58 | "styleExt": "css", 59 | "component": { 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | testem.log 34 | /typings 35 | yarn-error.log 36 | 37 | # e2e 38 | /e2e/*.js 39 | /e2e/*.map 40 | 41 | # System Files 42 | .DS_Store 43 | Thumbs.db 44 | 45 | load-testing 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS AppSync 2 | 3 | AWS recently launched [AppSync](https://aws.amazon.com/blogs/aws/introducing-amazon-appsync/)which is in public preview. I have yet to try it my self but it is significantly easier to setup than this package for graphql subscriptions. It also has cool features such as offline support and autoprovisioning of dynamodb tables based on your graphql schema. 4 | 5 | # ServerlessGraphqlSubscriptions 6 | 7 | [Demo](https://todo.girishnanda.com) 8 | 9 | Open 2 different tabs to the same team url i.e. (https://todo.girishnanda.com/team/${teamName}. If one tab creates a todo, it will appear up in the other tab. 10 | 11 | See https://github.com/ioxe/graphql-aws-iot-server for server side package 12 | 13 | See https://github.com/ioxe/graphql-aws-iot-client for client side ws transport package which works with current Apollo Client as is. 14 | 15 | # Deploying backend 16 | 17 | * The backend for this app is deployed using cloudformation. Cloudformation stacks can be deployed via AWS console. For deploying this into your own account some of the input parameters will have to be changed 18 | Below is the link to the full stack 19 | https://github.com/ioxe/graphql-aws-iot-example/blob/master/backend/todo-backend.yaml 20 | 21 | * For IotEndpoint you need to enter the IoT endpoint for the region and account number where you are deploying this app. 22 | Below is the command to get the endpoint for a specific aws profile and region 23 | 24 | * You need to specify an origin access identity as an input parameter to the stack. You can either choose an existing one or create a new one from the cloudfront section of the AWS console under Private Content / Origin Access Identity. An origin access identity should look like origin-access-identity/cloudfront/E2ZUH5OG8A4XID. If the identity is invalid the cloudfront distribution will fail to deploy and the stack create will fail. 25 | 26 | ``` 27 | aws describe iot-endpoint --profile profilename --region region 28 | ``` 29 | * You may want to also change the MinDynamoDbAutoScalingCapacity and the MaxDynamoDbAutoScalingCapacity. Currently there are two global indexes for the subscription publisher and pruner as well the 2 primary indexes (1 for subscriptions table 1 for todos table). So setting a min capacity of 5 would mean 20 write and 20 read capacity units would be provisioned. you get 25 read units and 25 write units in the free tier. I am sure this could be optimized perhaps using local secondary indexes instead of global indexes so that it would take less total read /write units. 30 | 31 | * The lambda build files for the lambda functions are served on a public bucket so you should not need to change the key / bucket for deploying this stack. If you want to deploy your own custom functions. You need to change the BackendCodeBucket, TodoApiKey, SubscriptionPublisherKey, and SubscriptionPrunerKey input parameters. 32 | 33 | * To rebuild the three lambda function please see package.json for each function for the current build scripts. You would need to change the variables in the config (devFunctionName, devCodeBucketName, codeKey, profile and region to match your own environment). devFunctionName is used to update and existing lambda function so it is not needed for your first deploy. 34 | 35 | * npm run update:S3 uploads the build zip to an s3 bucket so that it can be used with cloudformation. 36 | 37 | ## Post cloudformation backend deployment update client app 38 | 39 | * Update environment variables in client app - src/environments/environment.ts. IdentityPoolId is an output of the cloudformaiton stack. Please update the 'identityPoolId' variable to that value. Update the region and iotEndpoint to match the region of your stack and the iotEndpoint that you entered as a stack parameter. 40 | 41 | * Upload client build to s3 bucket - WebsiteBucketName is also an output of the stack. You need to change the s3BucketName in the config section of package.json at the root of this repo. You also need to update the profile and region to match your AWS account. npm run update will upload the client build to the website bucket. 42 | 43 | * Visit s3 bucket url - The cloudformation stack also has an output named WebsiteBucketUrl. This is the url where you can view the app once the build is uploaded. 44 | 45 | 46 | * the current example uses dynamodb to store subscription state. However the [graphql-aws-iot-server](https://github.com/ioxe/graphql-aws-iot-server) is designed to work with other dbs also. Please see server repo for more details. For another app, I currently use faunadb its a serverless database with less setup. Theres no need to think about provisioned capacity and autoscaling. 47 | 48 | -------------------------------------------------------------------------------- /backend/todo-api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "globals": { 6 | "Promise": true 7 | }, 8 | "plugins": [ 9 | "import" 10 | ], 11 | "parser": "babel-eslint", 12 | "parserOptions": { 13 | "ecmaVersion": 6, 14 | "sourceType": "module", 15 | "ecmaFeatures": { 16 | "es6": true 17 | } 18 | }, 19 | "rules": { 20 | "import/no-unresolved": [ 21 | 2, 22 | { 23 | "commonjs": true, 24 | "amd": true 25 | } 26 | ], 27 | "semi": 2, 28 | "no-undef": 2, 29 | "no-unused-vars": [ 30 | "error", 31 | { 32 | "argsIgnorePattern": "^_" 33 | } 34 | ], 35 | "comma-dangle": 2 36 | } 37 | } -------------------------------------------------------------------------------- /backend/todo-api/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | -------------------------------------------------------------------------------- /backend/todo-api/output-schema.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | var { introspectionQuery } = require('graphql'); 4 | 5 | const handler = require('./dist').schemaGenHandler; 6 | 7 | const outputPath = '../../schema.json'; 8 | 9 | handler({ 10 | query: introspectionQuery 11 | }, null, function (err, res) { 12 | if (res.errors) { 13 | console.log(res.errors); 14 | } else { 15 | fs.writeFileSync( 16 | path.join(__dirname, outputPath), // writes to frontend folder 17 | JSON.stringify(res, null, 2) 18 | ); 19 | console.log(path.join(__dirname, outputPath)); 20 | } 21 | }); -------------------------------------------------------------------------------- /backend/todo-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "config": { 7 | "devFunctionName": "todo-ws-transport-example-TodoApi-6N2TGOBFJXMG", 8 | "devCodeBucketName": "iox-playground", 9 | "codeKey": "todo-api.zip", 10 | "profile": "iox", 11 | "region": "us-west-2" 12 | }, 13 | "scripts": { 14 | "prebuild": "rm -rf dist && mkdir -p dist", 15 | "build": "webpack", 16 | "prezip": "rimraf build && mkdir -p build", 17 | "zip": "cd dist && zip -r ../build/build.zip *", 18 | "lint": "eslint src", 19 | "lint:dist": "eslint dist index.js", 20 | "preupdate": "npm run build && npm run zip", 21 | "update": "aws lambda update-function-code --function-name $npm_package_config_devFunctionName --zip-file fileb://$PWD/build/build.zip --profile $npm_package_config_profile --region $npm_package_config_region", 22 | "updateS3": "npm run preupdate && aws s3 cp build/build.zip s3://$npm_package_config_devCodeBucketName/$npm_package_config_codeKey --profile $npm_package_config_profile --region $npm_package_config_region", 23 | "schema:generate": "webpack && node output-schema.js" 24 | }, 25 | "author": "Girish Nanda ", 26 | "license": "MIT", 27 | "dependencies": { 28 | "graphql": "^0.11.2", 29 | "graphql-aws-iot-server": "^0.0.1", 30 | "source-map-support": "^0.4.16", 31 | "uuid": "^3.1.0" 32 | }, 33 | "devDependencies": { 34 | "aws-sdk": "^2.102.0", 35 | "aws-xray-sdk": "^1.1.4", 36 | "babel-cli": "^6.24.0", 37 | "babel-core": "^6.26.0", 38 | "babel-eslint": "^6.1.2", 39 | "babel-loader": "^7.1.2", 40 | "babel-plugin-transform-es2015-modules-commonjs": "^6.24.0", 41 | "babel-preset-env": "^1.3.3", 42 | "babel-preset-stage-2": "^6.22.0", 43 | "eslint": "^3.18.0", 44 | "eslint-plugin-import": "^2.2.0", 45 | "graphql-server-core": "^1.1.0", 46 | "graphql-tools": "^0.10.1", 47 | "webpack": "^3.5.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /backend/todo-api/src/index.js: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register'; 2 | 3 | import AWSXray from 'aws-xray-sdk'; 4 | const AWS = AWSXray.captureAWS(require('aws-sdk')); 5 | 6 | import { SubscriptionManager, PubSub } from 'graphql-aws-iot-server'; 7 | 8 | import schema from './root.schema'; 9 | 10 | let db; 11 | let manager; 12 | let pubsub; 13 | 14 | export const handler = (event, context, callback) => { 15 | console.log('Todo api handler running'); 16 | console.log(JSON.stringify(event)); 17 | 18 | if (!db) { 19 | db = new AWS.DynamoDB.DocumentClient(); 20 | } 21 | 22 | if (!pubsub) { 23 | // class that invokes publisher lambda to trigger a subscription publish has only one method publish 24 | pubsub = new PubSub(process.env.SubscriptionPublisherFunctionName); 25 | } 26 | 27 | if (!manager) { 28 | const subscriptionManagerOptions = { 29 | appPrefix: process.env.AppPrefix, 30 | iotEndpoint: process.env.IotEndpoint, 31 | schema, 32 | addSubscriptionFunction: (subscription) => { 33 | const putParams = { 34 | TableName: process.env.SubscriptionsTableName, 35 | Item: subscription 36 | }; 37 | return db.put(putParams).promise(); 38 | }, 39 | removeSubscriptionFunction: ({ clientId, subscriptionName }) => { 40 | const params = { 41 | TableName: process.env.SubscriptionsTableName, 42 | Key: { 43 | clientId, 44 | subscriptionName: subscriptionName 45 | } 46 | }; 47 | return db.delete(params).promise(); 48 | } 49 | }; 50 | manager = new SubscriptionManager(subscriptionManagerOptions); 51 | } 52 | 53 | const { data, clientId } = event; 54 | const parsedMessage = JSON.parse(data); 55 | 56 | const graphqlContext = { 57 | documentClient: db, 58 | TableName: process.env.TodosTableName, 59 | pubsub 60 | }; 61 | 62 | manager.onMessage(parsedMessage, clientId, graphqlContext) 63 | .then(_ => { 64 | callback(); 65 | }) 66 | .catch(err => { 67 | console.log('Todo Api Handler Error'); 68 | console.log(JSON.stringify(err)); 69 | callback(); 70 | }); 71 | }; 72 | 73 | // Used to generate schema for apollo code gen / automatic type generation 74 | // TODO take this out of handler as it is not used post deployment 75 | export const schemaGenHandler = ({ query, variables }, context, callback) => { 76 | const runQuery = require('graphql-server-core').runQuery; 77 | const queryOptions = { 78 | schema, 79 | query, 80 | variables, 81 | // context: graphQLcontext, 82 | debug: false // unnecessarily shows stacktrace when I throw a custom error message (like accountId not found) 83 | }; 84 | runQuery(queryOptions) 85 | .then(res => { 86 | callback(null, res); 87 | }) 88 | .catch(err => { 89 | console.log('error is ' + err); 90 | callback(err); 91 | }); 92 | }; -------------------------------------------------------------------------------- /backend/todo-api/src/models/todo/todo.mutations.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLNonNull, 3 | GraphQLID 4 | } from 'graphql'; 5 | 6 | import { 7 | CreateTodoInput, 8 | UpdateTodoInput, 9 | DeleteTodoInput, 10 | Todo 11 | } from './todo.schema'; 12 | 13 | import uuidv4 from 'uuid/v4'; 14 | 15 | export default { 16 | createTodo: { 17 | type: Todo, 18 | description: 'Create a new todo item', 19 | args: { 20 | input: { 21 | type: new GraphQLNonNull(CreateTodoInput), 22 | description: 'Create todo input' 23 | } 24 | }, 25 | resolve(root, { input }, { documentClient, TableName, pubsub }) { 26 | input.id = uuidv4(); 27 | input.expiration = Date.now() + 1000 * 60 * 60 * 24 * 3; 28 | const params = { 29 | TableName, 30 | Item: input 31 | }; 32 | return documentClient.put(params).promise().then(_ => { 33 | // Publish with the subscription name as the triggerName 34 | return pubsub.publish('NEW_TODO', { teamTodoAdded: input }); 35 | }).then(res => { 36 | return input; 37 | }); 38 | } 39 | }, 40 | updateTodo: { 41 | type: Todo, 42 | description: 'Update an existing todo item', 43 | args: { 44 | input: { 45 | type: new GraphQLNonNull(UpdateTodoInput), 46 | description: 'Update todo input' 47 | } 48 | }, 49 | resolve(root, { input }, { documentClient, TableName }) { 50 | const params = { 51 | TableName, 52 | Item: input 53 | }; 54 | return documentClient.put(params).promise().then(_ => input); 55 | } 56 | }, 57 | deleteTodo: { 58 | type: GraphQLID, 59 | args: { 60 | input: { 61 | type: new GraphQLNonNull(DeleteTodoInput) 62 | } 63 | }, 64 | resolve(root, { input }, { documentClient, TableName }) { 65 | const params = { 66 | TableName, 67 | Key: input.id 68 | }; 69 | return documentClient.deleteTodo(params).promise().then(_ => input); 70 | } 71 | } 72 | }; -------------------------------------------------------------------------------- /backend/todo-api/src/models/todo/todo.queries.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInt, 3 | GraphQLString, 4 | GraphQLList 5 | } from 'graphql'; 6 | 7 | import { Todo } from './todo.schema'; 8 | 9 | export default { 10 | teamTodos: { 11 | type: new GraphQLList(Todo), 12 | description: 'List of todos for user', 13 | args: { 14 | first: { 15 | type: GraphQLInt, 16 | description: 'Returns the first n todo items of the user', 17 | defaultValue: 25 18 | }, 19 | teamName: { 20 | type: GraphQLString, 21 | description: 'Name of team' 22 | } 23 | }, 24 | resolve(source, { first, teamName }, { documentClient, TableName }) { 25 | let params = { 26 | TableName, 27 | IndexName: process.env.TeamNameToTodosIndex, 28 | KeyConditionExpression: 'teamName = :hkey', 29 | ExpressionAttributeValues: { 30 | ':hkey': teamName 31 | }, 32 | Limit: first 33 | }; 34 | return documentClient.query(params).promise().then(res => { 35 | return res.Items; 36 | }); 37 | } 38 | } 39 | }; -------------------------------------------------------------------------------- /backend/todo-api/src/models/todo/todo.schema.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLNonNull, 4 | GraphQLID, 5 | GraphQLString, 6 | GraphQLInputObjectType 7 | } from 'graphql'; 8 | 9 | export const Todo = new GraphQLObjectType({ 10 | name: 'Todo', 11 | description: 'A todo item', 12 | fields: () => ({ 13 | id: { 14 | type: new GraphQLNonNull(GraphQLID), 15 | description: 'Db uuid' 16 | }, 17 | name: { 18 | type: new GraphQLNonNull(GraphQLString), 19 | description: 'Unique friendly name for todo item' 20 | }, 21 | author: { 22 | type: new GraphQLNonNull(GraphQLString), 23 | description: 'Author who created item' 24 | }, 25 | content: { 26 | type: new GraphQLNonNull(GraphQLString), 27 | description: 'Content of todo item' 28 | }, 29 | teamName: { 30 | type: new GraphQLNonNull(GraphQLString), 31 | description: 'Team name' 32 | }, 33 | timestamp: { 34 | type: new GraphQLNonNull(GraphQLString), 35 | description: 'ISO date string' 36 | } 37 | }) 38 | }); 39 | 40 | export const CreateTodoInput = new GraphQLInputObjectType({ 41 | name: 'CreateTodoInput', 42 | fields: () => ({ 43 | timestamp: { type: new GraphQLNonNull(GraphQLString) }, 44 | name: { type: new GraphQLNonNull(GraphQLString) }, 45 | content: { type: new GraphQLNonNull(GraphQLString) }, 46 | teamName: { type: new GraphQLNonNull(GraphQLString) }, 47 | author: { type: new GraphQLNonNull(GraphQLString) } 48 | }) 49 | }); 50 | 51 | export const UpdateTodoInput = CreateTodoInput; 52 | 53 | export const DeleteTodoInput = new GraphQLInputObjectType({ 54 | name: 'DeleteTodoInput', 55 | fields: () => ({ 56 | id: { type: new GraphQLNonNull(GraphQLID) } 57 | }) 58 | }); -------------------------------------------------------------------------------- /backend/todo-api/src/models/todo/todo.subscriptions.js: -------------------------------------------------------------------------------- 1 | import { Todo } from './todo.schema'; 2 | 3 | import { GraphQLString } from 'graphql'; 4 | 5 | export default { 6 | teamTodoAdded: { 7 | type: Todo, 8 | args: { 9 | teamName: { 10 | type: GraphQLString, 11 | description: 'Team name filter for todoAdded subscription' 12 | } 13 | }, 14 | description: 'New todo added', 15 | subscribe: () => {} // no logic should be here. See Publisher in ws transport for setting up filter functions and mapping subscription names to channels. 16 | } 17 | }; -------------------------------------------------------------------------------- /backend/todo-api/src/root.mutation.js: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql'; 2 | 3 | import todo from './models/todo/todo.mutations'; 4 | 5 | const rootFields = Object.assign({}, 6 | todo 7 | ); 8 | 9 | export default new GraphQLObjectType({ 10 | name: 'Mutation', 11 | fields: () => rootFields 12 | }); 13 | -------------------------------------------------------------------------------- /backend/todo-api/src/root.query.js: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql'; 2 | 3 | import todo from './models/todo/todo.queries'; 4 | 5 | const rootFields = Object.assign({}, 6 | todo 7 | ); 8 | 9 | export default new GraphQLObjectType({ 10 | name: 'Query', 11 | fields: () => rootFields 12 | }); 13 | -------------------------------------------------------------------------------- /backend/todo-api/src/root.schema.js: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | 3 | import query from './root.query'; 4 | import mutation from './root.mutation'; 5 | import subscription from './root.subscription'; 6 | 7 | export default new GraphQLSchema({ query, mutation, subscription }); 8 | -------------------------------------------------------------------------------- /backend/todo-api/src/root.subscription.js: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql'; 2 | 3 | import todo from './models/todo/todo.subscriptions'; 4 | 5 | const rootFields = Object.assign({}, 6 | todo 7 | ); 8 | 9 | export default new GraphQLObjectType({ 10 | name: 'Subscription', 11 | fields: () => rootFields 12 | }); -------------------------------------------------------------------------------- /backend/todo-api/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | target: 'node', 7 | output: { 8 | path: path.join(process.cwd(), 'dist'), 9 | filename: 'index.js', 10 | libraryTarget: 'commonjs2' 11 | }, 12 | devtool: 'source-map', 13 | externals: ['aws-sdk'], 14 | 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js$/, 19 | loader: 'babel-loader', 20 | options: { 21 | cacheDirectory: true, 22 | presets: [require('babel-preset-stage-2')], 23 | plugins: [require('babel-plugin-transform-es2015-modules-commonjs')] 24 | }, 25 | exclude: [/node_modules/] 26 | } 27 | ] 28 | }, 29 | resolve: { 30 | alias: { 31 | "graphql": path.resolve(__dirname, "node_modules/graphql") 32 | } 33 | }, 34 | plugins: [ 35 | new Webpack.NoEmitOnErrorsPlugin() 36 | ] 37 | }; 38 | -------------------------------------------------------------------------------- /backend/todo-backend.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Parameters: 3 | AppPrefix: 4 | Type: String 5 | Default: IOX 6 | IotEndpoint: 7 | Type: String 8 | Default: afbxc4n5814kd.iot.us-west-2.amazonaws.com 9 | Description: Iot Endpoint for region in which stack is being deployed 10 | SubscriptionToClientIdsIndex: 11 | Type: String 12 | Default: subscriptionToClientId 13 | Description: Index name for subscription to ClientIds 14 | ClientIdToSubscriptionsIndex: 15 | Type: String 16 | Default: clientIdToSubscription 17 | Description: Index name for ClientId to Subscription Names index 18 | TeamNameToTodosIndex: 19 | Type: String 20 | Default: teamNameToTodos 21 | Description: Index name for ClientId to Subscription Names index 22 | BackendCodeBucket: 23 | Type: String 24 | Default: iox-playground 25 | TodoApiKey: 26 | Type: String 27 | Default: todo-api.zip 28 | SubscriptionPublisherKey: 29 | Type: String 30 | Default: todo-subscription-publisher.zip 31 | SubscriptionPrunerKey: 32 | Type: String 33 | Default: todo-subscription-pruner.zip 34 | OriginAccessIdentity: 35 | Type: String 36 | MinLength: 2 37 | Description: 'You need to create an origin access identity via aws console. Required for cloudfront distribution' 38 | MaxDynamoDbAutoScalingCapacity: 39 | Type: Number 40 | Default: 10 41 | MinDynamoDbAutoScalingCapacity: 42 | Type: Number 43 | Default: 5 44 | 45 | Resources: 46 | # Api that runs graphql serverless subscriptions transport - handles all messages from socket 47 | TodoApi: 48 | Type: AWS::Lambda::Function 49 | Properties: 50 | Handler: index.handler 51 | Role: !GetAtt LambdaExecutionRole.Arn 52 | Code: 53 | S3Bucket: !Ref BackendCodeBucket 54 | S3Key: !Ref TodoApiKey 55 | Environment: 56 | Variables: 57 | AppPrefix: !Ref AppPrefix 58 | SubscriptionsTableName: !Ref SubscriptionsTable 59 | SubscriptionPublisherFunctionName: !Ref SubscriptionPublisher 60 | TodosTableName: !Ref TodosTable 61 | TeamNameToTodosIndex: !Ref TeamNameToTodosIndex 62 | IotEndpoint: !Ref IotEndpoint 63 | Runtime: 'nodejs6.10' 64 | Timeout: 10 65 | TracingConfig: 66 | Mode: "Active" 67 | TodoApiInvokePermission: 68 | Type: AWS::Lambda::Permission 69 | Properties: 70 | FunctionName: !GetAtt TodoApi.Arn 71 | Action: lambda:InvokeFunction 72 | Principal: iot.amazonaws.com 73 | SourceAccount: !Ref AWS::AccountId 74 | 75 | # Publisher publishes to all client ids for a particular subscription 76 | SubscriptionPublisher: 77 | Type: AWS::Lambda::Function 78 | Properties: 79 | Handler: index.handler 80 | Role: !GetAtt LambdaExecutionRole.Arn 81 | Code: 82 | S3Bucket: !Ref BackendCodeBucket 83 | S3Key: !Ref SubscriptionPublisherKey 84 | Environment: 85 | Variables: 86 | AppPrefix: !Ref AppPrefix 87 | SubscriptionsTableName: !Ref SubscriptionsTable 88 | SubscriptionToClientIdsIndex: !Ref SubscriptionToClientIdsIndex 89 | TodosTableName: !Ref TodosTable 90 | IotEndpoint: !Ref IotEndpoint 91 | Runtime: 'nodejs6.10' 92 | MemorySize: 384 93 | Timeout: 60 94 | TracingConfig: 95 | Mode: "Active" 96 | 97 | # Pruner removes client data from all existing subscriptions in subscriptions table on disconnect event 98 | SubscriptionPruner: 99 | Type: AWS::Lambda::Function 100 | Properties: 101 | Handler: index.handler 102 | Role: !GetAtt LambdaExecutionRole.Arn 103 | Code: 104 | S3Bucket: !Ref BackendCodeBucket 105 | S3Key: !Ref SubscriptionPrunerKey 106 | Environment: 107 | Variables: 108 | SubscriptionsTableName: !Ref SubscriptionsTable 109 | ClientIdToSubscriptionsIndex: !Ref ClientIdToSubscriptionsIndex 110 | Runtime: 'nodejs6.10' 111 | Timeout: 60 112 | TracingConfig: 113 | Mode: "Active" 114 | SubscriptionPrunerInvokePermission: 115 | Type: AWS::Lambda::Permission 116 | Properties: 117 | FunctionName: !GetAtt SubscriptionPruner.Arn 118 | Action: lambda:InvokeFunction 119 | Principal: iot.amazonaws.com 120 | SourceAccount: !Ref AWS::AccountId 121 | 122 | # Shared execution role for lambdas 123 | LambdaExecutionRole: 124 | Type: AWS::IAM::Role 125 | Properties: 126 | AssumeRolePolicyDocument: 127 | Version: '2012-10-17' 128 | Statement: 129 | - Effect: Allow 130 | Principal: 131 | Service: 132 | - lambda.amazonaws.com 133 | Action: 134 | - sts:AssumeRole 135 | Path: '/' 136 | Policies: 137 | - 138 | PolicyName: "root" 139 | PolicyDocument: 140 | Version: "2012-10-17" 141 | Statement: 142 | - 143 | Effect: Allow 144 | Action: 145 | - xray:PutTraceSegments 146 | - xray:PutTelemetryRecords 147 | Resource: '*' 148 | 149 | LambdaExecutionPolicy: 150 | Type: 'AWS::IAM::Policy' 151 | Properties: 152 | PolicyName: !Sub ${AWS::StackName}LambdaExecutionPolicy 153 | PolicyDocument: 154 | Version: "2012-10-17" 155 | Statement: 156 | - 157 | Effect: Allow 158 | Action: 159 | - dynamodb:DeleteItem 160 | - dynamodb:GetItem 161 | - dynamodb:BatchGetItem 162 | - dynamodb:PutItem 163 | - dynamodb:Query 164 | - dynamodb:Scan 165 | Resource: 166 | - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${TodosTable} 167 | - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${TodosTable}/index/${TeamNameToTodosIndex} 168 | - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${SubscriptionsTable} 169 | - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${SubscriptionsTable}/index/${SubscriptionToClientIdsIndex} 170 | - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${SubscriptionsTable}/index/${ClientIdToSubscriptionsIndex} 171 | - 172 | Effect: Allow 173 | Action: 174 | - iot:Publish 175 | Resource: '*' # below restriction should work - to debug later 176 | # - !Sub arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/${AppPrefix}/in/* # app prefix = namespace 177 | - 178 | Effect: Allow 179 | Action: 180 | - iot:Receive 181 | Resource: 182 | - !Sub arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/$aws/events/* # lifecycle events 183 | - 184 | Effect: Allow 185 | Action: logs:* 186 | Resource: '*' 187 | - 188 | Effect: Allow 189 | Action: lambda:InvokeFunction 190 | Resource: 191 | - !GetAtt SubscriptionPublisher.Arn 192 | Roles: 193 | - !Ref LambdaExecutionRole 194 | TodoIdentityPool: 195 | Type: AWS::Cognito::IdentityPool 196 | Properties: 197 | AllowUnauthenticatedIdentities: true 198 | TodoIdentityPoolRole: 199 | Type: 'AWS::Cognito::IdentityPoolRoleAttachment' 200 | Properties: 201 | IdentityPoolId: !Ref TodoIdentityPool 202 | Roles: 203 | unauthenticated: !GetAtt TodoIdentityPoolUnauthenticatedRole.Arn 204 | TodoIdentityPoolUnauthenticatedRole: 205 | Type: 'AWS::IAM::Role' 206 | Properties: 207 | AssumeRolePolicyDocument: 208 | Version: '2012-10-17' 209 | Statement: 210 | - 211 | Effect: 'Allow' 212 | Principal: 213 | Federated: 214 | - 'cognito-identity.amazonaws.com' 215 | Action: 216 | - 'sts:AssumeRoleWithWebIdentity' 217 | Condition: 218 | StringEquals: 219 | 'cognito-identity.amazonaws.com:aud': !Ref TodoIdentityPool 220 | 'ForAnyValue:StringLike': 221 | 'cognito-identity.amazonaws.com:amr': 'unauthenticated' 222 | Path: '/' 223 | Policies: 224 | - 225 | PolicyName: 'unauthpolicy' 226 | PolicyDocument: 227 | Version: '2012-10-17' 228 | Statement: 229 | - 230 | Effect: Allow 231 | Action: iot:Connect 232 | Resource: 233 | - !Sub arn:aws:iot:${AWS::Region}:${AWS::AccountId}:client/* 234 | - 235 | Effect: Allow 236 | Action: iot:Publish 237 | Resource: 238 | - !Sub arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/${AppPrefix}/out 239 | - 240 | Effect: Allow 241 | Action: 242 | - iot:Subscribe 243 | Resource: 244 | - !Join ['', ['arn:aws:iot:', !Ref 'AWS::Region',':', !Ref 'AWS::AccountId', ':topicfilter/', !Ref AppPrefix, '/in/${iot:ClientId}']] 245 | - 246 | Effect: Allow 247 | Action: 248 | - iot:Receive 249 | Resource: 250 | - !Join ['', ['arn:aws:iot:', !Ref 'AWS::Region', ':', !Ref 'AWS::AccountId', ':topic/', !Ref AppPrefix, '/in/${iot:ClientId}']] 251 | TodosTable: 252 | Type: 'AWS::DynamoDB::Table' 253 | Properties: 254 | AttributeDefinitions: 255 | - 256 | AttributeName: 'id' 257 | AttributeType: 'S' 258 | - 259 | AttributeName: 'teamName' 260 | AttributeType: 'S' 261 | KeySchema: 262 | - 263 | AttributeName: 'id' 264 | KeyType: 'HASH' 265 | ProvisionedThroughput: 266 | ReadCapacityUnits: 1 267 | WriteCapacityUnits: 1 268 | TimeToLiveSpecification: 269 | AttributeName: expiration 270 | Enabled: True 271 | GlobalSecondaryIndexes: 272 | - 273 | IndexName: !Ref TeamNameToTodosIndex 274 | KeySchema: 275 | - 276 | AttributeName: teamName 277 | KeyType: HASH 278 | Projection: 279 | ProjectionType: 'ALL' 280 | ProvisionedThroughput: 281 | ReadCapacityUnits: 1 282 | WriteCapacityUnits: 1 283 | # AWS IOT Rules 284 | TodoApiIoTRule: 285 | Type: 'AWS::IoT::TopicRule' 286 | Properties: 287 | TopicRulePayload: 288 | Actions: 289 | - Lambda: 290 | FunctionArn: !GetAtt TodoApi.Arn 291 | RuleDisabled: false 292 | Sql: !Sub SELECT *, clientId() AS clientId FROM '${AppPrefix}/out' 293 | SubscriptionPrunerIoTRule: 294 | Type: 'AWS::IoT::TopicRule' 295 | Properties: 296 | TopicRulePayload: 297 | Actions: 298 | - Lambda: 299 | FunctionArn: !GetAtt SubscriptionPruner.Arn 300 | RuleDisabled: false 301 | Sql: SELECT * FROM '$aws/events/presence/disconnected/#' 302 | SubscriptionsTable: 303 | Type: AWS::DynamoDB::Table 304 | Properties: 305 | AttributeDefinitions: 306 | - 307 | AttributeName: clientId 308 | AttributeType: S 309 | - 310 | AttributeName: subscriptionName 311 | AttributeType: S 312 | KeySchema: 313 | - 314 | AttributeName: clientId 315 | KeyType: HASH 316 | - 317 | AttributeName: subscriptionName 318 | KeyType: RANGE 319 | ProvisionedThroughput: 320 | ReadCapacityUnits: 1 321 | WriteCapacityUnits: 1 322 | GlobalSecondaryIndexes: 323 | - 324 | IndexName: !Ref SubscriptionToClientIdsIndex 325 | KeySchema: 326 | - 327 | AttributeName: subscriptionName 328 | KeyType: HASH 329 | Projection: 330 | ProjectionType: 'ALL' 331 | ProvisionedThroughput: 332 | ReadCapacityUnits: 1 333 | WriteCapacityUnits: 1 334 | - 335 | IndexName: !Ref ClientIdToSubscriptionsIndex 336 | KeySchema: 337 | - 338 | AttributeName: clientId 339 | KeyType: HASH 340 | Projection: 341 | NonKeyAttributes: 342 | - subscriptionName 343 | ProjectionType: 'INCLUDE' 344 | ProvisionedThroughput: 345 | ReadCapacityUnits: 1 346 | WriteCapacityUnits: 1 347 | 348 | # Domain for demo 349 | WebsiteBucket: 350 | Type: AWS::S3::Bucket 351 | Properties: 352 | AccessControl: PublicRead 353 | WebsiteConfiguration: 354 | IndexDocument: index.html 355 | ErrorDocument: 404.html 356 | CorsConfiguration: 357 | CorsRules: 358 | - AllowedHeaders: 359 | - '*' 360 | AllowedMethods: 361 | - GET 362 | - PUT 363 | - POST 364 | - HEAD 365 | - DELETE 366 | AllowedOrigins: 367 | - '*' 368 | ExposedHeaders: 369 | - Date 370 | MaxAge: '3600' 371 | WebsiteBucketPolicy: 372 | Type: AWS::S3::BucketPolicy 373 | Properties: 374 | Bucket: !Ref 'WebsiteBucket' 375 | PolicyDocument: 376 | Statement: 377 | - Action: 378 | - s3:GetObject 379 | Effect: Allow 380 | Resource: !Join ['', ['arn:aws:s3:::', !Ref 'WebsiteBucket', /*]] 381 | Principal: '*' 382 | CloudfrontDistribution: 383 | Type: AWS::CloudFront::Distribution 384 | Properties: 385 | DistributionConfig: 386 | Origins: 387 | - DomainName: !GetAtt WebsiteBucket.DomainName 388 | Id: graphqlAWSIOTTransportExample 389 | S3OriginConfig: 390 | OriginAccessIdentity: !Ref OriginAccessIdentity 391 | Enabled: 'true' 392 | DefaultRootObject: index.html 393 | # Aliases: 394 | # - todo.girishnanda.com // update with valid alias that matches custom domain 395 | DefaultCacheBehavior: 396 | AllowedMethods: 397 | - DELETE 398 | - GET 399 | - HEAD 400 | - OPTIONS 401 | - PATCH 402 | - POST 403 | - PUT 404 | TargetOriginId: graphqlAWSIOTTransportExample 405 | ForwardedValues: 406 | QueryString: 'false' 407 | Cookies: 408 | Forward: none 409 | ViewerProtocolPolicy: redirect-to-https 410 | PriceClass: PriceClass_200 411 | # ViewerCertificate: // only required if you are using a custom domain 412 | # AcmCertificateArn: // update with valid acm certificate arn 413 | # SslSupportMethod: sni-only 414 | CustomErrorResponses: 415 | - ErrorCachingMinTTL: '0' 416 | ErrorCode: '404' 417 | ResponseCode: '200' 418 | ResponsePagePath: /index.html 419 | 420 | # DDB Autoscaling policies 421 | TodosTableWriteCapacityScalableTarget: 422 | Type: AWS::ApplicationAutoScaling::ScalableTarget 423 | Properties: 424 | MaxCapacity: !Ref MaxDynamoDbAutoScalingCapacity 425 | MinCapacity: !Ref MinDynamoDbAutoScalingCapacity 426 | ResourceId: !Join [ /, [table, !Ref TodosTable]] 427 | RoleARN: !GetAtt ScalingRole.Arn 428 | ScalableDimension: dynamodb:table:WriteCapacityUnits 429 | ServiceNamespace: dynamodb 430 | TodosTableWriteScalingPolicy: 431 | Type: "AWS::ApplicationAutoScaling::ScalingPolicy" 432 | Properties: 433 | PolicyName: !Sub ${AWS::StackId}TodosTableWriteAutoScalingPolicy 434 | PolicyType: TargetTrackingScaling 435 | ScalingTargetId: !Ref TodosTableWriteCapacityScalableTarget 436 | TargetTrackingScalingPolicyConfiguration: 437 | TargetValue: 70.0 438 | ScaleInCooldown: 60 439 | ScaleOutCooldown: 60 440 | PredefinedMetricSpecification: 441 | PredefinedMetricType: DynamoDBWriteCapacityUtilization 442 | TodosTableReadCapacityScalableTarget: 443 | Type: AWS::ApplicationAutoScaling::ScalableTarget 444 | Properties: 445 | MaxCapacity: !Ref MaxDynamoDbAutoScalingCapacity 446 | MinCapacity: !Ref MinDynamoDbAutoScalingCapacity 447 | ResourceId: !Join [ /, [table, !Ref TodosTable]] 448 | RoleARN: !GetAtt ScalingRole.Arn 449 | ScalableDimension: dynamodb:table:ReadCapacityUnits 450 | ServiceNamespace: dynamodb 451 | TodosTableReadScalingPolicy: 452 | Type: "AWS::ApplicationAutoScaling::ScalingPolicy" 453 | Properties: 454 | PolicyName: !Sub ${AWS::StackId}TodosTableReadAutoScalingPolicy 455 | PolicyType: TargetTrackingScaling 456 | ScalingTargetId: !Ref TodosTableReadCapacityScalableTarget 457 | TargetTrackingScalingPolicyConfiguration: 458 | TargetValue: 70.0 459 | ScaleInCooldown: 60 460 | ScaleOutCooldown: 60 461 | PredefinedMetricSpecification: 462 | PredefinedMetricType: DynamoDBReadCapacityUtilization 463 | TeamNameToTodosIndexWriteCapacityScalableTarget: 464 | Type: AWS::ApplicationAutoScaling::ScalableTarget 465 | Properties: 466 | MaxCapacity: !Ref MaxDynamoDbAutoScalingCapacity 467 | MinCapacity: !Ref MinDynamoDbAutoScalingCapacity 468 | ResourceId: !Join [ /, [table, !Ref TodosTable, index, !Ref TeamNameToTodosIndex]] 469 | RoleARN: !GetAtt ScalingRole.Arn 470 | ScalableDimension: dynamodb:index:WriteCapacityUnits 471 | ServiceNamespace: dynamodb 472 | TeamNameToTodosIndexWriteScalingPolicy: 473 | Type: "AWS::ApplicationAutoScaling::ScalingPolicy" 474 | Properties: 475 | PolicyName: !Sub ${AWS::StackId}TeamNameToTodosIndexWriteAutoScalingPolicy 476 | PolicyType: TargetTrackingScaling 477 | ScalingTargetId: !Ref TeamNameToTodosIndexWriteCapacityScalableTarget 478 | TargetTrackingScalingPolicyConfiguration: 479 | TargetValue: 70.0 480 | ScaleInCooldown: 60 481 | ScaleOutCooldown: 60 482 | PredefinedMetricSpecification: 483 | PredefinedMetricType: DynamoDBWriteCapacityUtilization 484 | TeamNameToTodosIndexReadCapacityScalableTarget: 485 | Type: AWS::ApplicationAutoScaling::ScalableTarget 486 | Properties: 487 | MaxCapacity: !Ref MaxDynamoDbAutoScalingCapacity 488 | MinCapacity: !Ref MinDynamoDbAutoScalingCapacity 489 | ResourceId: !Join [ /, [table, !Ref TodosTable, index, !Ref TeamNameToTodosIndex]] 490 | RoleARN: !GetAtt ScalingRole.Arn 491 | ScalableDimension: dynamodb:index:ReadCapacityUnits 492 | ServiceNamespace: dynamodb 493 | TeamNameToTodosIndexReadScalingPolicy: 494 | Type: "AWS::ApplicationAutoScaling::ScalingPolicy" 495 | Properties: 496 | PolicyName: !Sub ${AWS::StackId}TeamNameToTodosIndexReadAutoScalingPolicy 497 | PolicyType: TargetTrackingScaling 498 | ScalingTargetId: !Ref TeamNameToTodosIndexReadCapacityScalableTarget 499 | TargetTrackingScalingPolicyConfiguration: 500 | TargetValue: 70.0 501 | ScaleInCooldown: 60 502 | ScaleOutCooldown: 60 503 | PredefinedMetricSpecification: 504 | PredefinedMetricType: DynamoDBReadCapacityUtilization 505 | SubscriptionsTableWriteCapacityScalableTarget: 506 | Type: AWS::ApplicationAutoScaling::ScalableTarget 507 | Properties: 508 | MaxCapacity: !Ref MaxDynamoDbAutoScalingCapacity 509 | MinCapacity: !Ref MinDynamoDbAutoScalingCapacity 510 | ResourceId: !Join [ /, [table, !Ref SubscriptionsTable]] 511 | RoleARN: !GetAtt ScalingRole.Arn 512 | ScalableDimension: dynamodb:table:WriteCapacityUnits 513 | ServiceNamespace: dynamodb 514 | SubscriptionsTableWriteScalingPolicy: 515 | Type: "AWS::ApplicationAutoScaling::ScalingPolicy" 516 | Properties: 517 | PolicyName: !Sub ${AWS::StackId}SubscriptionsTableWriteAutoScalingPolicy 518 | PolicyType: TargetTrackingScaling 519 | ScalingTargetId: !Ref SubscriptionsTableWriteCapacityScalableTarget 520 | TargetTrackingScalingPolicyConfiguration: 521 | TargetValue: 70.0 522 | ScaleInCooldown: 60 523 | ScaleOutCooldown: 60 524 | PredefinedMetricSpecification: 525 | PredefinedMetricType: DynamoDBWriteCapacityUtilization 526 | SubscriptionsTableReadCapacityScalableTarget: 527 | Type: AWS::ApplicationAutoScaling::ScalableTarget 528 | Properties: 529 | MaxCapacity: !Ref MaxDynamoDbAutoScalingCapacity 530 | MinCapacity: !Ref MinDynamoDbAutoScalingCapacity 531 | ResourceId: !Join [ /, [table, !Ref SubscriptionsTable]] 532 | RoleARN: !GetAtt ScalingRole.Arn 533 | ScalableDimension: dynamodb:table:ReadCapacityUnits 534 | ServiceNamespace: dynamodb 535 | SubscriptionsTableReadScalingPolicy: 536 | Type: "AWS::ApplicationAutoScaling::ScalingPolicy" 537 | Properties: 538 | PolicyName: !Sub ${AWS::StackId}SubscriptionsTableReadAutoScalingPolicy 539 | PolicyType: TargetTrackingScaling 540 | ScalingTargetId: !Ref SubscriptionsTableReadCapacityScalableTarget 541 | TargetTrackingScalingPolicyConfiguration: 542 | TargetValue: 70.0 543 | ScaleInCooldown: 60 544 | ScaleOutCooldown: 60 545 | PredefinedMetricSpecification: 546 | PredefinedMetricType: DynamoDBReadCapacityUtilization 547 | SubscriptionToClientIdsIndexWriteCapacityScalableTarget: 548 | Type: AWS::ApplicationAutoScaling::ScalableTarget 549 | Properties: 550 | MaxCapacity: !Ref MaxDynamoDbAutoScalingCapacity 551 | MinCapacity: !Ref MinDynamoDbAutoScalingCapacity 552 | ResourceId: !Join [ /, [table, !Ref SubscriptionsTable, index, !Ref SubscriptionToClientIdsIndex]] 553 | RoleARN: !GetAtt ScalingRole.Arn 554 | ScalableDimension: dynamodb:index:WriteCapacityUnits 555 | ServiceNamespace: dynamodb 556 | SubscriptionToClientIdsIndexWriteScalingPolicy: 557 | Type: "AWS::ApplicationAutoScaling::ScalingPolicy" 558 | Properties: 559 | PolicyName: !Sub ${AWS::StackId}SubscriptionToClientIdsWriteAutoScalingPolicy 560 | PolicyType: TargetTrackingScaling 561 | ScalingTargetId: !Ref SubscriptionToClientIdsIndexWriteCapacityScalableTarget 562 | TargetTrackingScalingPolicyConfiguration: 563 | TargetValue: 70.0 564 | ScaleInCooldown: 60 565 | ScaleOutCooldown: 60 566 | PredefinedMetricSpecification: 567 | PredefinedMetricType: DynamoDBWriteCapacityUtilization 568 | SubscriptionToClientIdsIndexReadCapacityScalableTarget: 569 | Type: AWS::ApplicationAutoScaling::ScalableTarget 570 | Properties: 571 | MaxCapacity: !Ref MaxDynamoDbAutoScalingCapacity 572 | MinCapacity: !Ref MinDynamoDbAutoScalingCapacity 573 | ResourceId: !Join [ /, [table, !Ref SubscriptionsTable, index, !Ref SubscriptionToClientIdsIndex]] 574 | RoleARN: !GetAtt ScalingRole.Arn 575 | ScalableDimension: dynamodb:index:ReadCapacityUnits 576 | ServiceNamespace: dynamodb 577 | SubscriptionToClientIdsIndexReadScalingPolicy: 578 | Type: "AWS::ApplicationAutoScaling::ScalingPolicy" 579 | Properties: 580 | PolicyName: !Sub ${AWS::StackId}SubscriptionToClientIdsReadAutoScalingPolicy 581 | PolicyType: TargetTrackingScaling 582 | ScalingTargetId: !Ref SubscriptionToClientIdsIndexReadCapacityScalableTarget 583 | TargetTrackingScalingPolicyConfiguration: 584 | TargetValue: 70.0 585 | ScaleInCooldown: 60 586 | ScaleOutCooldown: 60 587 | PredefinedMetricSpecification: 588 | PredefinedMetricType: DynamoDBReadCapacityUtilization 589 | ClientIdToSubscriptionsIndexWriteCapacityScalableTarget: 590 | Type: AWS::ApplicationAutoScaling::ScalableTarget 591 | Properties: 592 | MaxCapacity: !Ref MaxDynamoDbAutoScalingCapacity 593 | MinCapacity: !Ref MinDynamoDbAutoScalingCapacity 594 | ResourceId: !Join [ /, [table, !Ref SubscriptionsTable, index, !Ref ClientIdToSubscriptionsIndex]] 595 | RoleARN: !GetAtt ScalingRole.Arn 596 | ScalableDimension: dynamodb:index:WriteCapacityUnits 597 | ServiceNamespace: dynamodb 598 | ClientIdToSubscriptionsIndexWriteScalingPolicy: 599 | Type: "AWS::ApplicationAutoScaling::ScalingPolicy" 600 | Properties: 601 | PolicyName: !Sub ${AWS::StackId}ClientIdToSubscriptionsIndexWriteAutoScalingPolicy 602 | PolicyType: TargetTrackingScaling 603 | ScalingTargetId: !Ref ClientIdToSubscriptionsIndexWriteCapacityScalableTarget 604 | TargetTrackingScalingPolicyConfiguration: 605 | TargetValue: 70.0 606 | ScaleInCooldown: 60 607 | ScaleOutCooldown: 60 608 | PredefinedMetricSpecification: 609 | PredefinedMetricType: DynamoDBWriteCapacityUtilization 610 | ClientIdToSubscriptionsIndexReadCapacityScalableTarget: 611 | Type: AWS::ApplicationAutoScaling::ScalableTarget 612 | Properties: 613 | MaxCapacity: !Ref MaxDynamoDbAutoScalingCapacity 614 | MinCapacity: !Ref MinDynamoDbAutoScalingCapacity 615 | ResourceId: !Join [ /, [table, !Ref SubscriptionsTable, index, !Ref ClientIdToSubscriptionsIndex]] 616 | RoleARN: !GetAtt ScalingRole.Arn 617 | ScalableDimension: dynamodb:index:ReadCapacityUnits 618 | ServiceNamespace: dynamodb 619 | ClientIdToSubscriptionsIndexReadScalingPolicy: 620 | Type: "AWS::ApplicationAutoScaling::ScalingPolicy" 621 | Properties: 622 | PolicyName: !Sub ${AWS::StackId}ClientIdToSubscriptionsIndexReadAutoScalingPolicy 623 | PolicyType: TargetTrackingScaling 624 | ScalingTargetId: !Ref ClientIdToSubscriptionsIndexReadCapacityScalableTarget 625 | TargetTrackingScalingPolicyConfiguration: 626 | TargetValue: 70.0 627 | ScaleInCooldown: 60 628 | ScaleOutCooldown: 60 629 | PredefinedMetricSpecification: 630 | PredefinedMetricType: DynamoDBReadCapacityUtilization 631 | ScalingRole: 632 | Type: "AWS::IAM::Role" 633 | Properties: 634 | AssumeRolePolicyDocument: 635 | Version: "2012-10-17" 636 | Statement: 637 | - 638 | Effect: "Allow" 639 | Principal: 640 | Service: 641 | - application-autoscaling.amazonaws.com 642 | Action: 643 | - "sts:AssumeRole" 644 | Path: "/" 645 | Policies: 646 | - 647 | PolicyName: "root" 648 | PolicyDocument: 649 | Version: "2012-10-17" 650 | Statement: 651 | - 652 | Effect: "Allow" 653 | Action: 654 | - "dynamodb:DescribeTable" 655 | - "dynamodb:UpdateTable" 656 | - "cloudwatch:PutMetricAlarm" 657 | - "cloudwatch:DescribeAlarms" 658 | - "cloudwatch:GetMetricStatistics" 659 | - "cloudwatch:SetAlarmState" 660 | - "cloudwatch:DeleteAlarms" 661 | Resource: "*" 662 | Outputs: 663 | WebsiteBucketName: 664 | Description: Bucket to upload client build. Needs to be set in client app package.json to upload new build 665 | Value: !Ref WebsiteBucket 666 | WebsiteBucketUrl: 667 | Description: Url to view client app once build is uploaded to website bucket 668 | Value: !Join ['', ['https://', !GetAtt CloudfrontDistribution.DomainName]] 669 | IdentityPoolId: 670 | Description: Identity pool id. Needs to be set in client app environment variables 671 | Value: !Ref TodoIdentityPool 672 | IotEndpoint: 673 | Description: Iot endpoint. Needs to be updated in client app environment variables 674 | Value: !Ref IotEndpoint 675 | 676 | -------------------------------------------------------------------------------- /backend/todo-subscription-pruner/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "globals": { 6 | "Promise": true 7 | }, 8 | "plugins": [ 9 | "import" 10 | ], 11 | "parser": "babel-eslint", 12 | "parserOptions": { 13 | "ecmaVersion": 6, 14 | "sourceType": "module", 15 | "ecmaFeatures": { 16 | "es6": true 17 | } 18 | }, 19 | "rules": { 20 | "import/no-unresolved": [ 21 | 2, 22 | { 23 | "commonjs": true, 24 | "amd": true 25 | } 26 | ], 27 | "semi": 2, 28 | "no-undef": 2, 29 | "no-unused-vars": [ 30 | "error", 31 | { 32 | "argsIgnorePattern": "^_" 33 | } 34 | ], 35 | "comma-dangle": 2 36 | } 37 | } -------------------------------------------------------------------------------- /backend/todo-subscription-pruner/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | -------------------------------------------------------------------------------- /backend/todo-subscription-pruner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-subscription-pruner", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "config": { 7 | "devFunctionName": "todo-ws-transport-example-SubscriptionPruner-1T8BQL49S8QLF", 8 | "devCodeBucketName": "iox-playground", 9 | "codeKey": "todo-subscription-pruner.zip", 10 | "profile": "iox", 11 | "region": "us-west-2" 12 | }, 13 | "scripts": { 14 | "prebuild": "rm -rf dist && mkdir -p dist", 15 | "build": "webpack", 16 | "prezip": "rm -rf build && mkdir -p build", 17 | "zip": "cd dist && zip -r ../build/build.zip *", 18 | "lint": "eslint src", 19 | "lint:dist": "eslint dist index.js", 20 | "preupdate": "npm run build && npm run zip", 21 | "update": "aws lambda update-function-code --function-name $npm_package_config_devFunctionName --zip-file fileb://$PWD/build/build.zip --profile $npm_package_config_profile --region $npm_package_config_region", 22 | "updateS3": "npm run preupdate && aws s3 cp build/build.zip s3://$npm_package_config_devCodeBucketName/$npm_package_config_codeKey --profile $npm_package_config_profile --region $npm_package_config_region", 23 | "schema:generate": "babel src --out-dir dist -s && node output-schema.js" 24 | }, 25 | "author": "", 26 | "license": "ISC", 27 | "devDependencies": { 28 | "aws-sdk": "^2.102.0", 29 | "babel-cli": "^6.24.0", 30 | "babel-core": "^6.26.0", 31 | "babel-eslint": "^6.1.2", 32 | "babel-loader": "^7.1.2", 33 | "babel-plugin-transform-es2015-modules-commonjs": "^6.24.0", 34 | "babel-preset-env": "^1.3.3", 35 | "babel-preset-stage-2": "^6.22.0", 36 | "eslint": "^3.18.0", 37 | "eslint-plugin-import": "^2.2.0", 38 | "webpack": "^3.5.5" 39 | }, 40 | "dependencies": { 41 | "aws-xray-sdk": "^1.1.4", 42 | "graphql": "^0.11.2", 43 | "graphql-aws-iot-server": "^0.0.1", 44 | "graphql-subscriptions": "^0.4.4", 45 | "source-map-support": "^0.4.16" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /backend/todo-subscription-pruner/src/index.js: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register'; 2 | 3 | import AWSXray from 'aws-xray-sdk'; 4 | var AWS = AWSXray.captureAWS(require('aws-sdk')); // eslint-disable-line 5 | 6 | // Currently only subscribed to the AWS IoT disconnected lifecycle event 7 | 8 | let db; 9 | 10 | export const handler = (event, context, callback) => { 11 | console.log(JSON.stringify(event)); 12 | const { clientId } = event; 13 | 14 | if (!db) { 15 | db = new AWS.DynamoDB.DocumentClient(); 16 | } 17 | 18 | onDisconnect(clientId); 19 | 20 | function onDisconnect(clientId) { 21 | const params = { 22 | TableName: process.env.SubscriptionsTableName, 23 | IndexName: process.env.ClientIdToSubscriptionsIndex, 24 | KeyConditionExpression: 'clientId = :hkey', 25 | ExpressionAttributeValues: { 26 | ':hkey': clientId 27 | } 28 | }; 29 | 30 | return db.query(params).promise().then(res => { 31 | let promises = []; 32 | if (res.Items && res.Items.length) { 33 | res.Items.forEach(item => { 34 | const deleteParams = { 35 | TableName: process.env.SubscriptionsTableName, 36 | Key: { 37 | clientId, 38 | subscriptionName: item.subscriptionName 39 | } 40 | }; 41 | promises.push(db.delete(deleteParams).promise()); 42 | }); 43 | return Promise.all(promises); 44 | } 45 | }).then(_ => { 46 | callback(); 47 | }).catch(_ => { 48 | console.log('Prune error'); 49 | callback(); 50 | }); 51 | }; 52 | 53 | }; 54 | -------------------------------------------------------------------------------- /backend/todo-subscription-pruner/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | target: 'node', 7 | output: { 8 | path: path.join(process.cwd(), 'dist'), 9 | filename: 'index.js', 10 | libraryTarget: 'commonjs2' 11 | }, 12 | devtool: 'source-map', 13 | externals: ['aws-sdk'], 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.js$/, 18 | loader: 'babel-loader', 19 | options: { 20 | cacheDirectory: true, 21 | presets: [require('babel-preset-stage-2')], 22 | plugins: [require('babel-plugin-transform-es2015-modules-commonjs')] 23 | }, 24 | exclude: [/node_modules/] 25 | } 26 | ] 27 | }, 28 | resolve: { 29 | alias: { 30 | "graphql": path.resolve(__dirname, "node_modules/graphql") 31 | } 32 | }, 33 | plugins: [ 34 | new Webpack.NoEmitOnErrorsPlugin() 35 | ] 36 | }; 37 | -------------------------------------------------------------------------------- /backend/todo-subscription-publisher/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "globals": { 6 | "Promise": true 7 | }, 8 | "plugins": [ 9 | "import" 10 | ], 11 | "parser": "babel-eslint", 12 | "parserOptions": { 13 | "ecmaVersion": 6, 14 | "sourceType": "module", 15 | "ecmaFeatures": { 16 | "es6": true 17 | } 18 | }, 19 | "rules": { 20 | "import/no-unresolved": [ 21 | 2, 22 | { 23 | "commonjs": true, 24 | "amd": true 25 | } 26 | ], 27 | "semi": 2, 28 | "no-undef": 2, 29 | "no-unused-vars": [ 30 | "error", 31 | { 32 | "argsIgnorePattern": "^_" 33 | } 34 | ], 35 | "comma-dangle": 2 36 | } 37 | } -------------------------------------------------------------------------------- /backend/todo-subscription-publisher/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist -------------------------------------------------------------------------------- /backend/todo-subscription-publisher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-subscription-publisher", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "config": { 7 | "devFunctionName": "todo-ws-transport-example-SubscriptionPublisher-156FH0BIGO2ZB", 8 | "devCodeBucketName": "iox-playground", 9 | "codeKey": "todo-subscription-publisher.zip", 10 | "profile": "iox", 11 | "region": "us-west-2" 12 | }, 13 | "scripts": { 14 | "prebuild": "rm -rf dist && mkdir -p dist", 15 | "build": "webpack", 16 | "prezip": "rm -rf build && mkdir -p build", 17 | "zip": "cd dist && zip -r ../build/build.zip *", 18 | "lint": "eslint src", 19 | "lint:dist": "eslint dist index.js", 20 | "preupdate": "npm run build && npm run zip", 21 | "update": "aws lambda update-function-code --function-name $npm_package_config_devFunctionName --zip-file fileb://$PWD/build/build.zip --profile $npm_package_config_profile --region $npm_package_config_region", 22 | "updateS3": "npm run preupdate && aws s3 cp build/build.zip s3://$npm_package_config_devCodeBucketName/$npm_package_config_codeKey --profile $npm_package_config_profile --region $npm_package_config_region", 23 | "schema:generate": "babel src --out-dir dist -s && node output-schema.js" 24 | }, 25 | "author": "", 26 | "license": "ISC", 27 | "dependencies": { 28 | "aws-xray-sdk": "^1.1.4", 29 | "graphql": "^0.11.2", 30 | "graphql-aws-iot-server": "^0.0.1", 31 | "graphql-subscriptions": "^0.4.4", 32 | "rimraf": "^2.6.1", 33 | "source-map-support": "^0.4.14" 34 | }, 35 | "devDependencies": { 36 | "aws-sdk": "^2.102.0", 37 | "babel-cli": "^6.24.0", 38 | "babel-core": "^6.26.0", 39 | "babel-eslint": "^6.1.2", 40 | "babel-loader": "^7.1.2", 41 | "babel-plugin-transform-es2015-modules-commonjs": "^6.24.0", 42 | "babel-preset-env": "^1.3.3", 43 | "babel-preset-stage-2": "^6.22.0", 44 | "eslint": "^3.18.0", 45 | "eslint-plugin-import": "^2.2.0", 46 | "webpack": "^3.5.5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /backend/todo-subscription-publisher/src/index.js: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register'; 2 | 3 | import AWSXray from 'aws-xray-sdk'; 4 | var AWS = AWSXray.captureAWS(require('aws-sdk')); // eslint-disable-line 5 | 6 | 7 | import { SubscriptionPublisher } from 'graphql-aws-iot-server'; 8 | import schema from '../../todo-api/src/root.schema'; 9 | 10 | let db; 11 | let publisher; 12 | 13 | export const handler = (event, context, callback) => { 14 | console.log(JSON.stringify(event)); 15 | const { triggerName, payload } = event; 16 | 17 | const triggerNameToFilterFunctionsMap = { 18 | NEW_TODO: (payload, variables) => { 19 | return payload.teamTodoAdded.teamName === variables.teamName; 20 | } 21 | }; 22 | 23 | const triggerNameToSubscriptionNamesMap = { 24 | NEW_TODO: ['teamTodoAdded'] 25 | }; 26 | 27 | const subscriptionPublisherOptions = { 28 | appPrefix: process.env.AppPrefix, 29 | iotEndpoint: process.env.IotEndpoint, 30 | schema 31 | }; 32 | 33 | if (!publisher) { 34 | publisher = new SubscriptionPublisher(subscriptionPublisherOptions); 35 | } 36 | 37 | if (!db) { 38 | db = new AWS.DynamoDB.DocumentClient(); 39 | } 40 | 41 | 42 | onTrigger(triggerName, payload) 43 | .then(res => { 44 | console.log(res); 45 | callback(); 46 | }) 47 | .catch(err => { 48 | console.log('Subscription Publisher Error'); 49 | console.log(err); 50 | }); 51 | 52 | function onTrigger(triggerName, payload) { 53 | let promises = []; 54 | let subscriptions = triggerNameToSubscriptionNamesMap[triggerName]; 55 | subscriptions.forEach(subscriptionName => { 56 | promises.push(publishForSubscription(subscriptionName, triggerName, payload)); 57 | }); 58 | return Promise.all(promises); 59 | } 60 | 61 | function publishForSubscription(subscriptionName, triggerName, payload) { 62 | const params = { 63 | TableName: process.env.SubscriptionsTableName, 64 | IndexName: process.env.SubscriptionToClientIdsIndex, 65 | KeyConditionExpression: 'subscriptionName = :hkey', 66 | ExpressionAttributeValues: { 67 | ':hkey': subscriptionName 68 | } 69 | }; 70 | 71 | let subscriptionsToExecute = []; 72 | 73 | return db.query(params).promise() 74 | .then(res => { 75 | if (res.Items && res.Items.length) { 76 | res.Items.forEach(subscription => { 77 | if (triggerNameToFilterFunctionsMap[triggerName]) { 78 | const execute = triggerNameToFilterFunctionsMap[triggerName](payload, subscription.variableValues); 79 | if (!execute) return; 80 | } 81 | subscriptionsToExecute.push(subscription); 82 | }); 83 | } 84 | if (!subscriptionsToExecute.length) { 85 | return Promise.resolve(null); 86 | } 87 | return publisher.executeQueriesAndSendMessages(subscriptionsToExecute, payload); 88 | }); 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /backend/todo-subscription-publisher/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | target: 'node', 7 | output: { 8 | path: path.join(process.cwd(), 'dist'), 9 | filename: 'index.js', 10 | libraryTarget: 'commonjs2' 11 | }, 12 | devtool: 'source-map', 13 | externals: ['aws-sdk'], 14 | 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js$/, 19 | loader: 'babel-loader', 20 | options: { 21 | cacheDirectory: true, 22 | presets: [require('babel-preset-stage-2')], 23 | plugins: [require('babel-plugin-transform-es2015-modules-commonjs')] 24 | }, 25 | exclude: [/node_modules/] 26 | } 27 | ] 28 | }, 29 | resolve: { 30 | alias: { 31 | "graphql": path.resolve(__dirname, "node_modules/graphql") 32 | } 33 | }, 34 | plugins: [ 35 | new Webpack.NoEmitOnErrorsPlugin() 36 | ] 37 | }; 38 | -------------------------------------------------------------------------------- /backend/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { ServerlessGraphqlSubscriptionsPage } from './app.po'; 2 | 3 | describe('serverless-graphql-subscriptions App', () => { 4 | let page: ServerlessGraphqlSubscriptionsPage; 5 | 6 | beforeEach(() => { 7 | page = new ServerlessGraphqlSubscriptionsPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class ServerlessGraphqlSubscriptionsPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular/cli/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | angularCli: { 23 | environment: 'dev' 24 | }, 25 | reporters: ['progress', 'kjhtml'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: ['Chrome'], 31 | singleRun: false 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-aws-iot-transport-example", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "config": { 6 | "s3BucketName": "todo-ws-transport-example-websitebucket-dgqb0k2ixlar", 7 | "profile": "iox", 8 | "region": "us-west-2" 9 | }, 10 | "scripts": { 11 | "ng": "ng", 12 | "start": "webpack-dev-server --port=4200", 13 | "prebuild": "rm -rf dist", 14 | "build": "NODE_ENV=production webpack", 15 | "update": "npm run build && aws s3 cp --recursive dist/ s3://$npm_package_config_s3BucketName --profile $npm_package_config_profile --region $npm_package_config_region", 16 | "test": "karma start ./karma.conf.js", 17 | "lint": "ng lint", 18 | "e2e": "protractor ./protractor.conf.js", 19 | "schema:generate": "apollo-codegen generate src/graphql/**/*.graphql --schema ./schema.json --target typescript --output src/graphql/schema.ts", 20 | "pree2e": "webdriver-manager update --standalone false --gecko false --quiet" 21 | }, 22 | "dependencies": { 23 | "@angular/animations": "^4.3.6", 24 | "@angular/cdk": "^2.0.0-beta.10", 25 | "@angular/common": "^4.3.0", 26 | "@angular/compiler": "^4.3.0", 27 | "@angular/core": "^4.3.0", 28 | "@angular/forms": "^4.3.0", 29 | "@angular/http": "^4.3.0", 30 | "@angular/material": "^2.0.0-beta.10", 31 | "@angular/platform-browser": "^4.3.0", 32 | "@angular/platform-browser-dynamic": "^4.3.0", 33 | "@angular/router": "^4.3.0", 34 | "@types/isomorphic-fetch": "0.0.34", 35 | "apollo-angular": "0.13.0", 36 | "apollo-client": "1.9.1", 37 | "apollo-codegen": "^0.16.5", 38 | "graphql": "^0.11.2", 39 | "graphql-aws-iot-client": "^0.0.1", 40 | "graphql-tag": "^2.4.2", 41 | "hammerjs": "^2.0.8", 42 | "moment": "^2.18.1", 43 | "rxjs": "^5.4.1", 44 | "uuid": "^3.1.0", 45 | "zone.js": "^0.8.14" 46 | }, 47 | "devDependencies": { 48 | "@angular/cli": "1.2.7", 49 | "@angular/compiler-cli": "^4.0.0", 50 | "@angular/language-service": "^4.0.0", 51 | "@ngtools/webpack": "^1.6.2", 52 | "@types/jasmine": "~2.5.53", 53 | "@types/jasminewd2": "~2.0.2", 54 | "@types/node": "^6.0.85", 55 | "autoprefixer": "^6.5.3", 56 | "aws-sdk": "^2.106.0", 57 | "codelyzer": "~3.0.1", 58 | "css-loader": "^0.28.1", 59 | "cssnano": "^3.10.0", 60 | "exports-loader": "^0.6.3", 61 | "file-loader": "^0.10.0", 62 | "html-webpack-plugin": "^2.19.0", 63 | "istanbul-instrumenter-loader": "^2.0.0", 64 | "jasmine-core": "~2.6.2", 65 | "jasmine-spec-reporter": "~4.1.0", 66 | "karma": "~1.7.0", 67 | "karma-chrome-launcher": "~2.1.1", 68 | "karma-cli": "~1.0.1", 69 | "karma-coverage-istanbul-reporter": "^1.2.1", 70 | "karma-jasmine": "~1.1.0", 71 | "karma-jasmine-html-reporter": "^0.2.2", 72 | "less-loader": "^4.0.2", 73 | "postcss-loader": "^1.3.3", 74 | "postcss-url": "^5.1.2", 75 | "protractor": "~5.1.2", 76 | "raw-loader": "^0.5.1", 77 | "sass-loader": "^6.0.3", 78 | "script-loader": "^0.7.0", 79 | "source-map-loader": "^0.2.0", 80 | "style-loader": "^0.13.1", 81 | "stylus-loader": "^3.0.1", 82 | "ts-node": "~3.0.4", 83 | "tslint": "~5.3.2", 84 | "typescript": "~2.3.3", 85 | "uglifyjs-webpack-plugin": "^0.4.6", 86 | "url-loader": "^0.5.7", 87 | "webpack": "~2.4.0", 88 | "webpack-dev-server": "~2.4.5" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "__schema": { 4 | "queryType": { 5 | "name": "Query" 6 | }, 7 | "mutationType": { 8 | "name": "Mutation" 9 | }, 10 | "subscriptionType": { 11 | "name": "Subscription" 12 | }, 13 | "types": [ 14 | { 15 | "kind": "OBJECT", 16 | "name": "Query", 17 | "description": null, 18 | "fields": [ 19 | { 20 | "name": "teamTodos", 21 | "description": "List of todos for user", 22 | "args": [ 23 | { 24 | "name": "first", 25 | "description": "Returns the first n todo items of the user", 26 | "type": { 27 | "kind": "SCALAR", 28 | "name": "Int", 29 | "ofType": null 30 | }, 31 | "defaultValue": "25" 32 | }, 33 | { 34 | "name": "teamName", 35 | "description": "Name of team", 36 | "type": { 37 | "kind": "SCALAR", 38 | "name": "String", 39 | "ofType": null 40 | }, 41 | "defaultValue": null 42 | } 43 | ], 44 | "type": { 45 | "kind": "LIST", 46 | "name": null, 47 | "ofType": { 48 | "kind": "OBJECT", 49 | "name": "Todo", 50 | "ofType": null 51 | } 52 | }, 53 | "isDeprecated": false, 54 | "deprecationReason": null 55 | } 56 | ], 57 | "inputFields": null, 58 | "interfaces": [], 59 | "enumValues": null, 60 | "possibleTypes": null 61 | }, 62 | { 63 | "kind": "SCALAR", 64 | "name": "Int", 65 | "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", 66 | "fields": null, 67 | "inputFields": null, 68 | "interfaces": null, 69 | "enumValues": null, 70 | "possibleTypes": null 71 | }, 72 | { 73 | "kind": "SCALAR", 74 | "name": "String", 75 | "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", 76 | "fields": null, 77 | "inputFields": null, 78 | "interfaces": null, 79 | "enumValues": null, 80 | "possibleTypes": null 81 | }, 82 | { 83 | "kind": "OBJECT", 84 | "name": "Todo", 85 | "description": "A todo item", 86 | "fields": [ 87 | { 88 | "name": "id", 89 | "description": "Db uuid", 90 | "args": [], 91 | "type": { 92 | "kind": "NON_NULL", 93 | "name": null, 94 | "ofType": { 95 | "kind": "SCALAR", 96 | "name": "ID", 97 | "ofType": null 98 | } 99 | }, 100 | "isDeprecated": false, 101 | "deprecationReason": null 102 | }, 103 | { 104 | "name": "name", 105 | "description": "Unique friendly name for todo item", 106 | "args": [], 107 | "type": { 108 | "kind": "NON_NULL", 109 | "name": null, 110 | "ofType": { 111 | "kind": "SCALAR", 112 | "name": "String", 113 | "ofType": null 114 | } 115 | }, 116 | "isDeprecated": false, 117 | "deprecationReason": null 118 | }, 119 | { 120 | "name": "author", 121 | "description": "Author who created item", 122 | "args": [], 123 | "type": { 124 | "kind": "NON_NULL", 125 | "name": null, 126 | "ofType": { 127 | "kind": "SCALAR", 128 | "name": "String", 129 | "ofType": null 130 | } 131 | }, 132 | "isDeprecated": false, 133 | "deprecationReason": null 134 | }, 135 | { 136 | "name": "content", 137 | "description": "Content of todo item", 138 | "args": [], 139 | "type": { 140 | "kind": "NON_NULL", 141 | "name": null, 142 | "ofType": { 143 | "kind": "SCALAR", 144 | "name": "String", 145 | "ofType": null 146 | } 147 | }, 148 | "isDeprecated": false, 149 | "deprecationReason": null 150 | }, 151 | { 152 | "name": "teamName", 153 | "description": "Team name", 154 | "args": [], 155 | "type": { 156 | "kind": "NON_NULL", 157 | "name": null, 158 | "ofType": { 159 | "kind": "SCALAR", 160 | "name": "String", 161 | "ofType": null 162 | } 163 | }, 164 | "isDeprecated": false, 165 | "deprecationReason": null 166 | }, 167 | { 168 | "name": "timestamp", 169 | "description": "ISO date string", 170 | "args": [], 171 | "type": { 172 | "kind": "NON_NULL", 173 | "name": null, 174 | "ofType": { 175 | "kind": "SCALAR", 176 | "name": "String", 177 | "ofType": null 178 | } 179 | }, 180 | "isDeprecated": false, 181 | "deprecationReason": null 182 | } 183 | ], 184 | "inputFields": null, 185 | "interfaces": [], 186 | "enumValues": null, 187 | "possibleTypes": null 188 | }, 189 | { 190 | "kind": "SCALAR", 191 | "name": "ID", 192 | "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", 193 | "fields": null, 194 | "inputFields": null, 195 | "interfaces": null, 196 | "enumValues": null, 197 | "possibleTypes": null 198 | }, 199 | { 200 | "kind": "OBJECT", 201 | "name": "Mutation", 202 | "description": null, 203 | "fields": [ 204 | { 205 | "name": "createTodo", 206 | "description": "Create a new todo item", 207 | "args": [ 208 | { 209 | "name": "input", 210 | "description": "Create todo input", 211 | "type": { 212 | "kind": "NON_NULL", 213 | "name": null, 214 | "ofType": { 215 | "kind": "INPUT_OBJECT", 216 | "name": "CreateTodoInput", 217 | "ofType": null 218 | } 219 | }, 220 | "defaultValue": null 221 | } 222 | ], 223 | "type": { 224 | "kind": "OBJECT", 225 | "name": "Todo", 226 | "ofType": null 227 | }, 228 | "isDeprecated": false, 229 | "deprecationReason": null 230 | }, 231 | { 232 | "name": "updateTodo", 233 | "description": "Update an existing todo item", 234 | "args": [ 235 | { 236 | "name": "input", 237 | "description": "Update todo input", 238 | "type": { 239 | "kind": "NON_NULL", 240 | "name": null, 241 | "ofType": { 242 | "kind": "INPUT_OBJECT", 243 | "name": "CreateTodoInput", 244 | "ofType": null 245 | } 246 | }, 247 | "defaultValue": null 248 | } 249 | ], 250 | "type": { 251 | "kind": "OBJECT", 252 | "name": "Todo", 253 | "ofType": null 254 | }, 255 | "isDeprecated": false, 256 | "deprecationReason": null 257 | }, 258 | { 259 | "name": "deleteTodo", 260 | "description": null, 261 | "args": [ 262 | { 263 | "name": "input", 264 | "description": null, 265 | "type": { 266 | "kind": "NON_NULL", 267 | "name": null, 268 | "ofType": { 269 | "kind": "INPUT_OBJECT", 270 | "name": "DeleteTodoInput", 271 | "ofType": null 272 | } 273 | }, 274 | "defaultValue": null 275 | } 276 | ], 277 | "type": { 278 | "kind": "SCALAR", 279 | "name": "ID", 280 | "ofType": null 281 | }, 282 | "isDeprecated": false, 283 | "deprecationReason": null 284 | } 285 | ], 286 | "inputFields": null, 287 | "interfaces": [], 288 | "enumValues": null, 289 | "possibleTypes": null 290 | }, 291 | { 292 | "kind": "INPUT_OBJECT", 293 | "name": "CreateTodoInput", 294 | "description": null, 295 | "fields": null, 296 | "inputFields": [ 297 | { 298 | "name": "timestamp", 299 | "description": null, 300 | "type": { 301 | "kind": "NON_NULL", 302 | "name": null, 303 | "ofType": { 304 | "kind": "SCALAR", 305 | "name": "String", 306 | "ofType": null 307 | } 308 | }, 309 | "defaultValue": null 310 | }, 311 | { 312 | "name": "name", 313 | "description": null, 314 | "type": { 315 | "kind": "NON_NULL", 316 | "name": null, 317 | "ofType": { 318 | "kind": "SCALAR", 319 | "name": "String", 320 | "ofType": null 321 | } 322 | }, 323 | "defaultValue": null 324 | }, 325 | { 326 | "name": "content", 327 | "description": null, 328 | "type": { 329 | "kind": "NON_NULL", 330 | "name": null, 331 | "ofType": { 332 | "kind": "SCALAR", 333 | "name": "String", 334 | "ofType": null 335 | } 336 | }, 337 | "defaultValue": null 338 | }, 339 | { 340 | "name": "teamName", 341 | "description": null, 342 | "type": { 343 | "kind": "NON_NULL", 344 | "name": null, 345 | "ofType": { 346 | "kind": "SCALAR", 347 | "name": "String", 348 | "ofType": null 349 | } 350 | }, 351 | "defaultValue": null 352 | }, 353 | { 354 | "name": "author", 355 | "description": null, 356 | "type": { 357 | "kind": "NON_NULL", 358 | "name": null, 359 | "ofType": { 360 | "kind": "SCALAR", 361 | "name": "String", 362 | "ofType": null 363 | } 364 | }, 365 | "defaultValue": null 366 | } 367 | ], 368 | "interfaces": null, 369 | "enumValues": null, 370 | "possibleTypes": null 371 | }, 372 | { 373 | "kind": "INPUT_OBJECT", 374 | "name": "DeleteTodoInput", 375 | "description": null, 376 | "fields": null, 377 | "inputFields": [ 378 | { 379 | "name": "id", 380 | "description": null, 381 | "type": { 382 | "kind": "NON_NULL", 383 | "name": null, 384 | "ofType": { 385 | "kind": "SCALAR", 386 | "name": "ID", 387 | "ofType": null 388 | } 389 | }, 390 | "defaultValue": null 391 | } 392 | ], 393 | "interfaces": null, 394 | "enumValues": null, 395 | "possibleTypes": null 396 | }, 397 | { 398 | "kind": "OBJECT", 399 | "name": "Subscription", 400 | "description": null, 401 | "fields": [ 402 | { 403 | "name": "teamTodoAdded", 404 | "description": "New todo added", 405 | "args": [ 406 | { 407 | "name": "teamName", 408 | "description": "Team name filter for todoAdded subscription", 409 | "type": { 410 | "kind": "SCALAR", 411 | "name": "String", 412 | "ofType": null 413 | }, 414 | "defaultValue": null 415 | } 416 | ], 417 | "type": { 418 | "kind": "OBJECT", 419 | "name": "Todo", 420 | "ofType": null 421 | }, 422 | "isDeprecated": false, 423 | "deprecationReason": null 424 | } 425 | ], 426 | "inputFields": null, 427 | "interfaces": [], 428 | "enumValues": null, 429 | "possibleTypes": null 430 | }, 431 | { 432 | "kind": "OBJECT", 433 | "name": "__Schema", 434 | "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", 435 | "fields": [ 436 | { 437 | "name": "types", 438 | "description": "A list of all types supported by this server.", 439 | "args": [], 440 | "type": { 441 | "kind": "NON_NULL", 442 | "name": null, 443 | "ofType": { 444 | "kind": "LIST", 445 | "name": null, 446 | "ofType": { 447 | "kind": "NON_NULL", 448 | "name": null, 449 | "ofType": { 450 | "kind": "OBJECT", 451 | "name": "__Type", 452 | "ofType": null 453 | } 454 | } 455 | } 456 | }, 457 | "isDeprecated": false, 458 | "deprecationReason": null 459 | }, 460 | { 461 | "name": "queryType", 462 | "description": "The type that query operations will be rooted at.", 463 | "args": [], 464 | "type": { 465 | "kind": "NON_NULL", 466 | "name": null, 467 | "ofType": { 468 | "kind": "OBJECT", 469 | "name": "__Type", 470 | "ofType": null 471 | } 472 | }, 473 | "isDeprecated": false, 474 | "deprecationReason": null 475 | }, 476 | { 477 | "name": "mutationType", 478 | "description": "If this server supports mutation, the type that mutation operations will be rooted at.", 479 | "args": [], 480 | "type": { 481 | "kind": "OBJECT", 482 | "name": "__Type", 483 | "ofType": null 484 | }, 485 | "isDeprecated": false, 486 | "deprecationReason": null 487 | }, 488 | { 489 | "name": "subscriptionType", 490 | "description": "If this server support subscription, the type that subscription operations will be rooted at.", 491 | "args": [], 492 | "type": { 493 | "kind": "OBJECT", 494 | "name": "__Type", 495 | "ofType": null 496 | }, 497 | "isDeprecated": false, 498 | "deprecationReason": null 499 | }, 500 | { 501 | "name": "directives", 502 | "description": "A list of all directives supported by this server.", 503 | "args": [], 504 | "type": { 505 | "kind": "NON_NULL", 506 | "name": null, 507 | "ofType": { 508 | "kind": "LIST", 509 | "name": null, 510 | "ofType": { 511 | "kind": "NON_NULL", 512 | "name": null, 513 | "ofType": { 514 | "kind": "OBJECT", 515 | "name": "__Directive", 516 | "ofType": null 517 | } 518 | } 519 | } 520 | }, 521 | "isDeprecated": false, 522 | "deprecationReason": null 523 | } 524 | ], 525 | "inputFields": null, 526 | "interfaces": [], 527 | "enumValues": null, 528 | "possibleTypes": null 529 | }, 530 | { 531 | "kind": "OBJECT", 532 | "name": "__Type", 533 | "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", 534 | "fields": [ 535 | { 536 | "name": "kind", 537 | "description": null, 538 | "args": [], 539 | "type": { 540 | "kind": "NON_NULL", 541 | "name": null, 542 | "ofType": { 543 | "kind": "ENUM", 544 | "name": "__TypeKind", 545 | "ofType": null 546 | } 547 | }, 548 | "isDeprecated": false, 549 | "deprecationReason": null 550 | }, 551 | { 552 | "name": "name", 553 | "description": null, 554 | "args": [], 555 | "type": { 556 | "kind": "SCALAR", 557 | "name": "String", 558 | "ofType": null 559 | }, 560 | "isDeprecated": false, 561 | "deprecationReason": null 562 | }, 563 | { 564 | "name": "description", 565 | "description": null, 566 | "args": [], 567 | "type": { 568 | "kind": "SCALAR", 569 | "name": "String", 570 | "ofType": null 571 | }, 572 | "isDeprecated": false, 573 | "deprecationReason": null 574 | }, 575 | { 576 | "name": "fields", 577 | "description": null, 578 | "args": [ 579 | { 580 | "name": "includeDeprecated", 581 | "description": null, 582 | "type": { 583 | "kind": "SCALAR", 584 | "name": "Boolean", 585 | "ofType": null 586 | }, 587 | "defaultValue": "false" 588 | } 589 | ], 590 | "type": { 591 | "kind": "LIST", 592 | "name": null, 593 | "ofType": { 594 | "kind": "NON_NULL", 595 | "name": null, 596 | "ofType": { 597 | "kind": "OBJECT", 598 | "name": "__Field", 599 | "ofType": null 600 | } 601 | } 602 | }, 603 | "isDeprecated": false, 604 | "deprecationReason": null 605 | }, 606 | { 607 | "name": "interfaces", 608 | "description": null, 609 | "args": [], 610 | "type": { 611 | "kind": "LIST", 612 | "name": null, 613 | "ofType": { 614 | "kind": "NON_NULL", 615 | "name": null, 616 | "ofType": { 617 | "kind": "OBJECT", 618 | "name": "__Type", 619 | "ofType": null 620 | } 621 | } 622 | }, 623 | "isDeprecated": false, 624 | "deprecationReason": null 625 | }, 626 | { 627 | "name": "possibleTypes", 628 | "description": null, 629 | "args": [], 630 | "type": { 631 | "kind": "LIST", 632 | "name": null, 633 | "ofType": { 634 | "kind": "NON_NULL", 635 | "name": null, 636 | "ofType": { 637 | "kind": "OBJECT", 638 | "name": "__Type", 639 | "ofType": null 640 | } 641 | } 642 | }, 643 | "isDeprecated": false, 644 | "deprecationReason": null 645 | }, 646 | { 647 | "name": "enumValues", 648 | "description": null, 649 | "args": [ 650 | { 651 | "name": "includeDeprecated", 652 | "description": null, 653 | "type": { 654 | "kind": "SCALAR", 655 | "name": "Boolean", 656 | "ofType": null 657 | }, 658 | "defaultValue": "false" 659 | } 660 | ], 661 | "type": { 662 | "kind": "LIST", 663 | "name": null, 664 | "ofType": { 665 | "kind": "NON_NULL", 666 | "name": null, 667 | "ofType": { 668 | "kind": "OBJECT", 669 | "name": "__EnumValue", 670 | "ofType": null 671 | } 672 | } 673 | }, 674 | "isDeprecated": false, 675 | "deprecationReason": null 676 | }, 677 | { 678 | "name": "inputFields", 679 | "description": null, 680 | "args": [], 681 | "type": { 682 | "kind": "LIST", 683 | "name": null, 684 | "ofType": { 685 | "kind": "NON_NULL", 686 | "name": null, 687 | "ofType": { 688 | "kind": "OBJECT", 689 | "name": "__InputValue", 690 | "ofType": null 691 | } 692 | } 693 | }, 694 | "isDeprecated": false, 695 | "deprecationReason": null 696 | }, 697 | { 698 | "name": "ofType", 699 | "description": null, 700 | "args": [], 701 | "type": { 702 | "kind": "OBJECT", 703 | "name": "__Type", 704 | "ofType": null 705 | }, 706 | "isDeprecated": false, 707 | "deprecationReason": null 708 | } 709 | ], 710 | "inputFields": null, 711 | "interfaces": [], 712 | "enumValues": null, 713 | "possibleTypes": null 714 | }, 715 | { 716 | "kind": "ENUM", 717 | "name": "__TypeKind", 718 | "description": "An enum describing what kind of type a given `__Type` is.", 719 | "fields": null, 720 | "inputFields": null, 721 | "interfaces": null, 722 | "enumValues": [ 723 | { 724 | "name": "SCALAR", 725 | "description": "Indicates this type is a scalar.", 726 | "isDeprecated": false, 727 | "deprecationReason": null 728 | }, 729 | { 730 | "name": "OBJECT", 731 | "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", 732 | "isDeprecated": false, 733 | "deprecationReason": null 734 | }, 735 | { 736 | "name": "INTERFACE", 737 | "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", 738 | "isDeprecated": false, 739 | "deprecationReason": null 740 | }, 741 | { 742 | "name": "UNION", 743 | "description": "Indicates this type is a union. `possibleTypes` is a valid field.", 744 | "isDeprecated": false, 745 | "deprecationReason": null 746 | }, 747 | { 748 | "name": "ENUM", 749 | "description": "Indicates this type is an enum. `enumValues` is a valid field.", 750 | "isDeprecated": false, 751 | "deprecationReason": null 752 | }, 753 | { 754 | "name": "INPUT_OBJECT", 755 | "description": "Indicates this type is an input object. `inputFields` is a valid field.", 756 | "isDeprecated": false, 757 | "deprecationReason": null 758 | }, 759 | { 760 | "name": "LIST", 761 | "description": "Indicates this type is a list. `ofType` is a valid field.", 762 | "isDeprecated": false, 763 | "deprecationReason": null 764 | }, 765 | { 766 | "name": "NON_NULL", 767 | "description": "Indicates this type is a non-null. `ofType` is a valid field.", 768 | "isDeprecated": false, 769 | "deprecationReason": null 770 | } 771 | ], 772 | "possibleTypes": null 773 | }, 774 | { 775 | "kind": "SCALAR", 776 | "name": "Boolean", 777 | "description": "The `Boolean` scalar type represents `true` or `false`.", 778 | "fields": null, 779 | "inputFields": null, 780 | "interfaces": null, 781 | "enumValues": null, 782 | "possibleTypes": null 783 | }, 784 | { 785 | "kind": "OBJECT", 786 | "name": "__Field", 787 | "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", 788 | "fields": [ 789 | { 790 | "name": "name", 791 | "description": null, 792 | "args": [], 793 | "type": { 794 | "kind": "NON_NULL", 795 | "name": null, 796 | "ofType": { 797 | "kind": "SCALAR", 798 | "name": "String", 799 | "ofType": null 800 | } 801 | }, 802 | "isDeprecated": false, 803 | "deprecationReason": null 804 | }, 805 | { 806 | "name": "description", 807 | "description": null, 808 | "args": [], 809 | "type": { 810 | "kind": "SCALAR", 811 | "name": "String", 812 | "ofType": null 813 | }, 814 | "isDeprecated": false, 815 | "deprecationReason": null 816 | }, 817 | { 818 | "name": "args", 819 | "description": null, 820 | "args": [], 821 | "type": { 822 | "kind": "NON_NULL", 823 | "name": null, 824 | "ofType": { 825 | "kind": "LIST", 826 | "name": null, 827 | "ofType": { 828 | "kind": "NON_NULL", 829 | "name": null, 830 | "ofType": { 831 | "kind": "OBJECT", 832 | "name": "__InputValue", 833 | "ofType": null 834 | } 835 | } 836 | } 837 | }, 838 | "isDeprecated": false, 839 | "deprecationReason": null 840 | }, 841 | { 842 | "name": "type", 843 | "description": null, 844 | "args": [], 845 | "type": { 846 | "kind": "NON_NULL", 847 | "name": null, 848 | "ofType": { 849 | "kind": "OBJECT", 850 | "name": "__Type", 851 | "ofType": null 852 | } 853 | }, 854 | "isDeprecated": false, 855 | "deprecationReason": null 856 | }, 857 | { 858 | "name": "isDeprecated", 859 | "description": null, 860 | "args": [], 861 | "type": { 862 | "kind": "NON_NULL", 863 | "name": null, 864 | "ofType": { 865 | "kind": "SCALAR", 866 | "name": "Boolean", 867 | "ofType": null 868 | } 869 | }, 870 | "isDeprecated": false, 871 | "deprecationReason": null 872 | }, 873 | { 874 | "name": "deprecationReason", 875 | "description": null, 876 | "args": [], 877 | "type": { 878 | "kind": "SCALAR", 879 | "name": "String", 880 | "ofType": null 881 | }, 882 | "isDeprecated": false, 883 | "deprecationReason": null 884 | } 885 | ], 886 | "inputFields": null, 887 | "interfaces": [], 888 | "enumValues": null, 889 | "possibleTypes": null 890 | }, 891 | { 892 | "kind": "OBJECT", 893 | "name": "__InputValue", 894 | "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", 895 | "fields": [ 896 | { 897 | "name": "name", 898 | "description": null, 899 | "args": [], 900 | "type": { 901 | "kind": "NON_NULL", 902 | "name": null, 903 | "ofType": { 904 | "kind": "SCALAR", 905 | "name": "String", 906 | "ofType": null 907 | } 908 | }, 909 | "isDeprecated": false, 910 | "deprecationReason": null 911 | }, 912 | { 913 | "name": "description", 914 | "description": null, 915 | "args": [], 916 | "type": { 917 | "kind": "SCALAR", 918 | "name": "String", 919 | "ofType": null 920 | }, 921 | "isDeprecated": false, 922 | "deprecationReason": null 923 | }, 924 | { 925 | "name": "type", 926 | "description": null, 927 | "args": [], 928 | "type": { 929 | "kind": "NON_NULL", 930 | "name": null, 931 | "ofType": { 932 | "kind": "OBJECT", 933 | "name": "__Type", 934 | "ofType": null 935 | } 936 | }, 937 | "isDeprecated": false, 938 | "deprecationReason": null 939 | }, 940 | { 941 | "name": "defaultValue", 942 | "description": "A GraphQL-formatted string representing the default value for this input value.", 943 | "args": [], 944 | "type": { 945 | "kind": "SCALAR", 946 | "name": "String", 947 | "ofType": null 948 | }, 949 | "isDeprecated": false, 950 | "deprecationReason": null 951 | } 952 | ], 953 | "inputFields": null, 954 | "interfaces": [], 955 | "enumValues": null, 956 | "possibleTypes": null 957 | }, 958 | { 959 | "kind": "OBJECT", 960 | "name": "__EnumValue", 961 | "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", 962 | "fields": [ 963 | { 964 | "name": "name", 965 | "description": null, 966 | "args": [], 967 | "type": { 968 | "kind": "NON_NULL", 969 | "name": null, 970 | "ofType": { 971 | "kind": "SCALAR", 972 | "name": "String", 973 | "ofType": null 974 | } 975 | }, 976 | "isDeprecated": false, 977 | "deprecationReason": null 978 | }, 979 | { 980 | "name": "description", 981 | "description": null, 982 | "args": [], 983 | "type": { 984 | "kind": "SCALAR", 985 | "name": "String", 986 | "ofType": null 987 | }, 988 | "isDeprecated": false, 989 | "deprecationReason": null 990 | }, 991 | { 992 | "name": "isDeprecated", 993 | "description": null, 994 | "args": [], 995 | "type": { 996 | "kind": "NON_NULL", 997 | "name": null, 998 | "ofType": { 999 | "kind": "SCALAR", 1000 | "name": "Boolean", 1001 | "ofType": null 1002 | } 1003 | }, 1004 | "isDeprecated": false, 1005 | "deprecationReason": null 1006 | }, 1007 | { 1008 | "name": "deprecationReason", 1009 | "description": null, 1010 | "args": [], 1011 | "type": { 1012 | "kind": "SCALAR", 1013 | "name": "String", 1014 | "ofType": null 1015 | }, 1016 | "isDeprecated": false, 1017 | "deprecationReason": null 1018 | } 1019 | ], 1020 | "inputFields": null, 1021 | "interfaces": [], 1022 | "enumValues": null, 1023 | "possibleTypes": null 1024 | }, 1025 | { 1026 | "kind": "OBJECT", 1027 | "name": "__Directive", 1028 | "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", 1029 | "fields": [ 1030 | { 1031 | "name": "name", 1032 | "description": null, 1033 | "args": [], 1034 | "type": { 1035 | "kind": "NON_NULL", 1036 | "name": null, 1037 | "ofType": { 1038 | "kind": "SCALAR", 1039 | "name": "String", 1040 | "ofType": null 1041 | } 1042 | }, 1043 | "isDeprecated": false, 1044 | "deprecationReason": null 1045 | }, 1046 | { 1047 | "name": "description", 1048 | "description": null, 1049 | "args": [], 1050 | "type": { 1051 | "kind": "SCALAR", 1052 | "name": "String", 1053 | "ofType": null 1054 | }, 1055 | "isDeprecated": false, 1056 | "deprecationReason": null 1057 | }, 1058 | { 1059 | "name": "locations", 1060 | "description": null, 1061 | "args": [], 1062 | "type": { 1063 | "kind": "NON_NULL", 1064 | "name": null, 1065 | "ofType": { 1066 | "kind": "LIST", 1067 | "name": null, 1068 | "ofType": { 1069 | "kind": "NON_NULL", 1070 | "name": null, 1071 | "ofType": { 1072 | "kind": "ENUM", 1073 | "name": "__DirectiveLocation", 1074 | "ofType": null 1075 | } 1076 | } 1077 | } 1078 | }, 1079 | "isDeprecated": false, 1080 | "deprecationReason": null 1081 | }, 1082 | { 1083 | "name": "args", 1084 | "description": null, 1085 | "args": [], 1086 | "type": { 1087 | "kind": "NON_NULL", 1088 | "name": null, 1089 | "ofType": { 1090 | "kind": "LIST", 1091 | "name": null, 1092 | "ofType": { 1093 | "kind": "NON_NULL", 1094 | "name": null, 1095 | "ofType": { 1096 | "kind": "OBJECT", 1097 | "name": "__InputValue", 1098 | "ofType": null 1099 | } 1100 | } 1101 | } 1102 | }, 1103 | "isDeprecated": false, 1104 | "deprecationReason": null 1105 | }, 1106 | { 1107 | "name": "onOperation", 1108 | "description": null, 1109 | "args": [], 1110 | "type": { 1111 | "kind": "NON_NULL", 1112 | "name": null, 1113 | "ofType": { 1114 | "kind": "SCALAR", 1115 | "name": "Boolean", 1116 | "ofType": null 1117 | } 1118 | }, 1119 | "isDeprecated": true, 1120 | "deprecationReason": "Use `locations`." 1121 | }, 1122 | { 1123 | "name": "onFragment", 1124 | "description": null, 1125 | "args": [], 1126 | "type": { 1127 | "kind": "NON_NULL", 1128 | "name": null, 1129 | "ofType": { 1130 | "kind": "SCALAR", 1131 | "name": "Boolean", 1132 | "ofType": null 1133 | } 1134 | }, 1135 | "isDeprecated": true, 1136 | "deprecationReason": "Use `locations`." 1137 | }, 1138 | { 1139 | "name": "onField", 1140 | "description": null, 1141 | "args": [], 1142 | "type": { 1143 | "kind": "NON_NULL", 1144 | "name": null, 1145 | "ofType": { 1146 | "kind": "SCALAR", 1147 | "name": "Boolean", 1148 | "ofType": null 1149 | } 1150 | }, 1151 | "isDeprecated": true, 1152 | "deprecationReason": "Use `locations`." 1153 | } 1154 | ], 1155 | "inputFields": null, 1156 | "interfaces": [], 1157 | "enumValues": null, 1158 | "possibleTypes": null 1159 | }, 1160 | { 1161 | "kind": "ENUM", 1162 | "name": "__DirectiveLocation", 1163 | "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", 1164 | "fields": null, 1165 | "inputFields": null, 1166 | "interfaces": null, 1167 | "enumValues": [ 1168 | { 1169 | "name": "QUERY", 1170 | "description": "Location adjacent to a query operation.", 1171 | "isDeprecated": false, 1172 | "deprecationReason": null 1173 | }, 1174 | { 1175 | "name": "MUTATION", 1176 | "description": "Location adjacent to a mutation operation.", 1177 | "isDeprecated": false, 1178 | "deprecationReason": null 1179 | }, 1180 | { 1181 | "name": "SUBSCRIPTION", 1182 | "description": "Location adjacent to a subscription operation.", 1183 | "isDeprecated": false, 1184 | "deprecationReason": null 1185 | }, 1186 | { 1187 | "name": "FIELD", 1188 | "description": "Location adjacent to a field.", 1189 | "isDeprecated": false, 1190 | "deprecationReason": null 1191 | }, 1192 | { 1193 | "name": "FRAGMENT_DEFINITION", 1194 | "description": "Location adjacent to a fragment definition.", 1195 | "isDeprecated": false, 1196 | "deprecationReason": null 1197 | }, 1198 | { 1199 | "name": "FRAGMENT_SPREAD", 1200 | "description": "Location adjacent to a fragment spread.", 1201 | "isDeprecated": false, 1202 | "deprecationReason": null 1203 | }, 1204 | { 1205 | "name": "INLINE_FRAGMENT", 1206 | "description": "Location adjacent to an inline fragment.", 1207 | "isDeprecated": false, 1208 | "deprecationReason": null 1209 | }, 1210 | { 1211 | "name": "SCHEMA", 1212 | "description": "Location adjacent to a schema definition.", 1213 | "isDeprecated": false, 1214 | "deprecationReason": null 1215 | }, 1216 | { 1217 | "name": "SCALAR", 1218 | "description": "Location adjacent to a scalar definition.", 1219 | "isDeprecated": false, 1220 | "deprecationReason": null 1221 | }, 1222 | { 1223 | "name": "OBJECT", 1224 | "description": "Location adjacent to an object type definition.", 1225 | "isDeprecated": false, 1226 | "deprecationReason": null 1227 | }, 1228 | { 1229 | "name": "FIELD_DEFINITION", 1230 | "description": "Location adjacent to a field definition.", 1231 | "isDeprecated": false, 1232 | "deprecationReason": null 1233 | }, 1234 | { 1235 | "name": "ARGUMENT_DEFINITION", 1236 | "description": "Location adjacent to an argument definition.", 1237 | "isDeprecated": false, 1238 | "deprecationReason": null 1239 | }, 1240 | { 1241 | "name": "INTERFACE", 1242 | "description": "Location adjacent to an interface definition.", 1243 | "isDeprecated": false, 1244 | "deprecationReason": null 1245 | }, 1246 | { 1247 | "name": "UNION", 1248 | "description": "Location adjacent to a union definition.", 1249 | "isDeprecated": false, 1250 | "deprecationReason": null 1251 | }, 1252 | { 1253 | "name": "ENUM", 1254 | "description": "Location adjacent to an enum definition.", 1255 | "isDeprecated": false, 1256 | "deprecationReason": null 1257 | }, 1258 | { 1259 | "name": "ENUM_VALUE", 1260 | "description": "Location adjacent to an enum value definition.", 1261 | "isDeprecated": false, 1262 | "deprecationReason": null 1263 | }, 1264 | { 1265 | "name": "INPUT_OBJECT", 1266 | "description": "Location adjacent to an input object type definition.", 1267 | "isDeprecated": false, 1268 | "deprecationReason": null 1269 | }, 1270 | { 1271 | "name": "INPUT_FIELD_DEFINITION", 1272 | "description": "Location adjacent to an input object field definition.", 1273 | "isDeprecated": false, 1274 | "deprecationReason": null 1275 | } 1276 | ], 1277 | "possibleTypes": null 1278 | } 1279 | ], 1280 | "directives": [ 1281 | { 1282 | "name": "include", 1283 | "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", 1284 | "locations": [ 1285 | "FIELD", 1286 | "FRAGMENT_SPREAD", 1287 | "INLINE_FRAGMENT" 1288 | ], 1289 | "args": [ 1290 | { 1291 | "name": "if", 1292 | "description": "Included when true.", 1293 | "type": { 1294 | "kind": "NON_NULL", 1295 | "name": null, 1296 | "ofType": { 1297 | "kind": "SCALAR", 1298 | "name": "Boolean", 1299 | "ofType": null 1300 | } 1301 | }, 1302 | "defaultValue": null 1303 | } 1304 | ] 1305 | }, 1306 | { 1307 | "name": "skip", 1308 | "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", 1309 | "locations": [ 1310 | "FIELD", 1311 | "FRAGMENT_SPREAD", 1312 | "INLINE_FRAGMENT" 1313 | ], 1314 | "args": [ 1315 | { 1316 | "name": "if", 1317 | "description": "Skipped when true.", 1318 | "type": { 1319 | "kind": "NON_NULL", 1320 | "name": null, 1321 | "ofType": { 1322 | "kind": "SCALAR", 1323 | "name": "Boolean", 1324 | "ofType": null 1325 | } 1326 | }, 1327 | "defaultValue": null 1328 | } 1329 | ] 1330 | }, 1331 | { 1332 | "name": "deprecated", 1333 | "description": "Marks an element of a GraphQL schema as no longer supported.", 1334 | "locations": [ 1335 | "FIELD_DEFINITION", 1336 | "ENUM_VALUE" 1337 | ], 1338 | "args": [ 1339 | { 1340 | "name": "reason", 1341 | "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).", 1342 | "type": { 1343 | "kind": "SCALAR", 1344 | "name": "String", 1345 | "ofType": null 1346 | }, 1347 | "defaultValue": "\"No longer supported\"" 1348 | } 1349 | ] 1350 | } 1351 | ] 1352 | } 1353 | } 1354 | } -------------------------------------------------------------------------------- /src/app/apollo/app-apollo.module.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@angular/core'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { ApolloModule } from 'apollo-angular'; 5 | import { ApolloClient } from 'apollo-client'; 6 | 7 | import { SubscriptionClient } from 'graphql-aws-iot-client/src'; 8 | 9 | import { getCredentialsFunction } from './get-credentials'; 10 | 11 | import { environment } from '../../environments/environment'; 12 | const { region, iotEndpoint, AppPrefix } = environment; 13 | 14 | 15 | const wsClient = new SubscriptionClient(iotEndpoint, { 16 | appPrefix: AppPrefix, 17 | region, 18 | reconnect: true, 19 | getCredentialsFunction, 20 | debug: true 21 | }); 22 | 23 | const client: ApolloClient = new ApolloClient({ 24 | dataIdFromObject: (o: any) => o.id, 25 | networkInterface: wsClient, 26 | connectToDevTools: true, 27 | }); 28 | 29 | export function provideClient(): ApolloClient { 30 | return client; 31 | } 32 | 33 | @NgModule({ 34 | imports: [ApolloModule.forRoot(provideClient)], 35 | exports: [ApolloModule] 36 | }) 37 | export class AppApolloModule { } 38 | -------------------------------------------------------------------------------- /src/app/apollo/credentials.model.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | 3 | export class Credentials { 4 | accessKeyId: string; 5 | secretAccessKey: string; 6 | sessionToken: string; 7 | expiration: string; 8 | expirationTime: string; 9 | constructor(credentialsResponse) { 10 | this.accessKeyId = credentialsResponse.AccessKeyId; 11 | this.secretAccessKey = credentialsResponse.SecretAccessKey; 12 | this.sessionToken = credentialsResponse.SessionToken; 13 | this.expirationTime = credentialsResponse.Expiration; 14 | } 15 | get expired() { 16 | return moment.utc(this.expirationTime).isBefore(moment.utc()); 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/app/apollo/get-credentials.ts: -------------------------------------------------------------------------------- 1 | import { environment } from '../../environments/environment'; 2 | 3 | import { CognitoIdentityCredentials } from 'aws-sdk/global'; 4 | 5 | declare var AWS: any; 6 | AWS.config.region = 'us-west-2'; 7 | import { Credentials } from 'aws-sdk/global'; 8 | 9 | export const getCredentialsFunction = () => { 10 | const credentials = new CognitoIdentityCredentials({ 11 | IdentityPoolId: environment.identityPoolId, 12 | }); 13 | return credentials.refreshPromise().then((res: any) => { 14 | return credentials; 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /src/app/app-routing/app.routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { TodoHomeComponent } from '../todos/todo-home/todo-home.component'; 5 | import { TodoTeamComponent } from '../todos/todo-team/todo-team.component'; 6 | 7 | const appRoutes: Routes = [ 8 | { 9 | path: '', 10 | component: TodoHomeComponent 11 | }, 12 | { path: 'team', redirectTo: '', pathMatch: 'full' }, 13 | { 14 | path: 'team/:teamName', 15 | component: TodoTeamComponent 16 | } 17 | ]; 18 | 19 | @NgModule({ 20 | imports: [ 21 | RouterModule.forRoot( 22 | appRoutes 23 | ) 24 | ], 25 | exports: [ 26 | RouterModule 27 | ] 28 | }) 29 | export class AppRoutingModule {} 30 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 |
7 |
8 |

List

9 | 10 |
11 |
12 |
-------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | declarations: [ 9 | AppComponent 10 | ], 11 | }).compileComponents(); 12 | })); 13 | 14 | it('should create the app', async(() => { 15 | const fixture = TestBed.createComponent(AppComponent); 16 | const app = fixture.debugElement.componentInstance; 17 | expect(app).toBeTruthy(); 18 | })); 19 | 20 | it(`should have as title 'app'`, async(() => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.debugElement.componentInstance; 23 | expect(app.title).toEqual('app'); 24 | })); 25 | 26 | it('should render title in a h1 tag', async(() => { 27 | const fixture = TestBed.createComponent(AppComponent); 28 | fixture.detectChanges(); 29 | const compiled = fixture.debugElement.nativeElement; 30 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); 31 | })); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | template: '', 6 | }) 7 | export class AppComponent {} 8 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { NgModule } from '@angular/core'; 4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 5 | 6 | import { 7 | MdButtonModule, 8 | MdCheckboxModule, 9 | MdInputModule, 10 | MdIconModule, 11 | MdCardModule, 12 | MdProgressSpinnerModule, 13 | MdChipsModule 14 | } from '@angular/material'; 15 | 16 | import { AppApolloModule } from './apollo/app-apollo.module'; 17 | 18 | import { AppRoutingModule } from './app-routing/app.routing.module'; 19 | import { AppComponent } from './app.component'; 20 | import { TodoCreateComponent } from './todos/todo-create/todo-create.component'; 21 | import { TodoHomeComponent } from './todos/todo-home/todo-home.component'; 22 | import { TodoTeamComponent } from './todos/todo-team/todo-team.component'; 23 | import { TodoListItemComponent } from './todos/todo-list-item/todo-list-item.component'; 24 | import { TodoListComponent } from './todos/todo-list/todo-list.component'; 25 | 26 | 27 | @NgModule({ 28 | declarations: [ 29 | AppComponent, 30 | TodoCreateComponent, 31 | TodoHomeComponent, 32 | TodoListComponent, 33 | TodoListItemComponent, 34 | TodoTeamComponent, 35 | ], 36 | imports: [ 37 | BrowserModule, 38 | BrowserAnimationsModule, 39 | FormsModule, 40 | ReactiveFormsModule, 41 | MdButtonModule, 42 | MdInputModule, 43 | MdCardModule, 44 | MdProgressSpinnerModule, 45 | MdChipsModule, 46 | MdIconModule, 47 | AppApolloModule, 48 | AppRoutingModule 49 | ], 50 | providers: [], 51 | bootstrap: [AppComponent] 52 | }) 53 | export class AppModule { } 54 | -------------------------------------------------------------------------------- /src/app/todos/todo-create/todo-create.component.css: -------------------------------------------------------------------------------- 1 | .full-width { 2 | width: 100%; 3 | } 4 | 5 | .create-todo-form-container { 6 | background-color: #f2f2f2; 7 | padding: 2em; 8 | } 9 | 10 | .loading-spinner { 11 | height: 1.5em; 12 | width: 1.5em; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/todos/todo-create/todo-create.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |
-------------------------------------------------------------------------------- /src/app/todos/todo-create/todo-create.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | 4 | import { Apollo } from 'apollo-angular'; 5 | import { DocumentNode } from 'graphql'; 6 | import 'rxjs/add/operator/toPromise'; 7 | 8 | const CreateTodoMutationNode: DocumentNode = require('graphql-tag/loader!../../../graphql/CreateTodo.graphql'); 9 | 10 | import { 11 | CreateTodoMutationVariables 12 | } from '../../../graphql/schema'; 13 | 14 | @Component({ 15 | selector: 'app-todo-create', 16 | templateUrl: 'todo-create.component.html', 17 | styleUrls: ['todo-create.component.css'] 18 | }) 19 | export class TodoCreateComponent { 20 | @Input() teamName: string; 21 | todoCreateForm: FormGroup; 22 | loading: boolean; 23 | constructor( 24 | private apollo: Apollo, 25 | private fb: FormBuilder 26 | ) { 27 | this.init(); 28 | } 29 | 30 | init() { 31 | this.todoCreateForm = this.fb.group({ 32 | name: ['', Validators.required], 33 | author: ['', Validators.required], 34 | content: ['', Validators.required] 35 | }); 36 | } 37 | 38 | resetForm() { 39 | this.todoCreateForm.reset(); 40 | Object.keys(this.todoCreateForm.controls).forEach(key => { 41 | this.todoCreateForm.controls[key].setErrors(null); 42 | }); 43 | } 44 | 45 | onSubmit() { 46 | if (this.todoCreateForm.valid) { 47 | this.loading = true; 48 | const variables: CreateTodoMutationVariables = { 49 | input: { 50 | name: this.todoCreateForm.value.name, 51 | content: this.todoCreateForm.value.content, 52 | author: this.todoCreateForm.value.author, 53 | timestamp: new Date().toISOString(), 54 | teamName: this.teamName 55 | } 56 | }; 57 | this.apollo.mutate({ 58 | mutation: CreateTodoMutationNode, 59 | variables, 60 | updateQueries: { 61 | TeamTodos: (prev: any, { mutationResult }: any) => { 62 | if (!mutationResult.data) { 63 | return prev; 64 | } 65 | // handle case where subscription returned before mutation result 66 | const teamTodoExists = prev.teamTodos.find(obj => obj.id === mutationResult.data.createTodo.id); 67 | if (teamTodoExists) { 68 | return prev; 69 | } 70 | const newTodo = Object.assign({}, 71 | mutationResult.data.createTodo, 72 | variables.input); 73 | return { 74 | teamTodos: [...prev.teamTodos, newTodo] 75 | }; 76 | } 77 | } 78 | }) 79 | .toPromise() 80 | .then(res => { 81 | this.loading = false; 82 | this.resetForm(); 83 | }) 84 | .catch(err => { 85 | this.loading = false; 86 | console.log('error'); 87 | window.alert(err); 88 | }); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/app/todos/todo-home/todo-home.component.html: -------------------------------------------------------------------------------- 1 |
2 |

SELECT TEAM

3 |
4 | 5 | 6 | 7 | 8 |
9 |
-------------------------------------------------------------------------------- /src/app/todos/todo-home/todo-home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-todo-home', 7 | templateUrl: 'todo-home.component.html' 8 | }) 9 | export class TodoHomeComponent { 10 | selectTeamForm: FormGroup; 11 | constructor( 12 | private fb: FormBuilder, 13 | public router: Router 14 | ) { 15 | this.selectTeamForm = this.fb.group({ 16 | teamName: ['', Validators.required] 17 | }); 18 | } 19 | onSubmit() { 20 | if (this.selectTeamForm.valid) { 21 | this.router.navigate(['./team', this.selectTeamForm.value.teamName]); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/todos/todo-list-item/todo-list-item.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{item.name}} 4 | {{item.timestamp | date:'medium' }} 5 | 6 | 7 |

8 | {{item.content}} 9 |

10 |

11 | 12 | {{item.author}} 13 | 14 | 15 |

16 | 17 |
18 |
-------------------------------------------------------------------------------- /src/app/todos/todo-list-item/todo-list-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-todo-list-item', 5 | templateUrl: 'todo-list-item.component.html' 6 | }) 7 | export class TodoListItemComponent { 8 | @Input() item; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/todos/todo-list/todo-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
-------------------------------------------------------------------------------- /src/app/todos/todo-list/todo-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-todo-list', 5 | templateUrl: 'todo-list.component.html' 6 | }) 7 | export class TodoListComponent { 8 | _todoItems; 9 | @Input() 10 | set todoItems(value: Array) { 11 | let items = [...value]; 12 | this._todoItems = items.sort(function (a, b) { 13 | a = new Date(a.timestamp); 14 | b = new Date(b.timestamp); 15 | return a > b ? -1 : a < b ? 1 : 0; 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/todos/todo-team/todo-team.component.css: -------------------------------------------------------------------------------- 1 | .team-container { 2 | margin: 2em; 3 | } 4 | 5 | .team-main { 6 | display: flex; 7 | } 8 | 9 | .team-left-section { 10 | flex: 3; 11 | } 12 | 13 | .team-right-section { 14 | flex: 8; 15 | margin-left:3em; 16 | } 17 | 18 | .loading-spinner { 19 | height: 1.5em; 20 | width: 1.5em; 21 | } 22 | -------------------------------------------------------------------------------- /src/app/todos/todo-team/todo-team.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 8 |

TEAM: {{teamName | uppercase}}

9 |
10 | 11 |
12 |
13 |

TODO ITEMS

14 |
15 | 16 |
17 |
18 | 19 | No todo items found 20 |
21 |
22 |
23 |
-------------------------------------------------------------------------------- /src/app/todos/todo-team/todo-team.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { DocumentNode } from 'graphql'; 4 | import { Apollo } from 'apollo-angular'; 5 | 6 | 7 | import { 8 | TeamTodosQuery 9 | } from '../../../graphql/schema'; 10 | 11 | const TeamTodosQueryNode: DocumentNode = require('graphql-tag/loader!../../../graphql/TeamTodos.graphql'); 12 | const TeamTodoAdded: DocumentNode = require('graphql-tag/loader!../../../graphql/TeamTodoAdded.graphql'); 13 | 14 | 15 | @Component({ 16 | selector: 'app-todo-team', 17 | templateUrl: 'todo-team.component.html', 18 | styleUrls: ['todo-team.component.css'] 19 | }) 20 | export class TodoTeamComponent implements OnInit, OnDestroy { 21 | inited: boolean; 22 | teamTodosQuerySub; 23 | teamTodosSubscriptionSub; 24 | teamName: string; 25 | todoItems = []; 26 | constructor( 27 | private route: ActivatedRoute, 28 | private router: Router, 29 | private apollo: Apollo, 30 | private ref: ChangeDetectorRef 31 | ) { 32 | } 33 | 34 | ngOnInit() { 35 | this.teamName = this.route.snapshot.params.teamName.toLowerCase(); 36 | const todosQueryVariables = { 37 | teamName: this.teamName 38 | }; 39 | this.teamTodosQuerySub = this.apollo.watchQuery({ 40 | query: TeamTodosQueryNode, 41 | variables: todosQueryVariables 42 | }) 43 | .subscribe(res => { 44 | this.todoItems = res.data.teamTodos; 45 | this.ref.detectChanges(); 46 | this.inited = true; 47 | }); 48 | this.initTodoAddedSubscription(); 49 | } 50 | 51 | goBack() { 52 | this.router.navigate(['']); 53 | } 54 | 55 | initTodoAddedSubscription() { 56 | const self = this; 57 | // subscribeToMore helper in apollo-angular has an open issue hence using apollo client for subscribe method 58 | this.teamTodosSubscriptionSub = this.apollo.getClient().subscribe({ 59 | query: TeamTodoAdded, 60 | variables: { 61 | teamName: this.teamName 62 | } 63 | }).subscribe({ 64 | next(data) { 65 | let currentTeamTodos: any; 66 | try { 67 | currentTeamTodos = self.apollo.getClient().readQuery({ 68 | query: TeamTodosQueryNode, 69 | variables: { 70 | teamName: self.teamName, 71 | } 72 | }); 73 | } catch (e) { 74 | currentTeamTodos = { 75 | teamTodos: [] 76 | }; 77 | } 78 | const newTodo = data.teamTodoAdded; 79 | const teamTodoExists = currentTeamTodos.teamTodos.find(obj => obj.id === newTodo.id); 80 | if (!teamTodoExists) { 81 | const updatedTodos = { 82 | teamTodos: [...currentTeamTodos.teamTodos, newTodo] 83 | }; 84 | self.apollo.getClient().writeQuery({ 85 | query: TeamTodosQueryNode, 86 | variables: { 87 | teamName: self.teamName, 88 | }, 89 | data: updatedTodos 90 | }); 91 | } 92 | } 93 | }); 94 | } 95 | 96 | ngOnDestroy() { 97 | if (this.teamTodosQuerySub) { 98 | this.teamTodosQuerySub.unsubscribe(); 99 | } 100 | if (this.teamTodosSubscriptionSub) { 101 | this.teamTodosSubscriptionSub.unsubscribe(); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ioxe/graphql-aws-iot-example/dfa6c2213389464846684df6c217dcda40e69493/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/enviornment/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | AppPrefix: 'IOX', 8 | identityPoolId: 'us-west-2:23988fd3-692b-474c-9f99-67e9c6bc1100', 9 | region: 'us-west-2', 10 | iotEndpoint: 'afbxc4n5814kd.iot.us-west-2.amazonaws.com' 11 | }; 12 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false, 8 | AppPrefix: 'IOX', 9 | identityPoolId: 'us-west-2:23988fd3-692b-474c-9f99-67e9c6bc1100', 10 | region: 'us-west-2', 11 | iotEndpoint: 'afbxc4n5814kd.iot.us-west-2.amazonaws.com' 12 | }; 13 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ioxe/graphql-aws-iot-example/dfa6c2213389464846684df6c217dcda40e69493/src/favicon.ico -------------------------------------------------------------------------------- /src/graphql/CreateTodo.graphql: -------------------------------------------------------------------------------- 1 | mutation CreateTodo($input:CreateTodoInput!) { 2 | createTodo(input:$input) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/graphql/TeamTodoAdded.graphql: -------------------------------------------------------------------------------- 1 | subscription TeamTodoAdded($teamName: String!) { 2 | teamTodoAdded(teamName:$teamName) { 3 | id 4 | name 5 | author 6 | content 7 | timestamp 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/graphql/TeamTodos.graphql: -------------------------------------------------------------------------------- 1 | query TeamTodos($teamName: String!) { 2 | teamTodos(teamName: $teamName) { 3 | id 4 | name 5 | author 6 | content 7 | timestamp 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/graphql/schema.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | // This file was automatically generated and should not be edited. 3 | 4 | export type CreateTodoInput = { 5 | timestamp: string, 6 | name: string, 7 | content: string, 8 | teamName: string, 9 | author: string, 10 | }; 11 | 12 | export type CreateTodoMutationVariables = { 13 | input: CreateTodoInput, 14 | }; 15 | 16 | export type CreateTodoMutation = { 17 | // Create a new todo item 18 | createTodo: { 19 | // Db uuid 20 | id: string, 21 | } | null, 22 | }; 23 | 24 | export type TeamTodoAddedSubscriptionVariables = { 25 | teamName: string, 26 | }; 27 | 28 | export type TeamTodoAddedSubscription = { 29 | // New todo added 30 | teamTodoAdded: { 31 | // Db uuid 32 | id: string, 33 | // Unique friendly name for todo item 34 | name: string, 35 | // Content of todo item 36 | content: string, 37 | // ISO date string 38 | timestamp: string, 39 | } | null, 40 | }; 41 | 42 | export type TeamTodosQueryVariables = { 43 | teamName: string, 44 | }; 45 | 46 | export type TeamTodosQuery = { 47 | // List of todos for user 48 | teamTodos: Array< { 49 | // Db uuid 50 | id: string, 51 | // Unique friendly name for todo item 52 | name: string, 53 | // Content of todo item 54 | content: string, 55 | // ISO date string 56 | timestamp: string, 57 | } | null > | null, 58 | }; 59 | /* tslint:enable */ 60 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Serverless Subscriptions 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import 'hammerjs'; 2 | 3 | import { enableProdMode } from '@angular/core'; 4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 5 | 6 | import { AppModule } from './app/app.module'; 7 | import { environment } from './environments/environment'; 8 | 9 | enableProdMode(); 10 | 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** Evergreen browsers require these. **/ 41 | import 'core-js/es6/reflect'; 42 | import 'core-js/es7/reflect'; 43 | 44 | 45 | /** 46 | * Required to support Web Animations `@angular/animation`. 47 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 48 | **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | /** 70 | * Need to import at least one locale-data with intl. 71 | */ 72 | // import 'intl/locale-data/jsonp/en'; 73 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "~@angular/material/prebuilt-themes/indigo-pink.css"; 3 | 4 | .full-width { 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [ 8 | "node", 9 | "isomorphic-fetch" 10 | ] 11 | }, 12 | "exclude": [ 13 | "test.ts", 14 | "**/*.spec.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "lib": ["es2015", "dom"], 10 | "noImplicitAny": true, 11 | "suppressImplicitAnyIndexErrors": true 12 | } 13 | } -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2016", 16 | "dom", 17 | "esnext.asynciterable" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "eofline": true, 15 | "forin": true, 16 | "import-blacklist": [ 17 | true, 18 | "rxjs" 19 | ], 20 | "import-spacing": true, 21 | "indent": [ 22 | true, 23 | "spaces" 24 | ], 25 | "interface-over-type-literal": true, 26 | "label-position": true, 27 | "max-line-length": [ 28 | true, 29 | 140 30 | ], 31 | "member-access": false, 32 | "member-ordering": [ 33 | true, 34 | { 35 | "order": [ 36 | "static-field", 37 | "instance-field", 38 | "static-method", 39 | "instance-method" 40 | ] 41 | } 42 | ], 43 | "no-arg": true, 44 | "no-bitwise": true, 45 | "no-console": [ 46 | true, 47 | "debug", 48 | "info", 49 | "time", 50 | "timeEnd", 51 | "trace" 52 | ], 53 | "no-construct": true, 54 | "no-debugger": true, 55 | "no-duplicate-super": true, 56 | "no-empty": false, 57 | "no-empty-interface": true, 58 | "no-eval": true, 59 | "no-inferrable-types": [ 60 | true, 61 | "ignore-params" 62 | ], 63 | "no-misused-new": true, 64 | "no-non-null-assertion": true, 65 | "no-shadowed-variable": true, 66 | "no-string-literal": false, 67 | "no-string-throw": true, 68 | "no-switch-case-fall-through": true, 69 | "no-trailing-whitespace": true, 70 | "no-unnecessary-initializer": true, 71 | "no-unused-expression": true, 72 | "no-use-before-declare": true, 73 | "no-var-keyword": true, 74 | "object-literal-sort-keys": false, 75 | "one-line": [ 76 | true, 77 | "check-open-brace", 78 | "check-catch", 79 | "check-else", 80 | "check-whitespace" 81 | ], 82 | "prefer-const": true, 83 | "quotemark": [ 84 | true, 85 | "single" 86 | ], 87 | "radix": true, 88 | "semicolon": [ 89 | true, 90 | "always" 91 | ], 92 | "triple-equals": [ 93 | true, 94 | "allow-null-check" 95 | ], 96 | "typedef-whitespace": [ 97 | true, 98 | { 99 | "call-signature": "nospace", 100 | "index-signature": "nospace", 101 | "parameter": "nospace", 102 | "property-declaration": "nospace", 103 | "variable-declaration": "nospace" 104 | } 105 | ], 106 | "typeof-compare": true, 107 | "unified-signatures": true, 108 | "variable-name": false, 109 | "whitespace": [ 110 | true, 111 | "check-branch", 112 | "check-decl", 113 | "check-operator", 114 | "check-separator", 115 | "check-type" 116 | ], 117 | "directive-selector": [ 118 | true, 119 | "attribute", 120 | "app", 121 | "camelCase" 122 | ], 123 | "component-selector": [ 124 | true, 125 | "element", 126 | "app", 127 | "kebab-case" 128 | ], 129 | "use-input-property-decorator": true, 130 | "use-output-property-decorator": true, 131 | "use-host-property-decorator": true, 132 | "no-input-rename": true, 133 | "no-output-rename": true, 134 | "use-life-cycle-interface": true, 135 | "use-pipe-transform-interface": true, 136 | "component-class-suffix": true, 137 | "directive-class-suffix": true, 138 | "no-access-missing-member": true, 139 | "templates-use-public": true, 140 | "invoke-injectable": true 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const ProgressPlugin = require('webpack/lib/ProgressPlugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin') 6 | const autoprefixer = require('autoprefixer'); 7 | const postcssUrl = require('postcss-url'); 8 | const cssnano = require('cssnano'); 9 | 10 | const { NoEmitOnErrorsPlugin, SourceMapDevToolPlugin, NamedModulesPlugin, DefinePlugin } = require('webpack'); 11 | const { GlobCopyWebpackPlugin, BaseHrefWebpackPlugin } = require('@angular/cli/plugins/webpack'); 12 | const { CommonsChunkPlugin } = require('webpack').optimize; 13 | const { AotPlugin } = require('@ngtools/webpack'); 14 | 15 | const nodeModules = path.join(process.cwd(), 'node_modules'); 16 | const realNodeModules = fs.realpathSync(nodeModules); 17 | const genDirNodeModules = path.join(process.cwd(), 'src', '$$_gendir', 'node_modules'); 18 | const entryPoints = ["inline", "polyfills", "sw-register", "styles", "vendor", "main"]; 19 | const minimizeCss = false; 20 | const baseHref = ""; 21 | const deployUrl = ""; 22 | const postcssPlugins = function () { 23 | // safe settings based on: https://github.com/ben-eb/cssnano/issues/358#issuecomment-283696193 24 | const importantCommentRe = /@preserve|@license|[@#]\s*source(?:Mapping)?URL|^!/i; 25 | const minimizeOptions = { 26 | autoprefixer: false, 27 | safe: true, 28 | mergeLonghand: false, 29 | discardComments: { remove: (comment) => !importantCommentRe.test(comment) } 30 | }; 31 | return [ 32 | postcssUrl({ 33 | url: (URL) => { 34 | // Only convert root relative URLs, which CSS-Loader won't process into require(). 35 | if (!URL.startsWith('/') || URL.startsWith('//')) { 36 | return URL; 37 | } 38 | if (deployUrl.match(/:\/\//)) { 39 | // If deployUrl contains a scheme, ignore baseHref use deployUrl as is. 40 | return `${deployUrl.replace(/\/$/, '')}${URL}`; 41 | } 42 | else if (baseHref.match(/:\/\//)) { 43 | // If baseHref contains a scheme, include it as is. 44 | return baseHref.replace(/\/$/, '') + 45 | `/${deployUrl}/${URL}`.replace(/\/\/+/g, '/'); 46 | } 47 | else { 48 | // Join together base-href, deploy-url and the original URL. 49 | // Also dedupe multiple slashes into single ones. 50 | return `/${baseHref}/${deployUrl}/${URL}`.replace(/\/\/+/g, '/'); 51 | } 52 | } 53 | }), 54 | autoprefixer(), 55 | ].concat(minimizeCss ? [cssnano(minimizeOptions)] : []); 56 | }; 57 | 58 | 59 | 60 | 61 | const config = { 62 | "resolve": { 63 | "extensions": [ 64 | ".ts", 65 | ".js" 66 | ], 67 | "modules": [ 68 | "./node_modules", 69 | "./node_modules" 70 | ], 71 | "symlinks": true 72 | }, 73 | "resolveLoader": { 74 | "modules": [ 75 | "./node_modules", 76 | "./node_modules" 77 | ] 78 | }, 79 | "entry": { 80 | "main": [ 81 | "./src/main.ts" 82 | ], 83 | "polyfills": [ 84 | "./src/polyfills.ts" 85 | ], 86 | "styles": [ 87 | "./src/styles.css" 88 | ] 89 | }, 90 | "output": { 91 | "path": path.join(process.cwd(), "dist"), 92 | "filename": "[name].[hash].bundle.js", 93 | "chunkFilename": "[id].chunk.js" 94 | }, 95 | "module": { 96 | "rules": [ 97 | { 98 | "enforce": "pre", 99 | "test": /\.js$/, 100 | "loader": "source-map-loader", 101 | "exclude": [ 102 | /(\\|\/)node_modules(\\|\/)/ 103 | ] 104 | }, 105 | { 106 | "test": /\.html$/, 107 | "loader": "raw-loader" 108 | }, 109 | { 110 | "test": /\.(eot|svg|cur)$/, 111 | "loader": "file-loader?name=[name].[hash:20].[ext]" 112 | }, 113 | { 114 | "test": /\.(jpg|png|webp|gif|otf|ttf|woff|woff2|ani)$/, 115 | "loader": "url-loader?name=[name].[hash:20].[ext]&limit=10000" 116 | }, 117 | { 118 | "exclude": [ 119 | path.join(process.cwd(), "src/styles.css") 120 | ], 121 | "test": /\.css$/, 122 | "use": [ 123 | "exports-loader?module.exports.toString()", 124 | { 125 | "loader": "css-loader", 126 | "options": { 127 | "sourceMap": false, 128 | "importLoaders": 1 129 | } 130 | }, 131 | { 132 | "loader": "postcss-loader", 133 | "options": { 134 | "ident": "postcss", 135 | "plugins": postcssPlugins 136 | } 137 | } 138 | ] 139 | }, 140 | { 141 | "exclude": [ 142 | path.join(process.cwd(), "src/styles.css") 143 | ], 144 | "test": /\.scss$|\.sass$/, 145 | "use": [ 146 | "exports-loader?module.exports.toString()", 147 | { 148 | "loader": "css-loader", 149 | "options": { 150 | "sourceMap": false, 151 | "importLoaders": 1 152 | } 153 | }, 154 | { 155 | "loader": "postcss-loader", 156 | "options": { 157 | "ident": "postcss", 158 | "plugins": postcssPlugins 159 | } 160 | }, 161 | { 162 | "loader": "sass-loader", 163 | "options": { 164 | "sourceMap": false, 165 | "precision": 8, 166 | "includePaths": [] 167 | } 168 | } 169 | ] 170 | }, 171 | { 172 | "exclude": [ 173 | path.join(process.cwd(), "src/styles.css") 174 | ], 175 | "test": /\.less$/, 176 | "use": [ 177 | "exports-loader?module.exports.toString()", 178 | { 179 | "loader": "css-loader", 180 | "options": { 181 | "sourceMap": false, 182 | "importLoaders": 1 183 | } 184 | }, 185 | { 186 | "loader": "postcss-loader", 187 | "options": { 188 | "ident": "postcss", 189 | "plugins": postcssPlugins 190 | } 191 | }, 192 | { 193 | "loader": "less-loader", 194 | "options": { 195 | "sourceMap": false 196 | } 197 | } 198 | ] 199 | }, 200 | { 201 | "exclude": [ 202 | path.join(process.cwd(), "src/styles.css") 203 | ], 204 | "test": /\.styl$/, 205 | "use": [ 206 | "exports-loader?module.exports.toString()", 207 | { 208 | "loader": "css-loader", 209 | "options": { 210 | "sourceMap": false, 211 | "importLoaders": 1 212 | } 213 | }, 214 | { 215 | "loader": "postcss-loader", 216 | "options": { 217 | "ident": "postcss", 218 | "plugins": postcssPlugins 219 | } 220 | }, 221 | { 222 | "loader": "stylus-loader", 223 | "options": { 224 | "sourceMap": false, 225 | "paths": [] 226 | } 227 | } 228 | ] 229 | }, 230 | { 231 | "include": [ 232 | path.join(process.cwd(), "src/styles.css") 233 | ], 234 | "test": /\.css$/, 235 | "use": [ 236 | "style-loader", 237 | { 238 | "loader": "css-loader", 239 | "options": { 240 | "sourceMap": false, 241 | "importLoaders": 1 242 | } 243 | }, 244 | { 245 | "loader": "postcss-loader", 246 | "options": { 247 | "ident": "postcss", 248 | "plugins": postcssPlugins 249 | } 250 | } 251 | ] 252 | }, 253 | { 254 | "include": [ 255 | path.join(process.cwd(), "src/styles.css") 256 | ], 257 | "test": /\.scss$|\.sass$/, 258 | "use": [ 259 | "style-loader", 260 | { 261 | "loader": "css-loader", 262 | "options": { 263 | "sourceMap": false, 264 | "importLoaders": 1 265 | } 266 | }, 267 | { 268 | "loader": "postcss-loader", 269 | "options": { 270 | "ident": "postcss", 271 | "plugins": postcssPlugins 272 | } 273 | }, 274 | { 275 | "loader": "sass-loader", 276 | "options": { 277 | "sourceMap": false, 278 | "precision": 8, 279 | "includePaths": [] 280 | } 281 | } 282 | ] 283 | }, 284 | { 285 | "include": [ 286 | path.join(process.cwd(), "src/styles.css") 287 | ], 288 | "test": /\.less$/, 289 | "use": [ 290 | "style-loader", 291 | { 292 | "loader": "css-loader", 293 | "options": { 294 | "sourceMap": false, 295 | "importLoaders": 1 296 | } 297 | }, 298 | { 299 | "loader": "postcss-loader", 300 | "options": { 301 | "ident": "postcss", 302 | "plugins": postcssPlugins 303 | } 304 | }, 305 | { 306 | "loader": "less-loader", 307 | "options": { 308 | "sourceMap": false 309 | } 310 | } 311 | ] 312 | }, 313 | { 314 | "include": [ 315 | path.join(process.cwd(), "src/styles.css") 316 | ], 317 | "test": /\.styl$/, 318 | "use": [ 319 | "style-loader", 320 | { 321 | "loader": "css-loader", 322 | "options": { 323 | "sourceMap": false, 324 | "importLoaders": 1 325 | } 326 | }, 327 | { 328 | "loader": "postcss-loader", 329 | "options": { 330 | "ident": "postcss", 331 | "plugins": postcssPlugins 332 | } 333 | }, 334 | { 335 | "loader": "stylus-loader", 336 | "options": { 337 | "sourceMap": false, 338 | "paths": [] 339 | } 340 | } 341 | ] 342 | }, 343 | { 344 | "test": /\.ts$/, 345 | "loader": "@ngtools/webpack" 346 | } 347 | ] 348 | }, 349 | "plugins": [ 350 | new NoEmitOnErrorsPlugin(), 351 | new GlobCopyWebpackPlugin({ 352 | "patterns": [ 353 | "assets", 354 | "favicon.ico" 355 | ], 356 | "globOptions": { 357 | "cwd": path.join(process.cwd(), "src"), 358 | "dot": true, 359 | "ignore": "**/.gitkeep" 360 | } 361 | }), 362 | new ProgressPlugin(), 363 | new HtmlWebpackPlugin({ 364 | "template": "./src/index.html", 365 | "filename": "./index.html", 366 | "hash": false, 367 | "inject": true, 368 | "compile": true, 369 | "favicon": false, 370 | "minify": false, 371 | "cache": true, 372 | "showErrors": true, 373 | "chunks": "all", 374 | "excludeChunks": [], 375 | "title": "Webpack App", 376 | "xhtml": true, 377 | "chunksSortMode": function sort(left, right) { 378 | let leftIndex = entryPoints.indexOf(left.names[0]); 379 | let rightindex = entryPoints.indexOf(right.names[0]); 380 | if (leftIndex > rightindex) { 381 | return 1; 382 | } 383 | else if (leftIndex < rightindex) { 384 | return -1; 385 | } 386 | else { 387 | return 0; 388 | } 389 | } 390 | }), 391 | new BaseHrefWebpackPlugin({}), 392 | new CommonsChunkPlugin({ 393 | "minChunks": 2, 394 | "async": "common" 395 | }), 396 | new CommonsChunkPlugin({ 397 | "name": [ 398 | "inline" 399 | ], 400 | "minChunks": null 401 | }), 402 | new CommonsChunkPlugin({ 403 | "name": [ 404 | "vendor" 405 | ], 406 | "minChunks": (module) => { 407 | return module.resource 408 | && (module.resource.startsWith(nodeModules) 409 | || module.resource.startsWith(genDirNodeModules) 410 | || module.resource.startsWith(realNodeModules)); 411 | }, 412 | "chunks": [ 413 | "main" 414 | ] 415 | }), 416 | new SourceMapDevToolPlugin({ 417 | "filename": "[file].map[query]", 418 | "moduleFilenameTemplate": "[resource-path]", 419 | "fallbackModuleFilenameTemplate": "[resource-path]?[hash]", 420 | "sourceRoot": "webpack:///" 421 | }), 422 | new NamedModulesPlugin({}), 423 | new AotPlugin({ 424 | "mainPath": "main.ts", 425 | "hostReplacementPaths": { 426 | "environments/environment.ts": "environments/environment.ts" 427 | }, 428 | "exclude": [], 429 | "tsConfigPath": "src/tsconfig.app.json", 430 | "skipCodeGeneration": true 431 | }), 432 | new DefinePlugin({ 433 | 'process.env': { 434 | 'NODE_ENV': JSON.stringify(process.env.NODE_ENV) 435 | } 436 | }) 437 | ], 438 | "node": { 439 | "fs": "empty", 440 | "global": true, 441 | "tls": "empty", 442 | "net": "empty", 443 | "process": true, 444 | "module": false, 445 | "clearImmediate": false, 446 | "setImmediate": false 447 | }, 448 | "devServer": { 449 | "historyApiFallback": true 450 | } 451 | }; 452 | 453 | if (process.env.NODE_ENV === 'production') { 454 | config.plugins.push(new UglifyJSPlugin({ sourceMap: true }) 455 | ) 456 | } 457 | module.exports = config; --------------------------------------------------------------------------------