├── .babelrc ├── .circleci └── config.yml ├── .dockerignore ├── .github └── FUNDING.yml ├── .gitignore ├── .gitlab-ci.yml ├── .graphqlconfig ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── .webpack ├── dependencies │ ├── package-lock.json │ └── package.json └── service │ ├── handler.js │ └── handler.js.map ├── CHANGELOG ├── LICENSE ├── __tests__ ├── createEvent.test.js ├── createEvent.test.js.map ├── createEvent.test.ts ├── queryEvent.test.js ├── queryEvent.test.js.map └── queryEvent.test.ts ├── _config.yml ├── client ├── index.html └── style.css ├── configs └── test.webpack.config.js ├── docker-compose.yml ├── dockerfile ├── dynamodb └── index.js ├── elasticSearch ├── config.ts ├── elasticSearchConnect.ts └── mappings.ts ├── env.yml ├── handler.ts ├── images ├── GraphQLPlay.PNG ├── GraphQLPlayground.PNG └── deploy-dev.PNG ├── jest.config.js ├── package-lock.json ├── package.json ├── readme.md ├── resolvers └── events │ ├── create.ts │ ├── list.ts │ ├── remove.ts │ ├── typings.ts │ └── view.ts ├── resources ├── cognito-identity-pool.yml ├── cognito-user-pool.yml ├── dynamodb.yml ├── elasticsearch.yml └── s3-bucket-cdn.yml ├── schema.graphql ├── schemas └── index.ts ├── serverless.yml ├── streams ├── process.ts ├── sampleEvent.json └── utils │ └── index.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/typescript", 4 | [ 5 | "@babel/env", 6 | { 7 | "modules": false, 8 | "targets": { 9 | "node": "8.20" 10 | } 11 | } 12 | ] 13 | ], 14 | "plugins": ["@babel/plugin-proposal-object-rest-spread"] 15 | } -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | # The primary container is an instance of the first image listed. The job's commands run in this container. 5 | docker: 6 | - image: circleci/node:8.2 7 | steps: 8 | - checkout 9 | - run: 10 | name: Npm Install 11 | command: npm install 12 | - save_cache: 13 | key: dependency-cache-se-be-{{ checksum "package.json" }} 14 | paths: 15 | - ./node_modules 16 | test: 17 | docker: 18 | - image: circleci/node:8.2 19 | steps: 20 | - checkout 21 | - restore_cache: 22 | key: dependency-cache-se-be-{{ checksum "package.json" }} 23 | - run: 24 | name: Test 25 | command: npm install tslint 26 | - run: 27 | name: Test 28 | command: npm run tslint 29 | - run: 30 | name: Build unit tests 31 | command: npm run compile-tests 32 | - run: 33 | name: Run unit tests and get code coverage 34 | command: npm run unit-tests 35 | deploy: 36 | working_directory: ~/serverlesseventsbe 37 | docker: 38 | - image: circleci/node:8.2 39 | steps: 40 | - checkout 41 | - restore_cache: 42 | key: dependency-cache-se-be-{{ checksum "package.json" }} 43 | - run: 44 | name: Npm Install 45 | command: npm install 46 | - run: 47 | name: Install global dependancies 48 | command: sudo npm install -g serverless 49 | - run: 50 | name: Create default profile on aws using credentials 51 | command: serverless config credentials --provider aws --key $AWS_ACCESS_KEY_ID --secret $AWS_SECRET_ACCESS_KEY 52 | - run: 53 | name: Deploy resources and upload code to aws 54 | command: npm run deploy-dev 55 | 56 | 57 | workflows: 58 | version: 2 59 | build_test_and_deploy: 60 | jobs: 61 | - build 62 | - test: 63 | requires: 64 | - build 65 | - deploy: 66 | context: AWS_CRED 67 | requires: 68 | - build -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile 4 | .dockerignore 5 | .env.docker -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: dasithkuruppu 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | #dynamodb offline relate files 9 | .dynamodb 10 | 11 | 12 | # test coveragr files 13 | coverage 14 | 15 | # env files 16 | .env* -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:latest 2 | 3 | stages: 4 | - build # install all npm dependencies 5 | - test # execute unit tests 6 | - deploy # deploy to aws 7 | 8 | cache: 9 | paths: 10 | - node_modules/ 11 | 12 | npm_Install: 13 | stage: build 14 | script: 15 | - npm install 16 | # artifacts describe the result of the stage 17 | # that can be used in consecutive stages 18 | artifacts: 19 | untracked: true 20 | 21 | linter_code_check: 22 | stage: test 23 | script: 24 | # install standardjs for linting 25 | - npm run tslint 26 | Jest_Unit_Tests: 27 | stage: test 28 | script: 29 | - npm run unit-tests 30 | coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/ 31 | 32 | Deploy_To_AWS: 33 | stage: deploy 34 | # use the artifcats of the 'npmInstall' job 35 | dependencies: 36 | - npm_Install 37 | script: 38 | - npm install -g serverless 39 | - serverless config credentials --provider aws --key $AWS_ACCESS_KEY_ID --secret $AWS_SECRET_ACCESS_KEY 40 | - npm run deploy-dev 41 | -------------------------------------------------------------------------------- /.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "Events-Backend": { 4 | "schemaPath": "schema.graphql", 5 | "extensions": { 6 | "endpoints": { 7 | "dev": "https://a8noghdsdb.execute-api.us-east-1.amazonaws.com/default/events" 8 | } 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Serverless Offline", 11 | "program": "${workspaceRoot}/node_modules/serverless/bin/serverless", 12 | "args": ["offline", "--noTimeout", "--dontPrintOutput", "--stage=default"], 13 | "sourceMaps": true, 14 | "runtimeArgs": ["--lazy"], 15 | "outFiles": ["${workspaceFolder}/.webpack/**/*.js"], 16 | "protocol": "inspector", 17 | "runtimeExecutable": "node", 18 | "env": { 19 | // Here we set some environment vars that should be set locally. 20 | // They can and will overwrite the ones coming from your serverless.yml 21 | }, 22 | "windows": { 23 | "program": "${workspaceRoot}\\node_modules\\serverless\\bin\\serverless" 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {// Format a file on save. A formatter must be available, the file must not be auto-saved, and editor must not be shutting down. 2 | "editor.formatOnSave": true, 3 | // Enable/disable default JavaScript formatter (For Prettier) 4 | "javascript.format.enable": false, 5 | // Use 'prettier-eslint' instead of 'prettier'. Other settings will only be fallbacks in case they could not be inferred from eslint rules. 6 | } -------------------------------------------------------------------------------- /.webpack/dependencies/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-nodejs", 3 | "version": "1.0.0", 4 | "description": "Packaged externals for aws-nodejs", 5 | "private": true, 6 | "scripts": {}, 7 | "dependencies": { 8 | "graphql": "^14.0.2", 9 | "aws-sdk": "^2.361.0", 10 | "graphql-iso-date": "^3.6.1", 11 | "uuid": "^3.3.2", 12 | "@elastic/elasticsearch": "^7.0.0-rc.2" 13 | } 14 | } -------------------------------------------------------------------------------- /.webpack/service/handler.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | /******/ (function(modules) { // webpackBootstrap 3 | /******/ // The module cache 4 | /******/ var installedModules = {}; 5 | /******/ 6 | /******/ // The require function 7 | /******/ function __webpack_require__(moduleId) { 8 | /******/ 9 | /******/ // Check if module is in cache 10 | /******/ if(installedModules[moduleId]) { 11 | /******/ return installedModules[moduleId].exports; 12 | /******/ } 13 | /******/ // Create a new module (and put it into the cache) 14 | /******/ var module = installedModules[moduleId] = { 15 | /******/ i: moduleId, 16 | /******/ l: false, 17 | /******/ exports: {} 18 | /******/ }; 19 | /******/ 20 | /******/ // Execute the module function 21 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 22 | /******/ 23 | /******/ // Flag the module as loaded 24 | /******/ module.l = true; 25 | /******/ 26 | /******/ // Return the exports of the module 27 | /******/ return module.exports; 28 | /******/ } 29 | /******/ 30 | /******/ 31 | /******/ // expose the modules object (__webpack_modules__) 32 | /******/ __webpack_require__.m = modules; 33 | /******/ 34 | /******/ // expose the module cache 35 | /******/ __webpack_require__.c = installedModules; 36 | /******/ 37 | /******/ // define getter function for harmony exports 38 | /******/ __webpack_require__.d = function(exports, name, getter) { 39 | /******/ if(!__webpack_require__.o(exports, name)) { 40 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 41 | /******/ } 42 | /******/ }; 43 | /******/ 44 | /******/ // define __esModule on exports 45 | /******/ __webpack_require__.r = function(exports) { 46 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 47 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 48 | /******/ } 49 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 50 | /******/ }; 51 | /******/ 52 | /******/ // create a fake namespace object 53 | /******/ // mode & 1: value is a module id, require it 54 | /******/ // mode & 2: merge all properties of value into the ns 55 | /******/ // mode & 4: return value when already ns object 56 | /******/ // mode & 8|1: behave like require 57 | /******/ __webpack_require__.t = function(value, mode) { 58 | /******/ if(mode & 1) value = __webpack_require__(value); 59 | /******/ if(mode & 8) return value; 60 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 61 | /******/ var ns = Object.create(null); 62 | /******/ __webpack_require__.r(ns); 63 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 64 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 65 | /******/ return ns; 66 | /******/ }; 67 | /******/ 68 | /******/ // getDefaultExport function for compatibility with non-harmony modules 69 | /******/ __webpack_require__.n = function(module) { 70 | /******/ var getter = module && module.__esModule ? 71 | /******/ function getDefault() { return module['default']; } : 72 | /******/ function getModuleExports() { return module; }; 73 | /******/ __webpack_require__.d(getter, 'a', getter); 74 | /******/ return getter; 75 | /******/ }; 76 | /******/ 77 | /******/ // Object.prototype.hasOwnProperty.call 78 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 79 | /******/ 80 | /******/ // __webpack_public_path__ 81 | /******/ __webpack_require__.p = ""; 82 | /******/ 83 | /******/ 84 | /******/ // Load entry module and return exports 85 | /******/ return __webpack_require__(__webpack_require__.s = 5); 86 | /******/ }) 87 | /************************************************************************/ 88 | /******/ ([ 89 | /* 0 */ 90 | /***/ (function(module, exports) { 91 | 92 | module.exports = require("graphql"); 93 | 94 | /***/ }), 95 | /* 1 */ 96 | /***/ (function(module, exports) { 97 | 98 | module.exports = require("aws-sdk"); 99 | 100 | /***/ }), 101 | /* 2 */ 102 | /***/ (function(module, exports) { 103 | 104 | module.exports = require("graphql-iso-date"); 105 | 106 | /***/ }), 107 | /* 3 */ 108 | /***/ (function(module, exports) { 109 | 110 | module.exports = require("uuid/v4"); 111 | 112 | /***/ }), 113 | /* 4 */ 114 | /***/ (function(module, exports) { 115 | 116 | module.exports = require("@elastic/elasticsearch"); 117 | 118 | /***/ }), 119 | /* 5 */ 120 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 121 | 122 | "use strict"; 123 | __webpack_require__.r(__webpack_exports__); 124 | 125 | // EXTERNAL MODULE: external "graphql" 126 | var external_graphql_ = __webpack_require__(0); 127 | 128 | // EXTERNAL MODULE: external "graphql-iso-date" 129 | var external_graphql_iso_date_ = __webpack_require__(2); 130 | 131 | // EXTERNAL MODULE: external "aws-sdk" 132 | var external_aws_sdk_ = __webpack_require__(1); 133 | 134 | // CONCATENATED MODULE: ./dynamodb/index.js 135 | 136 | 137 | let dynamoDbClient = new external_aws_sdk_["DynamoDB"].DocumentClient(); 138 | if (process.env.IS_OFFLINE) { 139 | dynamoDbClient = new external_aws_sdk_["DynamoDB"].DocumentClient({ 140 | endpoint: "http://localhost:8000", 141 | region: "localhost", 142 | }); 143 | } 144 | 145 | 146 | 147 | // EXTERNAL MODULE: external "uuid/v4" 148 | var v4_ = __webpack_require__(3); 149 | 150 | // CONCATENATED MODULE: ./resolvers/events/create.ts 151 | 152 | 153 | function createParams(data, TableName, uniqueID) { 154 | return { 155 | Item: { 156 | name: data.name, 157 | description: data.description, 158 | id: uniqueID, 159 | addedAt: Date.now() 160 | }, 161 | TableName 162 | }; 163 | } 164 | /* harmony default export */ var create = (data => { 165 | const putParams = createParams(data, process.env.TABLE_NAME, v4_()); 166 | return dynamoDbClient.put(putParams).promise().then(() => { 167 | return putParams.Item; 168 | }).catch(err => { 169 | throw err; 170 | }); 171 | }); 172 | // CONCATENATED MODULE: ./resolvers/events/view.ts 173 | 174 | /* harmony default export */ var view = (async id => { 175 | const params = { 176 | TableName: process.env.TABLE_NAME, 177 | Key: { 178 | id 179 | } 180 | }; 181 | const GetEvents = await dynamoDbClient.get(params).promise(); 182 | return GetEvents.Item; 183 | }); 184 | // CONCATENATED MODULE: ./resolvers/events/list.ts 185 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; } 186 | 187 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 188 | 189 | 190 | /* harmony default export */ var list = (() => dynamoDbClient.scan({ 191 | TableName: process.env.TABLE_NAME 192 | }).promise().then(list => list.Items.map(Item => { 193 | return _objectSpread({}, Item, { 194 | addedAt: new Date(Item.addedAt) 195 | }); 196 | }))); 197 | // CONCATENATED MODULE: ./resolvers/events/remove.ts 198 | 199 | /* harmony default export */ var remove = (async id => { 200 | const params = { 201 | TableName: process.env.TABLE_NAME, 202 | Key: { 203 | id 204 | }, 205 | ReturnValues: "ALL_OLD" 206 | }; 207 | 208 | try { 209 | const response = await dynamoDbClient.delete(params).promise(); 210 | return response.Attributes; 211 | } catch (error) { 212 | throw error; 213 | } 214 | }); 215 | // CONCATENATED MODULE: ./schemas/index.ts 216 | /*import { 217 | paginationToParams, 218 | dataToConnection 219 | } from "graphql-dynamodb-connections"; 220 | */ 221 | 222 | 223 | 224 | 225 | 226 | 227 | const eventType = new external_graphql_["GraphQLObjectType"]({ 228 | name: "Event", 229 | fields: { 230 | id: { 231 | type: new external_graphql_["GraphQLNonNull"](external_graphql_["GraphQLString"]) 232 | }, 233 | name: { 234 | type: new external_graphql_["GraphQLNonNull"](external_graphql_["GraphQLString"]) 235 | }, 236 | description: { 237 | type: new external_graphql_["GraphQLNonNull"](external_graphql_["GraphQLString"]) 238 | }, 239 | addedAt: { 240 | type: new external_graphql_["GraphQLNonNull"](external_graphql_iso_date_["GraphQLDateTime"]) 241 | } 242 | } 243 | }); 244 | const schema = new external_graphql_["GraphQLSchema"]({ 245 | query: new external_graphql_["GraphQLObjectType"]({ 246 | name: "Query", 247 | fields: { 248 | listEvents: { 249 | type: new external_graphql_["GraphQLList"](eventType), 250 | resolve: parent => { 251 | return list(); 252 | } 253 | }, 254 | viewEvent: { 255 | args: { 256 | id: { 257 | type: new external_graphql_["GraphQLNonNull"](external_graphql_["GraphQLString"]) 258 | } 259 | }, 260 | type: eventType, 261 | resolve: (parent, args) => { 262 | return view(args.id); 263 | } 264 | } 265 | } 266 | }), 267 | mutation: new external_graphql_["GraphQLObjectType"]({ 268 | name: "Mutation", 269 | fields: { 270 | createEvent: { 271 | args: { 272 | name: { 273 | type: new external_graphql_["GraphQLNonNull"](external_graphql_["GraphQLString"]) 274 | }, 275 | description: { 276 | type: new external_graphql_["GraphQLNonNull"](external_graphql_["GraphQLString"]) 277 | } 278 | }, 279 | type: eventType, 280 | resolve: (parent, args) => { 281 | return create(args); 282 | } 283 | }, 284 | removeEvent: { 285 | args: { 286 | id: { 287 | type: new external_graphql_["GraphQLNonNull"](external_graphql_["GraphQLString"]) 288 | } 289 | }, 290 | type: eventType, 291 | resolve: (parent, args) => { 292 | return remove(args.id); 293 | } 294 | } 295 | } 296 | }) 297 | }); 298 | /* harmony default export */ var schemas = (schema); 299 | // EXTERNAL MODULE: external "@elastic/elasticsearch" 300 | var elasticsearch_ = __webpack_require__(4); 301 | 302 | // CONCATENATED MODULE: ./elasticSearch/elasticSearchConnect.ts 303 | 304 | const esClient = new elasticsearch_["Client"]({ 305 | node: `https://${process.env.ELASTICSEARCH_URL}` 306 | }); 307 | // CONCATENATED MODULE: ./elasticSearch/mappings.ts 308 | function getESMappings(index) { 309 | return { 310 | index, 311 | body: { 312 | properties: { 313 | id: { 314 | type: "text" 315 | }, 316 | description: { 317 | type: "text" 318 | }, 319 | name: { 320 | type: "text" 321 | }, 322 | addedAt: { 323 | type: "number" 324 | } 325 | } 326 | } 327 | }; 328 | } 329 | // CONCATENATED MODULE: ./streams/utils/index.ts 330 | function utils_objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { utils_defineProperty(target, key, source[key]); }); } return target; } 331 | 332 | function utils_defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 333 | 334 | function transformData(newImage) { 335 | const transformedObject = {}; 336 | Object.keys(newImage).forEach(key => { 337 | const dataType = Object.keys(newImage[key])[0]; 338 | transformedObject[key] = newImage[key][dataType]; 339 | }); 340 | return transformedObject; 341 | } 342 | 343 | function extractRecordsFromDynamodbEvent(event) { 344 | if (!event.Records || !Array.isArray(event.Records) || event.Records.length <= 0) { 345 | return null; 346 | } 347 | 348 | return event.Records.reduce((acculator, current) => { 349 | const ACTION = current.eventName; 350 | const existingRecords = acculator[ACTION] || []; 351 | const existsDynamoDb = current.dynamodb && current.dynamodb.NewImage; 352 | 353 | if (existsDynamoDb) { 354 | return utils_objectSpread({}, acculator, { 355 | [ACTION]: [...existingRecords, transformData(current.dynamodb.NewImage)] 356 | }); 357 | } 358 | }, {}); 359 | } 360 | const actions = { 361 | INSERT: "INSERT", 362 | UPDATE: "UPDATE" 363 | }; 364 | // CONCATENATED MODULE: ./elasticSearch/config.ts 365 | const config = { 366 | INDEX: "defaultevents", 367 | TYPE: "bookingevent" 368 | }; 369 | // CONCATENATED MODULE: ./streams/process.ts 370 | 371 | 372 | 373 | 374 | async function indexElasticSearch(event) { 375 | try { 376 | // check if indices already exist 377 | const exists = await esClient.indices.exists({ 378 | index: config.INDEX 379 | }); 380 | 381 | if (!exists) { 382 | // if not create new index and mappings for it 383 | await esClient.indices.create(getESMappings(config.INDEX)); 384 | } // extract data 385 | 386 | 387 | const dataArray = extractRecordsFromDynamodbEvent(event)[actions.INSERT] || []; // default to empty 388 | 389 | await Promise.all(dataArray.map(async data => { 390 | await esClient.index({ 391 | id: data.id, 392 | index: config.INDEX, 393 | body: data 394 | }); 395 | })); 396 | } catch (err) { 397 | throw err; 398 | } 399 | } 400 | // CONCATENATED MODULE: ./handler.ts 401 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "queryEvents", function() { return queryEvents; }); 402 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "processStreams", function() { return processStreams; }); 403 | 404 | 405 | // Highly scalable FaaS architecture :) 406 | // Export a function which would be hooked up to the the λ node/ nodes as specified on serverless.yml template 407 | 408 | async function queryEvents(event) // context: Context, 409 | { 410 | const parsedRequestBody = event && event.body ? JSON.parse(event.body) : {}; 411 | 412 | try { 413 | const graphQLResult = await Object(external_graphql_["graphql"])(schemas, parsedRequestBody.query, null, null, parsedRequestBody.variables, parsedRequestBody.operationName); 414 | return { 415 | statusCode: 200, 416 | body: JSON.stringify(graphQLResult) 417 | }; 418 | } catch (error) { 419 | throw error; 420 | } 421 | } 422 | async function processStreams(event) { 423 | await indexElasticSearch(event); 424 | } 425 | 426 | /***/ }) 427 | /******/ ]); 428 | //# sourceMappingURL=handler.js.map -------------------------------------------------------------------------------- /.webpack/service/handler.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"handler.js","sources":["webpack:///webpack/bootstrap","webpack:///external \"graphql\"","webpack:///external \"aws-sdk\"","webpack:///external \"graphql-iso-date\"","webpack:///external \"uuid/v4\"","webpack:///external \"@elastic/elasticsearch\"","webpack:///./dynamodb/index.js","webpack:///./resolvers/events/create.ts","webpack:///./resolvers/events/view.ts","webpack:///./resolvers/events/list.ts","webpack:///./resolvers/events/remove.ts","webpack:///./schemas/index.ts","webpack:///./elasticSearch/elasticSearchConnect.ts","webpack:///./elasticSearch/mappings.ts","webpack:///./streams/utils/index.ts","webpack:///./elasticSearch/config.ts","webpack:///./streams/process.ts","webpack:///./handler.ts"],"sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 5);\n","module.exports = require(\"graphql\");","module.exports = require(\"aws-sdk\");","module.exports = require(\"graphql-iso-date\");","module.exports = require(\"uuid/v4\");","module.exports = require(\"@elastic/elasticsearch\");","import { DynamoDB } from \"aws-sdk\";\r\n\r\nlet dynamoDbClient = new DynamoDB.DocumentClient();\r\nif (process.env.IS_OFFLINE) {\r\n dynamoDbClient = new DynamoDB.DocumentClient({\r\n endpoint: \"http://localhost:8000\",\r\n region: \"localhost\",\r\n });\r\n}\r\n\r\nexport { dynamoDbClient };\r\n","import { IEvent } from \"./typings\";\r\nimport { dynamoDbClient } from \"../../dynamodb\";\r\nimport * as uuidv4 from \"uuid/v4\";\r\n\r\nexport function createParams(data: IEvent, TableName: string , uniqueID: string) {\r\n return {\r\n Item: {\r\n name: data.name,\r\n description: data.description,\r\n id: uniqueID,\r\n addedAt: Date.now(),\r\n },\r\n TableName,\r\n };\r\n}\r\n\r\nexport default (data: IEvent) => {\r\n const putParams = createParams(data, process.env.TABLE_NAME, uuidv4());\r\n return dynamoDbClient\r\n .put(putParams)\r\n .promise()\r\n .then(() => {\r\n return putParams.Item;\r\n }).catch((err) => {\r\n throw err;\r\n });\r\n};\r\n","import { dynamoDbClient } from \"../../dynamodb\";\r\n\r\nexport default async (id: string) => {\r\n const params = {\r\n TableName: process.env.TABLE_NAME,\r\n Key: { id },\r\n };\r\n const GetEvents = await dynamoDbClient.get(params).promise();\r\n return GetEvents.Item;\r\n};\r\n","import { DynamoDB } from \"aws-sdk\";\r\nimport { dynamoDbClient } from \"../../dynamodb\";\r\nexport default () =>\r\ndynamoDbClient\r\n .scan({ TableName: process.env.TABLE_NAME })\r\n .promise()\r\n .then((list: DynamoDB.DocumentClient.ScanOutput) => list.Items.map(\r\n (Item) => {\r\n return ({ ...Item, addedAt: new Date(Item.addedAt) });\r\n }));\r\n","import { dynamoDbClient } from \"../../dynamodb\";\r\n\r\nexport default async (id: string) => {\r\n const params = {\r\n TableName: process.env.TABLE_NAME,\r\n Key: { id },\r\n ReturnValues: \"ALL_OLD\",\r\n };\r\n try {\r\n const response = await dynamoDbClient.delete(params).promise();\r\n return response.Attributes;\r\n } catch (error) {\r\n throw error;\r\n }\r\n};\r\n","/*import {\r\n paginationToParams,\r\n dataToConnection\r\n} from \"graphql-dynamodb-connections\";\r\n*/\r\nimport {\r\n GraphQLSchema,\r\n GraphQLObjectType,\r\n GraphQLString,\r\n GraphQLList,\r\n GraphQLNonNull,\r\n GraphQLBoolean,\r\n} from \"graphql\";\r\n\r\nimport {\r\n /*GraphQLDate,\r\n GraphQLTime,*/\r\n GraphQLDateTime,\r\n} from \"graphql-iso-date\";\r\n\r\nimport { IEvent } from \"../resolvers/events/typings\";\r\nimport addEvent from \"../resolvers/events/create\";\r\nimport viewEvent from \"../resolvers/events/view\";\r\nimport listEvents from \"../resolvers/events/list\";\r\nimport removeEvent from \"../resolvers/events/remove\";\r\n\r\nconst eventType = new GraphQLObjectType({\r\n name: \"Event\",\r\n fields: {\r\n id: { type: new GraphQLNonNull(GraphQLString) },\r\n name: { type: new GraphQLNonNull(GraphQLString) },\r\n description: { type: new GraphQLNonNull(GraphQLString) },\r\n addedAt: { type: new GraphQLNonNull(GraphQLDateTime) },\r\n },\r\n});\r\n\r\nconst schema = new GraphQLSchema({\r\n query: new GraphQLObjectType({\r\n name: \"Query\",\r\n fields: {\r\n listEvents: {\r\n type: new GraphQLList(eventType),\r\n resolve: (parent ) => {\r\n return listEvents();\r\n },\r\n },\r\n viewEvent: {\r\n args: {\r\n id: { type: new GraphQLNonNull(GraphQLString) },\r\n },\r\n type: eventType,\r\n resolve: (parent, args: { id: string }) => {\r\n return viewEvent(args.id);\r\n },\r\n },\r\n },\r\n }),\r\n\r\n mutation: new GraphQLObjectType({\r\n name: \"Mutation\",\r\n fields: {\r\n createEvent: {\r\n args: {\r\n name: { type: new GraphQLNonNull(GraphQLString) },\r\n description: { type: new GraphQLNonNull(GraphQLString) },\r\n },\r\n type: eventType,\r\n resolve: (parent, args: IEvent) => {\r\n return addEvent(args);\r\n },\r\n },\r\n removeEvent: {\r\n args: {\r\n id: { type: new GraphQLNonNull(GraphQLString) },\r\n },\r\n type: eventType,\r\n resolve: (parent, args: { id: string }) => {\r\n return removeEvent(args.id);\r\n },\r\n },\r\n },\r\n }),\r\n});\r\nexport default schema;\r\n","import { Client } from \"@elastic/elasticsearch\";\r\n\r\nexport const esClient = new Client({ node: `https://${process.env.ELASTICSEARCH_URL}` });\r\n","import { RequestParams } from \"@elastic/elasticsearch\";\r\n\r\nexport function getESMappings(index: string): RequestParams.IndicesCreate {\r\n return {\r\n index,\r\n body: {\r\n properties: {\r\n id: { type: \"text\" },\r\n description: { type: \"text\" },\r\n name: { type: \"text\" },\r\n addedAt: { type: \"number\" },\r\n },\r\n },\r\n };\r\n}\r\n","\r\nfunction transformData(newImage) {\r\n const transformedObject = {};\r\n Object.keys(newImage).forEach((key: string) => {\r\n const dataType = Object.keys(newImage[key])[0];\r\n transformedObject[key] = newImage[key][dataType];\r\n });\r\n return transformedObject;\r\n}\r\n\r\nexport function extractRecordsFromDynamodbEvent(event) {\r\n if (!event.Records || !Array.isArray(event.Records) || event.Records.length <= 0) {\r\n return null;\r\n }\r\n\r\n return event.Records.reduce((acculator, current) => {\r\n const ACTION: \"INSERT\" | \"UPDATE\" = current.eventName;\r\n const existingRecords = acculator[ACTION] || [];\r\n const existsDynamoDb = current.dynamodb && current.dynamodb.NewImage;\r\n if (existsDynamoDb) {\r\n return { ...acculator, [ACTION]: [...existingRecords, transformData(current.dynamodb.NewImage)] };\r\n }\r\n }, {});\r\n}\r\n\r\nexport const actions = {\r\n INSERT: \"INSERT\",\r\n UPDATE: \"UPDATE\",\r\n};\r\n","export const config = {\r\n INDEX: \"defaultevents\",\r\n TYPE: \"bookingevent\",\r\n};\r\n","import { esClient } from \"../elasticSearch/elasticSearchConnect\";\r\nimport { getESMappings } from \"../elasticSearch/mappings\";\r\nimport { extractRecordsFromDynamodbEvent, actions } from \"./utils\";\r\nimport { config } from \"../elasticSearch/config\";\r\n\r\nexport async function indexElasticSearch(event) {\r\n try {\r\n // check if indices already exist\r\n const exists = await esClient.indices.exists({ index: config.INDEX });\r\n if (!exists) {\r\n // if not create new index and mappings for it\r\n await esClient.indices.create(getESMappings(config.INDEX));\r\n }\r\n // extract data\r\n\r\n const dataArray = extractRecordsFromDynamodbEvent(event)[actions.INSERT] || []; // default to empty\r\n\r\n await Promise.all(\r\n dataArray.map(async (data) => {\r\n await esClient.index({\r\n id: data.id,\r\n index: config.INDEX,\r\n body: data,\r\n });\r\n }),\r\n );\r\n } catch (err) {\r\n throw err;\r\n }\r\n}\r\n","import schema from \"./schemas/index\";\r\nimport { graphql } from \"graphql\";\r\nimport { APIGatewayProxyEvent } from \"aws-lambda\";\r\nimport { indexElasticSearch } from \"./streams/process\";\r\n// Highly scalable FaaS architecture :)\r\n// Export a function which would be hooked up to the the λ node/ nodes as specified on serverless.yml template\r\nexport async function queryEvents(\r\n event: APIGatewayProxyEvent,\r\n // context: Context,\r\n) {\r\n const parsedRequestBody = event && event.body ? JSON.parse(event.body) : {};\r\n try {\r\n const graphQLResult = await graphql(\r\n schema,\r\n parsedRequestBody.query,\r\n null,\r\n null,\r\n parsedRequestBody.variables,\r\n parsedRequestBody.operationName,\r\n );\r\n\r\n return { statusCode: 200, body: JSON.stringify(graphQLResult) };\r\n } catch (error) {\r\n throw error;\r\n }\r\n}\r\n\r\nexport async function processStreams(event: APIGatewayProxyEvent) {\r\n await indexElasticSearch(event);\r\n}\r\n"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;AClFA;;;;;;ACAA;;;;;;ACAA;;;;;;ACAA;;;;;;ACAA;;;;;;;;;;;;;;;;;;;ACAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;ACTA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAJA;AAMA;AAPA;AASA;AAEA;AACA;AACA;AAIA;AACA;AACA;AACA;AACA;;AC1BA;AAEA;AACA;AACA;AACA;AAAA;AAAA;AAFA;AAIA;AACA;AACA;;;;;;ACRA;AACA;AAEA;AAAA;AAIA;AAAA;AAAA;AACA;;ACTA;AAEA;AACA;AACA;AACA;AAAA;AAAA;AACA;AAHA;AACA;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;;ACdA;;;;;AAKA;AAYA;AAIA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AAJA;AAFA;AAUA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAJA;AAMA;AACA;AACA;AAAA;AAAA;AADA;AAGA;AACA;AACA;AACA;AAPA;AAPA;AAFA;AAqBA;AACA;AACA;AACA;AACA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AAFA;AAIA;AACA;AACA;AACA;AARA;AAUA;AACA;AACA;AAAA;AAAA;AADA;AAGA;AACA;AACA;AACA;AAPA;AAXA;AAFA;AAtBA;AA+CA;;;;;ACnFA;AAEA;AAAA;AAAA;;ACAA;AACA;AACA;AACA;AACA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AAJA;AADA;AAFA;AAWA;;;;;;ACbA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AAAA;AAAA;AACA;AACA;AACA;AAEA;AACA;AACA;AAFA;;ACzBA;AACA;AACA;AAFA;;ACAA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAA;AAAA;AACA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAHA;AAKA;AAEA;AACA;AACA;AACA;;AC7BA;AAAA;AAAA;AACA;AAEA;AAEA;AACA;AAAA;AAGA;AACA;AACA;AAAA;AACA;AASA;AAAA;AAAA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;;;A","sourceRoot":""} -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # ${project-name} Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [0.0.0] - 2018-12-12 8 | 9 | ### Added 10 | - added changelog in hopes of maintianing it properly. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Dasith Kuruppu 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /__tests__/createEvent.test.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | /******/ (function(modules) { // webpackBootstrap 3 | /******/ // The module cache 4 | /******/ var installedModules = {}; 5 | /******/ 6 | /******/ // The require function 7 | /******/ function __webpack_require__(moduleId) { 8 | /******/ 9 | /******/ // Check if module is in cache 10 | /******/ if(installedModules[moduleId]) { 11 | /******/ return installedModules[moduleId].exports; 12 | /******/ } 13 | /******/ // Create a new module (and put it into the cache) 14 | /******/ var module = installedModules[moduleId] = { 15 | /******/ i: moduleId, 16 | /******/ l: false, 17 | /******/ exports: {} 18 | /******/ }; 19 | /******/ 20 | /******/ // Execute the module function 21 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 22 | /******/ 23 | /******/ // Flag the module as loaded 24 | /******/ module.l = true; 25 | /******/ 26 | /******/ // Return the exports of the module 27 | /******/ return module.exports; 28 | /******/ } 29 | /******/ 30 | /******/ 31 | /******/ // expose the modules object (__webpack_modules__) 32 | /******/ __webpack_require__.m = modules; 33 | /******/ 34 | /******/ // expose the module cache 35 | /******/ __webpack_require__.c = installedModules; 36 | /******/ 37 | /******/ // define getter function for harmony exports 38 | /******/ __webpack_require__.d = function(exports, name, getter) { 39 | /******/ if(!__webpack_require__.o(exports, name)) { 40 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 41 | /******/ } 42 | /******/ }; 43 | /******/ 44 | /******/ // define __esModule on exports 45 | /******/ __webpack_require__.r = function(exports) { 46 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 47 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 48 | /******/ } 49 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 50 | /******/ }; 51 | /******/ 52 | /******/ // create a fake namespace object 53 | /******/ // mode & 1: value is a module id, require it 54 | /******/ // mode & 2: merge all properties of value into the ns 55 | /******/ // mode & 4: return value when already ns object 56 | /******/ // mode & 8|1: behave like require 57 | /******/ __webpack_require__.t = function(value, mode) { 58 | /******/ if(mode & 1) value = __webpack_require__(value); 59 | /******/ if(mode & 8) return value; 60 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 61 | /******/ var ns = Object.create(null); 62 | /******/ __webpack_require__.r(ns); 63 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 64 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 65 | /******/ return ns; 66 | /******/ }; 67 | /******/ 68 | /******/ // getDefaultExport function for compatibility with non-harmony modules 69 | /******/ __webpack_require__.n = function(module) { 70 | /******/ var getter = module && module.__esModule ? 71 | /******/ function getDefault() { return module['default']; } : 72 | /******/ function getModuleExports() { return module; }; 73 | /******/ __webpack_require__.d(getter, 'a', getter); 74 | /******/ return getter; 75 | /******/ }; 76 | /******/ 77 | /******/ // Object.prototype.hasOwnProperty.call 78 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 79 | /******/ 80 | /******/ // __webpack_public_path__ 81 | /******/ __webpack_require__.p = ""; 82 | /******/ 83 | /******/ 84 | /******/ // Load entry module and return exports 85 | /******/ return __webpack_require__(__webpack_require__.s = 7); 86 | /******/ }) 87 | /************************************************************************/ 88 | /******/ ([ 89 | /* 0 */, 90 | /* 1 */ 91 | /***/ (function(module, exports) { 92 | 93 | module.exports = require("aws-sdk"); 94 | 95 | /***/ }), 96 | /* 2 */ 97 | /***/ (function(module, exports) { 98 | 99 | module.exports = require("uuid/v4"); 100 | 101 | /***/ }), 102 | /* 3 */ 103 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 104 | 105 | "use strict"; 106 | 107 | // EXTERNAL MODULE: external "aws-sdk" 108 | var external_aws_sdk_ = __webpack_require__(1); 109 | 110 | // CONCATENATED MODULE: ./dynamodb/index.js 111 | 112 | 113 | let dynamoDbClient = new external_aws_sdk_["DynamoDB"].DocumentClient(); 114 | if (process.env.IS_OFFLINE) { 115 | dynamoDbClient = new external_aws_sdk_["DynamoDB"].DocumentClient({ 116 | endpoint: "http://localhost:8000", 117 | region: "localhost", 118 | }); 119 | } 120 | 121 | 122 | 123 | // EXTERNAL MODULE: external "uuid/v4" 124 | var v4_ = __webpack_require__(2); 125 | 126 | // CONCATENATED MODULE: ./resolvers/events/create.ts 127 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return createParams; }); 128 | 129 | 130 | function createParams(data, TableName, uniqueID) { 131 | return { 132 | Item: { 133 | name: data.name, 134 | description: data.description, 135 | id: uniqueID, 136 | addedAt: Date.now() 137 | }, 138 | TableName 139 | }; 140 | } 141 | /* harmony default export */ var create = __webpack_exports__["b"] = (data => { 142 | const putParams = createParams(data, process.env.TABLE_NAME, v4_()); 143 | return dynamoDbClient.put(putParams).promise().then(() => { 144 | return putParams.Item; 145 | }).catch(err => { 146 | throw err; 147 | }); 148 | }); 149 | 150 | /***/ }), 151 | /* 4 */, 152 | /* 5 */, 153 | /* 6 */, 154 | /* 7 */ 155 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 156 | 157 | "use strict"; 158 | __webpack_require__.r(__webpack_exports__); 159 | /* harmony import */ var _resolvers_events_create__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); 160 | /* harmony import */ var uuid_v4__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2); 161 | /* harmony import */ var uuid_v4__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(uuid_v4__WEBPACK_IMPORTED_MODULE_1__); 162 | 163 | 164 | test("Event object creation", () => { 165 | const createdParams = Object(_resolvers_events_create__WEBPACK_IMPORTED_MODULE_0__[/* createParams */ "a"])({ 166 | id: "1", 167 | name: "Whatever", 168 | description: "unknown", 169 | addedAt: Date.now(), 170 | startingOn: Date.now() 171 | }, process.env.TableName, uuid_v4__WEBPACK_IMPORTED_MODULE_1__()); 172 | expect(createdParams).not.toBe(null); 173 | }); 174 | 175 | /***/ }) 176 | /******/ ]); 177 | //# sourceMappingURL=createEvent.test.js.map -------------------------------------------------------------------------------- /__tests__/createEvent.test.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createEvent.test.js","sources":["webpack:///webpack/bootstrap","webpack:///external \"aws-sdk\"","webpack:///external \"uuid/v4\"","webpack:///./dynamodb/index.js","webpack:///./resolvers/events/create.ts","webpack:///./__tests__/createEvent.test.ts"],"sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 7);\n","module.exports = require(\"aws-sdk\");","module.exports = require(\"uuid/v4\");","import { DynamoDB } from \"aws-sdk\";\r\n\r\nlet dynamoDbClient = new DynamoDB.DocumentClient();\r\nif (process.env.IS_OFFLINE) {\r\n dynamoDbClient = new DynamoDB.DocumentClient({\r\n endpoint: \"http://localhost:8000\",\r\n region: \"localhost\",\r\n });\r\n}\r\n\r\nexport { dynamoDbClient };\r\n","import { IEvent } from \"./typings\";\r\nimport { dynamoDbClient } from \"../../dynamodb\";\r\nimport * as uuidv4 from \"uuid/v4\";\r\n\r\nexport function createParams(data: IEvent, TableName: string , uniqueID: string) {\r\n return {\r\n Item: {\r\n name: data.name,\r\n description: data.description,\r\n id: uniqueID,\r\n addedAt: Date.now(),\r\n },\r\n TableName,\r\n };\r\n}\r\n\r\nexport default (data: IEvent) => {\r\n const putParams = createParams(data, process.env.TABLE_NAME, uuidv4());\r\n return dynamoDbClient\r\n .put(putParams)\r\n .promise()\r\n .then(() => {\r\n return putParams.Item;\r\n }).catch((err) => {\r\n throw err;\r\n });\r\n};\r\n","import { createParams } from \"../resolvers/events/create\";\r\nimport * as uuidv4 from \"uuid/v4\";\r\ntest(\"Event object creation\", () => {\r\n const createdParams = createParams(\r\n {\r\n id: \"1\",\r\n name: \"Whatever\",\r\n description: \"unknown\",\r\n addedAt: Date.now(),\r\n startingOn: Date.now(),\r\n },\r\n process.env.TableName,\r\n uuidv4(),\r\n );\r\n expect(createdParams).not.toBe(null);\r\n});\r\n"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;AClFA;;;;;;ACAA;;;;;;;;;;;;ACAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;ACTA;AAAA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAJA;AAMA;AAPA;AASA;AAEA;AACA;AACA;AAIA;AACA;AACA;AACA;AACA;;;;;;;;;;AC1BA;AAAA;AAAA;AAAA;AAAA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AALA;AAUA;AACA;;;A","sourceRoot":""} -------------------------------------------------------------------------------- /__tests__/createEvent.test.ts: -------------------------------------------------------------------------------- 1 | import { createParams } from "../resolvers/events/create"; 2 | import * as uuidv4 from "uuid/v4"; 3 | test("Event object creation", () => { 4 | const createdParams = createParams( 5 | { 6 | id: "1", 7 | name: "Whatever", 8 | description: "unknown", 9 | addedAt: Date.now(), 10 | startingOn: Date.now(), 11 | }, 12 | process.env.TableName, 13 | uuidv4(), 14 | ); 15 | expect(createdParams).not.toBe(null); 16 | }); 17 | -------------------------------------------------------------------------------- /__tests__/queryEvent.test.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | /******/ (function(modules) { // webpackBootstrap 3 | /******/ // The module cache 4 | /******/ var installedModules = {}; 5 | /******/ 6 | /******/ // The require function 7 | /******/ function __webpack_require__(moduleId) { 8 | /******/ 9 | /******/ // Check if module is in cache 10 | /******/ if(installedModules[moduleId]) { 11 | /******/ return installedModules[moduleId].exports; 12 | /******/ } 13 | /******/ // Create a new module (and put it into the cache) 14 | /******/ var module = installedModules[moduleId] = { 15 | /******/ i: moduleId, 16 | /******/ l: false, 17 | /******/ exports: {} 18 | /******/ }; 19 | /******/ 20 | /******/ // Execute the module function 21 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 22 | /******/ 23 | /******/ // Flag the module as loaded 24 | /******/ module.l = true; 25 | /******/ 26 | /******/ // Return the exports of the module 27 | /******/ return module.exports; 28 | /******/ } 29 | /******/ 30 | /******/ 31 | /******/ // expose the modules object (__webpack_modules__) 32 | /******/ __webpack_require__.m = modules; 33 | /******/ 34 | /******/ // expose the module cache 35 | /******/ __webpack_require__.c = installedModules; 36 | /******/ 37 | /******/ // define getter function for harmony exports 38 | /******/ __webpack_require__.d = function(exports, name, getter) { 39 | /******/ if(!__webpack_require__.o(exports, name)) { 40 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 41 | /******/ } 42 | /******/ }; 43 | /******/ 44 | /******/ // define __esModule on exports 45 | /******/ __webpack_require__.r = function(exports) { 46 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 47 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 48 | /******/ } 49 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 50 | /******/ }; 51 | /******/ 52 | /******/ // create a fake namespace object 53 | /******/ // mode & 1: value is a module id, require it 54 | /******/ // mode & 2: merge all properties of value into the ns 55 | /******/ // mode & 4: return value when already ns object 56 | /******/ // mode & 8|1: behave like require 57 | /******/ __webpack_require__.t = function(value, mode) { 58 | /******/ if(mode & 1) value = __webpack_require__(value); 59 | /******/ if(mode & 8) return value; 60 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 61 | /******/ var ns = Object.create(null); 62 | /******/ __webpack_require__.r(ns); 63 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 64 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 65 | /******/ return ns; 66 | /******/ }; 67 | /******/ 68 | /******/ // getDefaultExport function for compatibility with non-harmony modules 69 | /******/ __webpack_require__.n = function(module) { 70 | /******/ var getter = module && module.__esModule ? 71 | /******/ function getDefault() { return module['default']; } : 72 | /******/ function getModuleExports() { return module; }; 73 | /******/ __webpack_require__.d(getter, 'a', getter); 74 | /******/ return getter; 75 | /******/ }; 76 | /******/ 77 | /******/ // Object.prototype.hasOwnProperty.call 78 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 79 | /******/ 80 | /******/ // __webpack_public_path__ 81 | /******/ __webpack_require__.p = ""; 82 | /******/ 83 | /******/ 84 | /******/ // Load entry module and return exports 85 | /******/ return __webpack_require__(__webpack_require__.s = 8); 86 | /******/ }) 87 | /************************************************************************/ 88 | /******/ ([ 89 | /* 0 */ 90 | /***/ (function(module, exports) { 91 | 92 | module.exports = require("graphql"); 93 | 94 | /***/ }), 95 | /* 1 */ 96 | /***/ (function(module, exports) { 97 | 98 | module.exports = require("aws-sdk"); 99 | 100 | /***/ }), 101 | /* 2 */ 102 | /***/ (function(module, exports) { 103 | 104 | module.exports = require("uuid/v4"); 105 | 106 | /***/ }), 107 | /* 3 */ 108 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 109 | 110 | "use strict"; 111 | 112 | // EXTERNAL MODULE: external "aws-sdk" 113 | var external_aws_sdk_ = __webpack_require__(1); 114 | 115 | // CONCATENATED MODULE: ./dynamodb/index.js 116 | 117 | 118 | let dynamoDbClient = new external_aws_sdk_["DynamoDB"].DocumentClient(); 119 | if (process.env.IS_OFFLINE) { 120 | dynamoDbClient = new external_aws_sdk_["DynamoDB"].DocumentClient({ 121 | endpoint: "http://localhost:8000", 122 | region: "localhost", 123 | }); 124 | } 125 | 126 | 127 | 128 | // EXTERNAL MODULE: external "uuid/v4" 129 | var v4_ = __webpack_require__(2); 130 | 131 | // CONCATENATED MODULE: ./resolvers/events/create.ts 132 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return createParams; }); 133 | 134 | 135 | function createParams(data, TableName, uniqueID) { 136 | return { 137 | Item: { 138 | name: data.name, 139 | description: data.description, 140 | id: uniqueID, 141 | addedAt: Date.now() 142 | }, 143 | TableName 144 | }; 145 | } 146 | /* harmony default export */ var create = __webpack_exports__["b"] = (data => { 147 | const putParams = createParams(data, process.env.TABLE_NAME, v4_()); 148 | return dynamoDbClient.put(putParams).promise().then(() => { 149 | return putParams.Item; 150 | }).catch(err => { 151 | throw err; 152 | }); 153 | }); 154 | 155 | /***/ }), 156 | /* 4 */ 157 | /***/ (function(module, exports) { 158 | 159 | module.exports = require("graphql-iso-date"); 160 | 161 | /***/ }), 162 | /* 5 */ 163 | /***/ (function(module, exports) { 164 | 165 | module.exports = require("@elastic/elasticsearch"); 166 | 167 | /***/ }), 168 | /* 6 */ 169 | /***/ (function(module, exports) { 170 | 171 | module.exports = require("serverless-jest-plugin"); 172 | 173 | /***/ }), 174 | /* 7 */, 175 | /* 8 */ 176 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 177 | 178 | "use strict"; 179 | __webpack_require__.r(__webpack_exports__); 180 | var handler_namespaceObject = {}; 181 | __webpack_require__.r(handler_namespaceObject); 182 | __webpack_require__.d(handler_namespaceObject, "queryEvents", function() { return queryEvents; }); 183 | __webpack_require__.d(handler_namespaceObject, "processStreams", function() { return processStreams; }); 184 | 185 | // EXTERNAL MODULE: external "graphql" 186 | var external_graphql_ = __webpack_require__(0); 187 | 188 | // EXTERNAL MODULE: external "graphql-iso-date" 189 | var external_graphql_iso_date_ = __webpack_require__(4); 190 | 191 | // EXTERNAL MODULE: ./resolvers/events/create.ts + 1 modules 192 | var create = __webpack_require__(3); 193 | 194 | // EXTERNAL MODULE: external "aws-sdk" 195 | var external_aws_sdk_ = __webpack_require__(1); 196 | 197 | // CONCATENATED MODULE: ./resolvers/events/view.ts 198 | 199 | const dynamoDb = new external_aws_sdk_["DynamoDB"].DocumentClient(); 200 | /* harmony default export */ var view = (async id => { 201 | const params = { 202 | TableName: process.env.TABLE_NAME, 203 | Key: { 204 | id 205 | } 206 | }; 207 | const GetEvents = await dynamoDb.get(params).promise(); 208 | return GetEvents.Item; 209 | }); 210 | // CONCATENATED MODULE: ./resolvers/events/list.ts 211 | 212 | const list_dynamoDb = new external_aws_sdk_["DynamoDB"].DocumentClient(); 213 | /* harmony default export */ var list = (() => list_dynamoDb.scan({ 214 | TableName: process.env.TABLE_NAME 215 | }).promise().then(list => list.Items.map(Item => { 216 | return { ...Item, 217 | addedAt: new Date(Item.addedAt) 218 | }; 219 | }))); 220 | // CONCATENATED MODULE: ./resolvers/events/remove.ts 221 | 222 | const remove_dynamoDb = new external_aws_sdk_["DynamoDB"].DocumentClient(); 223 | /* harmony default export */ var remove = (async id => { 224 | const params = { 225 | TableName: process.env.TABLE_NAME, 226 | Key: { 227 | id 228 | }, 229 | ReturnValues: "ALL_OLD" 230 | }; 231 | 232 | try { 233 | const response = await remove_dynamoDb.delete(params).promise(); 234 | return response.Attributes; 235 | } catch (error) { 236 | throw error; 237 | } 238 | }); 239 | // CONCATENATED MODULE: ./schemas/index.ts 240 | /*import { 241 | paginationToParams, 242 | dataToConnection 243 | } from "graphql-dynamodb-connections"; 244 | */ 245 | 246 | 247 | 248 | 249 | 250 | 251 | const eventType = new external_graphql_["GraphQLObjectType"]({ 252 | name: "Event", 253 | fields: { 254 | id: { 255 | type: new external_graphql_["GraphQLNonNull"](external_graphql_["GraphQLString"]) 256 | }, 257 | name: { 258 | type: new external_graphql_["GraphQLNonNull"](external_graphql_["GraphQLString"]) 259 | }, 260 | description: { 261 | type: new external_graphql_["GraphQLNonNull"](external_graphql_["GraphQLString"]) 262 | }, 263 | addedAt: { 264 | type: new external_graphql_["GraphQLNonNull"](external_graphql_iso_date_["GraphQLDateTime"]) 265 | } 266 | } 267 | }); 268 | const schema = new external_graphql_["GraphQLSchema"]({ 269 | query: new external_graphql_["GraphQLObjectType"]({ 270 | name: "Query", 271 | fields: { 272 | listEvents: { 273 | type: new external_graphql_["GraphQLList"](eventType), 274 | resolve: parent => { 275 | return list(); 276 | } 277 | }, 278 | viewEvent: { 279 | args: { 280 | id: { 281 | type: new external_graphql_["GraphQLNonNull"](external_graphql_["GraphQLString"]) 282 | } 283 | }, 284 | type: eventType, 285 | resolve: (parent, args) => { 286 | return view(args.id); 287 | } 288 | } 289 | } 290 | }), 291 | mutation: new external_graphql_["GraphQLObjectType"]({ 292 | name: "Mutation", 293 | fields: { 294 | createEvent: { 295 | args: { 296 | name: { 297 | type: new external_graphql_["GraphQLNonNull"](external_graphql_["GraphQLString"]) 298 | }, 299 | description: { 300 | type: new external_graphql_["GraphQLNonNull"](external_graphql_["GraphQLString"]) 301 | } 302 | }, 303 | type: eventType, 304 | resolve: (parent, args) => { 305 | return Object(create["b" /* default */])(args); 306 | } 307 | }, 308 | removeEvent: { 309 | args: { 310 | id: { 311 | type: new external_graphql_["GraphQLNonNull"](external_graphql_["GraphQLString"]) 312 | } 313 | }, 314 | type: eventType, 315 | resolve: (parent, args) => { 316 | return remove(args.id); 317 | } 318 | } 319 | } 320 | }) 321 | }); 322 | /* harmony default export */ var schemas = (schema); 323 | // EXTERNAL MODULE: external "@elastic/elasticsearch" 324 | var elasticsearch_ = __webpack_require__(5); 325 | 326 | // CONCATENATED MODULE: ./elasticSearch/elasticSearchConnect.ts 327 | 328 | const esClient = new elasticsearch_["Client"]({ 329 | node: `https://${process.env.ELASTICSEARCH_URL}` 330 | }); 331 | // CONCATENATED MODULE: ./elasticSearch/mappings.ts 332 | function getESMappings(index) { 333 | return { 334 | index, 335 | body: { 336 | properties: { 337 | id: { 338 | type: "text" 339 | }, 340 | description: { 341 | type: "text" 342 | }, 343 | name: { 344 | type: "text" 345 | }, 346 | addedAt: { 347 | type: "number" 348 | } 349 | } 350 | } 351 | }; 352 | } 353 | // CONCATENATED MODULE: ./streams/utils/index.ts 354 | function transformData(newImage) { 355 | const transformedObject = {}; 356 | Object.keys(newImage).forEach(key => { 357 | const dataType = Object.keys(newImage[key])[0]; 358 | transformedObject[key] = newImage[key][dataType]; 359 | }); 360 | return transformedObject; 361 | } 362 | 363 | function extractRecordsFromDynamodbEvent(event) { 364 | if (!event.Records || !Array.isArray(event.Records) || event.Records.length <= 0) { 365 | return null; 366 | } 367 | 368 | return event.Records.reduce((acculator, current) => { 369 | const ACTION = current.eventName; 370 | const existingRecords = acculator[ACTION] || []; 371 | const existsDynamoDb = current.dynamodb && current.dynamodb.NewImage; 372 | 373 | if (existsDynamoDb) { 374 | return { ...acculator, 375 | [ACTION]: [...existingRecords, transformData(current.dynamodb.NewImage)] 376 | }; 377 | } 378 | }, {}); 379 | } 380 | const actions = { 381 | INSERT: "INSERT", 382 | UPDATE: "UPDATE" 383 | }; 384 | // CONCATENATED MODULE: ./elasticSearch/config.ts 385 | const config = { 386 | INDEX: "defaultevents", 387 | TYPE: "bookingevent" 388 | }; 389 | // CONCATENATED MODULE: ./streams/process.ts 390 | 391 | 392 | 393 | 394 | async function indexElasticSearch(event) { 395 | try { 396 | // check if indices already exist 397 | const exists = await esClient.indices.exists({ 398 | index: config.INDEX 399 | }); 400 | 401 | if (!exists) { 402 | // if not create new index and mappings for it 403 | await esClient.indices.create(getESMappings(config.INDEX)); 404 | } // extract data 405 | 406 | 407 | const dataArray = extractRecordsFromDynamodbEvent(event)[actions.INSERT] || []; // default to empty 408 | 409 | await Promise.all(dataArray.map(async data => { 410 | await esClient.index({ 411 | id: data.id, 412 | index: config.INDEX, 413 | body: data 414 | }); 415 | })); 416 | } catch (err) { 417 | throw err; 418 | } 419 | } 420 | // CONCATENATED MODULE: ./handler.ts 421 | 422 | 423 | // Highly scalable FaaS architecture :) 424 | // Export a function which would be hooked up to the the λ node/ nodes as specified on serverless.yml template 425 | 426 | async function queryEvents(event) // context: Context, 427 | { 428 | const parsedRequestBody = event && event.body ? JSON.parse(event.body) : {}; 429 | 430 | try { 431 | const graphQLResult = await Object(external_graphql_["graphql"])(schemas, parsedRequestBody.query, null, null, parsedRequestBody.variables, parsedRequestBody.operationName); 432 | return { 433 | statusCode: 200, 434 | body: JSON.stringify(graphQLResult) 435 | }; 436 | } catch (error) { 437 | throw error; 438 | } 439 | } 440 | async function processStreams(event) { 441 | await indexElasticSearch(event); 442 | } 443 | // EXTERNAL MODULE: external "serverless-jest-plugin" 444 | var external_serverless_jest_plugin_ = __webpack_require__(6); 445 | 446 | // CONCATENATED MODULE: ./__tests__/queryEvent.test.ts 447 | // tests for queryEvents 448 | // Generated by serverless-jest-plugin 449 | 450 | 451 | const lambdaWrapper = external_serverless_jest_plugin_["lambdaWrapper"]; 452 | const wrapped = lambdaWrapper.wrap(handler_namespaceObject, { 453 | handler: "queryEvents" 454 | }); 455 | describe("λ Function - queryEvents", () => { 456 | beforeAll(done => { 457 | // lambdaWrapper.init(liveFunction); // Run the deployed lambda 458 | done(); 459 | }); 460 | it("Invoke / Initiate function test", () => { 461 | return wrapped.run({}).then(response => { 462 | expect(response).not.toBe(null); 463 | expect(response).toHaveProperty("statusCode"); 464 | expect(response).toHaveProperty("body"); 465 | expect(response.statusCode).toBe(200); 466 | }); 467 | }); 468 | }); 469 | 470 | /***/ }) 471 | /******/ ]); 472 | //# sourceMappingURL=queryEvent.test.js.map -------------------------------------------------------------------------------- /__tests__/queryEvent.test.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"queryEvent.test.js","sources":["webpack:///webpack/bootstrap","webpack:///external \"graphql\"","webpack:///external \"aws-sdk\"","webpack:///external \"uuid/v4\"","webpack:///./dynamodb/index.js","webpack:///./resolvers/events/create.ts","webpack:///external \"graphql-iso-date\"","webpack:///external \"@elastic/elasticsearch\"","webpack:///external \"serverless-jest-plugin\"","webpack:///./resolvers/events/view.ts","webpack:///./resolvers/events/list.ts","webpack:///./resolvers/events/remove.ts","webpack:///./schemas/index.ts","webpack:///./elasticSearch/elasticSearchConnect.ts","webpack:///./elasticSearch/mappings.ts","webpack:///./streams/utils/index.ts","webpack:///./elasticSearch/config.ts","webpack:///./streams/process.ts","webpack:///./handler.ts","webpack:///./__tests__/queryEvent.test.ts"],"sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 8);\n","module.exports = require(\"graphql\");","module.exports = require(\"aws-sdk\");","module.exports = require(\"uuid/v4\");","import { DynamoDB } from \"aws-sdk\";\r\n\r\nlet dynamoDbClient = new DynamoDB.DocumentClient();\r\nif (process.env.IS_OFFLINE) {\r\n dynamoDbClient = new DynamoDB.DocumentClient({\r\n endpoint: \"http://localhost:8000\",\r\n region: \"localhost\",\r\n });\r\n}\r\n\r\nexport { dynamoDbClient };\r\n","import { IEvent } from \"./typings\";\r\nimport { dynamoDbClient } from \"../../dynamodb\";\r\nimport * as uuidv4 from \"uuid/v4\";\r\n\r\nexport function createParams(data: IEvent, TableName: string , uniqueID: string) {\r\n return {\r\n Item: {\r\n name: data.name,\r\n description: data.description,\r\n id: uniqueID,\r\n addedAt: Date.now(),\r\n },\r\n TableName,\r\n };\r\n}\r\n\r\nexport default (data: IEvent) => {\r\n const putParams = createParams(data, process.env.TABLE_NAME, uuidv4());\r\n return dynamoDbClient\r\n .put(putParams)\r\n .promise()\r\n .then(() => {\r\n return putParams.Item;\r\n }).catch((err) => {\r\n throw err;\r\n });\r\n};\r\n","module.exports = require(\"graphql-iso-date\");","module.exports = require(\"@elastic/elasticsearch\");","module.exports = require(\"serverless-jest-plugin\");","import { DynamoDB } from \"aws-sdk\";\r\nconst dynamoDb = new DynamoDB.DocumentClient();\r\n\r\nexport default async (id: string) => {\r\n const params = {\r\n TableName: process.env.TABLE_NAME,\r\n Key: { id },\r\n };\r\n const GetEvents = await dynamoDb\r\n .get(params)\r\n .promise();\r\n return GetEvents.Item;\r\n};\r\n","import { DynamoDB } from \"aws-sdk\";\r\nconst dynamoDb = new DynamoDB.DocumentClient();\r\nexport default () =>\r\n dynamoDb\r\n .scan({ TableName: process.env.TABLE_NAME })\r\n .promise()\r\n .then((list: DynamoDB.DocumentClient.ScanOutput) => list.Items.map(\r\n (Item) => {\r\n return ({ ...Item, addedAt: new Date(Item.addedAt) });\r\n }));\r\n","import { DynamoDB, AWSError } from \"aws-sdk\";\r\nconst dynamoDb = new DynamoDB.DocumentClient();\r\n\r\nexport default async (id: string) => {\r\n const params = {\r\n TableName: process.env.TABLE_NAME,\r\n Key: { id },\r\n ReturnValues: \"ALL_OLD\",\r\n };\r\n try {\r\n const response = await dynamoDb.delete(params).promise();\r\n return response.Attributes;\r\n } catch (error) {\r\n throw error;\r\n }\r\n};\r\n","/*import {\r\n paginationToParams,\r\n dataToConnection\r\n} from \"graphql-dynamodb-connections\";\r\n*/\r\nimport {\r\n GraphQLSchema,\r\n GraphQLObjectType,\r\n GraphQLString,\r\n GraphQLList,\r\n GraphQLNonNull,\r\n GraphQLBoolean,\r\n} from \"graphql\";\r\n\r\nimport {\r\n /*GraphQLDate,\r\n GraphQLTime,*/\r\n GraphQLDateTime,\r\n} from \"graphql-iso-date\";\r\n\r\nimport { IEvent } from \"../resolvers/events/typings\";\r\nimport addEvent from \"../resolvers/events/create\";\r\nimport viewEvent from \"../resolvers/events/view\";\r\nimport listEvents from \"../resolvers/events/list\";\r\nimport removeEvent from \"../resolvers/events/remove\";\r\n\r\nconst eventType = new GraphQLObjectType({\r\n name: \"Event\",\r\n fields: {\r\n id: { type: new GraphQLNonNull(GraphQLString) },\r\n name: { type: new GraphQLNonNull(GraphQLString) },\r\n description: { type: new GraphQLNonNull(GraphQLString) },\r\n addedAt: { type: new GraphQLNonNull(GraphQLDateTime) },\r\n },\r\n});\r\n\r\nconst schema = new GraphQLSchema({\r\n query: new GraphQLObjectType({\r\n name: \"Query\",\r\n fields: {\r\n listEvents: {\r\n type: new GraphQLList(eventType),\r\n resolve: (parent ) => {\r\n return listEvents();\r\n },\r\n },\r\n viewEvent: {\r\n args: {\r\n id: { type: new GraphQLNonNull(GraphQLString) },\r\n },\r\n type: eventType,\r\n resolve: (parent, args: { id: string }) => {\r\n return viewEvent(args.id);\r\n },\r\n },\r\n },\r\n }),\r\n\r\n mutation: new GraphQLObjectType({\r\n name: \"Mutation\",\r\n fields: {\r\n createEvent: {\r\n args: {\r\n name: { type: new GraphQLNonNull(GraphQLString) },\r\n description: { type: new GraphQLNonNull(GraphQLString) },\r\n },\r\n type: eventType,\r\n resolve: (parent, args: IEvent) => {\r\n return addEvent(args);\r\n },\r\n },\r\n removeEvent: {\r\n args: {\r\n id: { type: new GraphQLNonNull(GraphQLString) },\r\n },\r\n type: eventType,\r\n resolve: (parent, args: { id: string }) => {\r\n return removeEvent(args.id);\r\n },\r\n },\r\n },\r\n }),\r\n});\r\nexport default schema;\r\n","import { Client } from \"@elastic/elasticsearch\";\r\n\r\nexport const esClient = new Client({ node: `https://${process.env.ELASTICSEARCH_URL}` });\r\n","import { RequestParams } from \"@elastic/elasticsearch\";\r\n\r\nexport function getESMappings(index: string): RequestParams.IndicesCreate {\r\n return {\r\n index,\r\n body: {\r\n properties: {\r\n id: { type: \"text\" },\r\n description: { type: \"text\" },\r\n name: { type: \"text\" },\r\n addedAt: { type: \"number\" },\r\n },\r\n },\r\n };\r\n}\r\n","\r\nfunction transformData(newImage) {\r\n const transformedObject = {};\r\n Object.keys(newImage).forEach((key: string) => {\r\n const dataType = Object.keys(newImage[key])[0];\r\n transformedObject[key] = newImage[key][dataType];\r\n });\r\n return transformedObject;\r\n}\r\n\r\nexport function extractRecordsFromDynamodbEvent(event) {\r\n if (!event.Records || !Array.isArray(event.Records) || event.Records.length <= 0) {\r\n return null;\r\n }\r\n\r\n return event.Records.reduce((acculator, current) => {\r\n const ACTION: \"INSERT\" | \"UPDATE\" = current.eventName;\r\n const existingRecords = acculator[ACTION] || [];\r\n const existsDynamoDb = current.dynamodb && current.dynamodb.NewImage;\r\n if (existsDynamoDb) {\r\n return { ...acculator, [ACTION]: [...existingRecords, transformData(current.dynamodb.NewImage)] };\r\n }\r\n }, {});\r\n}\r\n\r\nexport const actions = {\r\n INSERT: \"INSERT\",\r\n UPDATE: \"UPDATE\",\r\n};\r\n","export const config = {\r\n INDEX: \"defaultevents\",\r\n TYPE: \"bookingevent\",\r\n};\r\n","import { esClient } from \"../elasticSearch/elasticSearchConnect\";\r\nimport { getESMappings } from \"../elasticSearch/mappings\";\r\nimport { extractRecordsFromDynamodbEvent, actions } from \"./utils\";\r\nimport { config } from \"../elasticSearch/config\";\r\n\r\nexport async function indexElasticSearch(event) {\r\n try {\r\n // check if indices already exist\r\n const exists = await esClient.indices.exists({ index: config.INDEX });\r\n if (!exists) {\r\n // if not create new index and mappings for it\r\n await esClient.indices.create(getESMappings(config.INDEX));\r\n }\r\n // extract data\r\n\r\n const dataArray = extractRecordsFromDynamodbEvent(event)[actions.INSERT] || []; // default to empty\r\n\r\n await Promise.all(\r\n dataArray.map(async (data) => {\r\n await esClient.index({\r\n id: data.id,\r\n index: config.INDEX,\r\n body: data,\r\n });\r\n }),\r\n );\r\n } catch (err) {\r\n throw err;\r\n }\r\n}\r\n","import schema from \"./schemas/index\";\r\nimport { graphql } from \"graphql\";\r\nimport { APIGatewayProxyEvent } from \"aws-lambda\";\r\nimport { indexElasticSearch } from \"./streams/process\";\r\n// Highly scalable FaaS architecture :)\r\n// Export a function which would be hooked up to the the λ node/ nodes as specified on serverless.yml template\r\nexport async function queryEvents(\r\n event: APIGatewayProxyEvent,\r\n // context: Context,\r\n) {\r\n const parsedRequestBody = event && event.body ? JSON.parse(event.body) : {};\r\n try {\r\n const graphQLResult = await graphql(\r\n schema,\r\n parsedRequestBody.query,\r\n null,\r\n null,\r\n parsedRequestBody.variables,\r\n parsedRequestBody.operationName,\r\n );\r\n\r\n return { statusCode: 200, body: JSON.stringify(graphQLResult) };\r\n } catch (error) {\r\n throw error;\r\n }\r\n}\r\n\r\nexport async function processStreams(event: APIGatewayProxyEvent) {\r\n await indexElasticSearch(event);\r\n}\r\n","// tests for queryEvents\r\n// Generated by serverless-jest-plugin\r\n\r\nimport * as mod from \"../handler\";\r\n\r\nimport * as jestPlugin from \"serverless-jest-plugin\";\r\nconst lambdaWrapper = jestPlugin.lambdaWrapper;\r\nconst wrapped = lambdaWrapper.wrap(mod, { handler: \"queryEvents\" });\r\n\r\ndescribe(\"λ Function - queryEvents\", () => {\r\n beforeAll((done) => {\r\n // lambdaWrapper.init(liveFunction); // Run the deployed lambda\r\n\r\n done();\r\n });\r\n\r\n it(\"Invoke / Initiate function test\", () => {\r\n return wrapped.run({}).then((response) => {\r\n expect(response).not.toBe(null);\r\n expect(response).toHaveProperty(\"statusCode\");\r\n expect(response).toHaveProperty(\"body\");\r\n expect(response.statusCode).toBe(200);\r\n });\r\n });\r\n});\r\n"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;AClFA;;;;;;ACAA;;;;;;ACAA;;;;;;;;;;;;ACAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;ACTA;AAAA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAJA;AAMA;AAPA;AASA;AAEA;AACA;AACA;AAIA;AACA;AACA;AACA;AACA;;;;;;AC1BA;;;;;;ACAA;;;;;;ACAA;;;;;;;;;;;;;;;;;;;;;;;;;;;ACAA;AACA;AAEA;AACA;AACA;AACA;AAAA;AAAA;AAFA;AAIA;AAGA;AACA;;ACZA;AACA;AACA;AAEA;AAAA;AAIA;AAAA;AAAA;AACA;;ACTA;AACA;AAEA;AACA;AACA;AACA;AAAA;AAAA;AACA;AAHA;AACA;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;;ACfA;;;;;AAKA;AAYA;AAIA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AAJA;AAFA;AAUA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAJA;AAMA;AACA;AACA;AAAA;AAAA;AADA;AAGA;AACA;AACA;AACA;AAPA;AAPA;AAFA;AAqBA;AACA;AACA;AACA;AACA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AAFA;AAIA;AACA;AACA;AACA;AARA;AAUA;AACA;AACA;AAAA;AAAA;AADA;AAGA;AACA;AACA;AACA;AAPA;AAXA;AAFA;AAtBA;AA+CA;;;;;ACnFA;AAEA;AAAA;AAAA;;ACAA;AACA;AACA;AACA;AACA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AAJA;AADA;AAFA;AAWA;;ACbA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AAAA;AAAA;AACA;AACA;AACA;AAEA;AACA;AACA;AAFA;;ACzBA;AACA;AACA;AAFA;;ACAA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAA;AAAA;AACA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAHA;AAKA;AAEA;AACA;AACA;AACA;;AC7BA;AACA;AAEA;AAEA;AACA;AAAA;AAGA;AACA;AACA;AAAA;AACA;AASA;AAAA;AAAA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;;;;;AC7BA;AACA;AAEA;AAEA;AACA;AACA;AAAA;AAAA;AAEA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;A","sourceRoot":""} -------------------------------------------------------------------------------- /__tests__/queryEvent.test.ts: -------------------------------------------------------------------------------- 1 | // tests for queryEvents 2 | // Generated by serverless-jest-plugin 3 | 4 | import * as mod from "../handler"; 5 | 6 | import * as jestPlugin from "serverless-jest-plugin"; 7 | const lambdaWrapper = jestPlugin.lambdaWrapper; 8 | const wrapped = lambdaWrapper.wrap(mod, { handler: "queryEvents" }); 9 | 10 | describe("λ Function - queryEvents", () => { 11 | beforeAll((done) => { 12 | // lambdaWrapper.init(liveFunction); // Run the deployed lambda 13 | 14 | done(); 15 | }); 16 | 17 | it("Invoke / Initiate function test", () => { 18 | return wrapped.run({}).then((response) => { 19 | expect(response).not.toBe(null); 20 | expect(response).toHaveProperty("statusCode"); 21 | expect(response).toHaveProperty("body"); 22 | expect(response.statusCode).toBe(200); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |My App
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DasithKuruppu/Serverless-GraphQL/d871d801a9c46d76187204dcecece573bf508495/client/style.css -------------------------------------------------------------------------------- /configs/test.webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const slsw = require("serverless-webpack"); 3 | const nodeExternals = require("webpack-node-externals"); 4 | const fs = require('fs'); 5 | 6 | const TestEntries = fs.readdirSync('./__tests__').filter(function(file) { 7 | return file.match(/.*\.ts$/); 8 | }); 9 | 10 | module.exports = { 11 | entry: TestEntries.reduce(function(prev,currentfileName){ 12 | var pathObj= Object.assign({},prev); 13 | var splitFiles=currentfileName.split("."); 14 | var newFileName=splitFiles.slice(0,splitFiles.length-1).join("."); 15 | pathObj[newFileName]=path.join(__dirname, "../__tests__/"+currentfileName) 16 | return pathObj; 17 | },{}), 18 | target: "node", 19 | mode: slsw.lib.webpack.isLocal ? "development" : "production", 20 | optimization: { 21 | // We no want to minimize our code. 22 | minimize: false, 23 | }, 24 | performance: { 25 | // Turn off size warnings for entry points 26 | hints: false, 27 | }, 28 | devtool: "cheap-module-source-map", 29 | externals: [nodeExternals()], 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.ts?$/, 34 | use: "babel-loader", 35 | exclude: /node_modules/, 36 | }, 37 | { 38 | test: /\.js$/, 39 | use: ["source-map-loader"], 40 | enforce: "pre", 41 | }, 42 | ], 43 | }, 44 | resolve: { 45 | extensions: [".ts", ".js"], 46 | }, 47 | output: { 48 | libraryTarget: "commonjs2", 49 | path: path.join(__dirname, "../__tests__/"), 50 | filename: "[name].js", 51 | sourceMapFilename: "[file].map", 52 | }, 53 | }; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | dynamodblocal: 4 | build: . 5 | ports: 6 | - "8000:8000" 7 | command: npm run dynamodb-start 8 | tty: true 9 | stdin_open: true 10 | image: dynamodboffline:dasith 11 | working_dir: /app 12 | volumes: 13 | - .:/app 14 | container_name: dynamodboffline-dasith 15 | env_file: 16 | - .env.docker 17 | logging: 18 | driver: "json-file" 19 | options: 20 | max-size: "10m" 21 | max-file: "3" -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | # install python tooling 4 | RUN apt-get update -y && apt-get install -y python-dev python-pip && pip install --upgrade pip 5 | 6 | # install other utils 7 | RUN apt-get update -y && apt-get install -y screen 8 | 9 | # install aws-cli 10 | RUN pip install awscli 11 | 12 | # install java 13 | RUN apt-get update && \ 14 | apt-get install -y openjdk-8-jdk && \ 15 | apt-get install -y ant && \ 16 | apt-get clean && \ 17 | rm -rf /var/lib/apt/lists/* && \ 18 | rm -rf /var/cache/oracle-jdk8-installer; 19 | 20 | # Fix certificate issues, found as of 21 | # https://bugs.launchpad.net/ubuntu/+source/ca-certificates-java/+bug/983302 22 | RUN apt-get update && \ 23 | apt-get install -y ca-certificates-java && \ 24 | apt-get clean && \ 25 | update-ca-certificates -f && \ 26 | rm -rf /var/lib/apt/lists/* && \ 27 | rm -rf /var/cache/oracle-jdk8-installer; 28 | 29 | # Setup JAVA_HOME, this is useful for docker commandline 30 | ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64/ 31 | RUN export JAVA_HOME 32 | 33 | # change work directory 34 | 35 | RUN mkdir -p /app 36 | WORKDIR /app 37 | 38 | # install serverless framework 39 | RUN npm install -g serverless 40 | 41 | RUN npm install 42 | 43 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 44 | 45 | EXPOSE 8000 46 | 47 | CMD npm run dynamodb-install -------------------------------------------------------------------------------- /dynamodb/index.js: -------------------------------------------------------------------------------- 1 | import { DynamoDB } from "aws-sdk"; 2 | 3 | let dynamoDbClient = new DynamoDB.DocumentClient(); 4 | if (process.env.IS_OFFLINE) { 5 | dynamoDbClient = new DynamoDB.DocumentClient({ 6 | endpoint: "http://localhost:8000", 7 | region: "localhost", 8 | }); 9 | } 10 | 11 | export { dynamoDbClient }; 12 | -------------------------------------------------------------------------------- /elasticSearch/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | INDEX: "defaultevents", 3 | TYPE: "bookingevent", 4 | }; 5 | -------------------------------------------------------------------------------- /elasticSearch/elasticSearchConnect.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@elastic/elasticsearch"; 2 | 3 | export const esClient = new Client({ node: `https://${process.env.ELASTICSEARCH_URL}` }); 4 | -------------------------------------------------------------------------------- /elasticSearch/mappings.ts: -------------------------------------------------------------------------------- 1 | import { RequestParams } from "@elastic/elasticsearch"; 2 | 3 | export function getESMappings(index: string): RequestParams.IndicesCreate { 4 | return { 5 | index, 6 | body: { 7 | properties: { 8 | id: { type: "text" }, 9 | description: { type: "text" }, 10 | name: { type: "text" }, 11 | addedAt: { type: "number" }, 12 | }, 13 | }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /env.yml: -------------------------------------------------------------------------------- 1 | # Add the environment variables for the various stages 2 | 3 | prod: 4 | ENV_TYPE: "prod" 5 | default: 6 | ENV_TYPE: "dev" 7 | ELASTICSEARCH_URL: 8 | Fn::GetAtt: 9 | - EventsGqlElasticSearch 10 | - DomainEndpoint # get domain endpoitn attribute from elastic search resource 11 | test: 12 | ENV_TYPE: "test" 13 | ELASTICSEARCH_URL: 14 | Fn::GetAtt: 15 | - EventsGqlElasticSearch 16 | - DomainEndpoint -------------------------------------------------------------------------------- /handler.ts: -------------------------------------------------------------------------------- 1 | import schema from "./schemas/index"; 2 | import { graphql } from "graphql"; 3 | import { APIGatewayProxyEvent } from "aws-lambda"; 4 | import { indexElasticSearch } from "./streams/process"; 5 | // Highly scalable FaaS architecture :) 6 | // Export a function which would be hooked up to the the λ node/ nodes as specified on serverless.yml template 7 | export async function queryEvents( 8 | event: APIGatewayProxyEvent, 9 | // context: Context, 10 | ) { 11 | const parsedRequestBody = event && event.body ? JSON.parse(event.body) : {}; 12 | try { 13 | const graphQLResult = await graphql( 14 | schema, 15 | parsedRequestBody.query, 16 | null, 17 | null, 18 | parsedRequestBody.variables, 19 | parsedRequestBody.operationName, 20 | ); 21 | 22 | return { statusCode: 200, body: JSON.stringify(graphQLResult) }; 23 | } catch (error) { 24 | throw error; 25 | } 26 | } 27 | 28 | export async function processStreams(event: APIGatewayProxyEvent) { 29 | await indexElasticSearch(event); 30 | } 31 | -------------------------------------------------------------------------------- /images/GraphQLPlay.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DasithKuruppu/Serverless-GraphQL/d871d801a9c46d76187204dcecece573bf508495/images/GraphQLPlay.PNG -------------------------------------------------------------------------------- /images/GraphQLPlayground.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DasithKuruppu/Serverless-GraphQL/d871d801a9c46d76187204dcecece573bf508495/images/GraphQLPlayground.PNG -------------------------------------------------------------------------------- /images/deploy-dev.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DasithKuruppu/Serverless-GraphQL/d871d801a9c46d76187204dcecece573bf508495/images/deploy-dev.PNG -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | collectCoverageFrom: [ 5 | "**/*.ts", 6 | "!**/__tests__**", 7 | "!**/node_modules/**", 8 | "!**/.webpack/**" 9 | ], 10 | testMatch: ["**/__tests__/**/*.js", "**/?(*.)+(spec|test).js"], 11 | globals: { 12 | 'ts-jest': { 13 | diagnostics: { 14 | pathRegex: /\.(spec|test)\.ts$/, 15 | warnOnly: true, 16 | ignoreCodes: [6133] 17 | } 18 | } 19 | }, 20 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-graphql-template", 3 | "version": "1.0.0", 4 | "author": { 5 | "name": "Dasith kuruppu", 6 | "email": "dasithkuruppu@gmail.com", 7 | "url": "https://github.com/DasithKuruppu" 8 | }, 9 | "description": "Initial boilerplate for serverless lambda with dynamodb,elasticsearch and graphql - events booking app", 10 | "keywords": [ 11 | "FaaS", 12 | "nodejs", 13 | "dynamodb", 14 | "lambda", 15 | "serverless", 16 | "graphql", 17 | "elasticsearch" 18 | ], 19 | "main": "handler.js", 20 | "scripts": { 21 | "test-lambda": "serverless invoke test --stage default -f queryEvents", 22 | "start": "serverless offline start", 23 | "compile-debug": "serverless webpack --out dist", 24 | "deploy-dev": "serverless deploy -v --stage=devdefault --force", 25 | "deploy-test": "serverless deploy -v --stage=test", 26 | "deploy-prod": "serverless deploy -v --stage=prod", 27 | "deploy-to-bucket": "serverless s3sync", 28 | "tslint": "tslint --project tsconfig.json --config tslint.json", 29 | "compile-tests": "webpack --config configs/test.webpack.config.js", 30 | "unit-tests": "jest --config jest.config.js --verbose", 31 | "dynamodb-start": "serverless dynamodb start", 32 | "dynamodb-install": "serverless dynamodb install" 33 | }, 34 | "license": "ISC", 35 | "dependencies": { 36 | "@elastic/elasticsearch": "^7.0.0-rc.2", 37 | "aws-sdk": "^2.361.0", 38 | "graphql": "^14.0.2", 39 | "graphql-dynamodb-connections": "^1.0.2", 40 | "graphql-iso-date": "^3.6.1", 41 | "serverless-s3-sync": "^1.8.0", 42 | "uuid": "^3.3.2" 43 | }, 44 | "devDependencies": { 45 | "@babel/cli": "^7.1.5", 46 | "@babel/core": "^7.1.6", 47 | "@babel/plugin-proposal-object-rest-spread": "^7.4.4", 48 | "@babel/preset-env": "^7.1.6", 49 | "@babel/preset-typescript": "^7.1.0", 50 | "@types/aws-lambda": "^8.10.15", 51 | "@types/graphql": "^14.0.3", 52 | "@types/jest": "^23.3.10", 53 | "@types/node": "^10.12.11", 54 | "babel-loader": "^8.0.4", 55 | "jest": "^24.6.0", 56 | "serverless": "^1.41.1", 57 | "serverless-dynamodb-local": "^0.2.25", 58 | "serverless-jest-plugin": "^0.2.1", 59 | "serverless-offline": "^3.31.3", 60 | "serverless-plugin-offline-dynamodb-stream": "^1.0.18", 61 | "serverless-webpack": "^5.2.0", 62 | "source-map-loader": "^0.2.4", 63 | "ts-jest": "^23.10.5", 64 | "tslint": "^5.11.0", 65 | "tslint-eslint-rules": "^5.4.0", 66 | "typescript": "^3.2.1", 67 | "webpack": "^4.26.0", 68 | "webpack-cli": "^3.1.2", 69 | "webpack-node-externals": "^1.7.2", 70 | "serverless-s3-deploy": "^0.8.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## 🌩️ Serverless Nodejs/GraphQL Boilerplate 3 | (IaC using YML/Serverless & other 🦄 magical stuff) 4 | 5 | [](https://circleci.com/gh/DasithKuruppu/Serverless-GraphQL) 6 | [](https://github.com/DasithKuruppu/Serverless-GraphQL) 7 | [](https://github.com/DasithKuruppu/Serverless-GraphQL) 8 | [](https://github.com/DasithKuruppu/Serverless-GraphQL) 9 | [](https://github.com/facebook/jest) 10 | [](https://github.com/facebook/jest) 11 | [](http://forthebadge.com) 12 | [](http://forthebadge.com) 13 | 14 | Please go to [Gitlab Repo](https://gitlab.com/DasithKuruppu/serverlessgraphql) or use gitlab if you want to use free gitlab CI/CD instead of [circleCI](https://circleci.com/) 15 | 16 | Read more on : 17 | * [Medium post](https://levelup.gitconnected.com/going-serverless-with-nodejs-graphql-5b34f5d280f4) on setting this up 18 | * Vist [dasithsblog.com](https://www.dasithsblog.com/) for other cool stuff. 19 | 20 | 21 | ## Features. 22 | 23 | --- 24 | 25 | 1. ⚡ [Serverless](https://serverless.com/) YML templates(Infrastucture As Code- IAC) that provide and provision dynamic/scalable resources like DynamoDb,S3,elastic search, lambda functions etc with a single command. 26 | 2. 📜 Support for [Typescript](https://www.typescriptlang.org/). 27 | 3. 🎭 An attempt at unit testablity and test coverage via [Jest](https://jestjs.io/), this would be focused more heavily on later updates. 28 | 4. 🌀 Attempts to focus on a simple development structure / flow. 29 | 5. 🏋️♂️ Support for any amount of environments (Dev,Test,Production etc..) replications via Cloud formation templates. 30 | 6. 🕸️[GraphQL](https://graphql.org/) api exposed via a single endpoint with the posiblilty to scale via step functions or otherwise in future. 31 | 7. ✔️ CI/CD for all 3 environments (dev,test,production) via [Gitlab CI/CD](https://docs.gitlab.com/ee/ci/) 32 | 8. 🐋 Docker file / compose for running dynamodb offline on a docker container. 33 | 34 | 35 | ## Architecture 36 | 37 |  38 | 39 | 40 | ## How to Install & Pre-requisites 41 | 42 | --- 43 | 44 | **_Clone project into your local folder.... & then navigate to project on terminal or Shell_** 45 | 46 | ```javascript 47 | npm install -g serverless 48 | 49 | serverless config credentials --provider aws --key $AWS_ACCESS_KEY_ID --secret $AWS_SECRET_ACCESS_KEY 50 | ``` 51 | 52 | `Note that the $AWS_ACCESS_KEY_ID and $AWS_SECRET_ACCESS_KEY here needs to be replaced by credentials given to you by project owner or you may create your own AWS account and IAM role / credentials for programatic access` 53 | [Click here for more info !!!](https://serverless.com/framework/docs/providers/aws/guide/credentials/) 54 | 55 | Install [GraphQL Playground](https://github.com/prisma/graphql-playground/releases) `optional for easy querying` 56 | 57 | ## Getting started 58 | 59 | --- 60 | 61 | ```javascript 62 | npm install 63 | 64 | npm install -g graphql-cli 65 | 66 | npm run deploy-dev 67 | 68 | ``` 69 | 70 | ```javascript 71 | npm start 72 | ``` 73 | 74 | To run it locally 75 | 76 | ## Using / Playing around with it... 77 | 78 | --- 79 | 80 | Initially make sure you have completed steps in both **_Install & Pre-requisites_** and in **_Getting started_** 81 | 82 | ```javascript 83 | npm run deploy-dev 84 | ``` 85 | 86 | Copy URL to the lambda function that is output onto the terminal once above command is run 87 |  88 | 89 | Then Open GraphQL Playground and Select "URL EndPoint" and paste the Copied URL there 90 |  91 | 92 | Then Run 93 | 94 | ```javascript 95 | query { 96 | listEvents { 97 | id 98 | name 99 | } 100 | } 101 | 102 | ``` 103 | 104 | To see if the GraphQL query works as inteded either by returning empty list or list of "events" 105 |  106 | 107 | Read more on graphQL to learn Queries / Mutations to try out more queries againt the API 108 | -------------------------------------------------------------------------------- /resolvers/events/create.ts: -------------------------------------------------------------------------------- 1 | import { IEvent } from "./typings"; 2 | import { dynamoDbClient } from "../../dynamodb"; 3 | import * as uuidv4 from "uuid/v4"; 4 | 5 | export function createParams(data: IEvent, TableName: string , uniqueID: string) { 6 | return { 7 | Item: { 8 | name: data.name, 9 | description: data.description, 10 | id: uniqueID, 11 | addedAt: Date.now(), 12 | }, 13 | TableName, 14 | }; 15 | } 16 | 17 | export default (data: IEvent) => { 18 | const putParams = createParams(data, process.env.TABLE_NAME, uuidv4()); 19 | return dynamoDbClient 20 | .put(putParams) 21 | .promise() 22 | .then(() => { 23 | return putParams.Item; 24 | }).catch((err) => { 25 | throw err; 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /resolvers/events/list.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDB } from "aws-sdk"; 2 | import { dynamoDbClient } from "../../dynamodb"; 3 | export default () => 4 | dynamoDbClient 5 | .scan({ TableName: process.env.TABLE_NAME }) 6 | .promise() 7 | .then((list: DynamoDB.DocumentClient.ScanOutput) => list.Items.map( 8 | (Item) => { 9 | return ({ ...Item, addedAt: new Date(Item.addedAt) }); 10 | })); 11 | -------------------------------------------------------------------------------- /resolvers/events/remove.ts: -------------------------------------------------------------------------------- 1 | import { dynamoDbClient } from "../../dynamodb"; 2 | 3 | export default async (id: string) => { 4 | const params = { 5 | TableName: process.env.TABLE_NAME, 6 | Key: { id }, 7 | ReturnValues: "ALL_OLD", 8 | }; 9 | try { 10 | const response = await dynamoDbClient.delete(params).promise(); 11 | return response.Attributes; 12 | } catch (error) { 13 | throw error; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /resolvers/events/typings.ts: -------------------------------------------------------------------------------- 1 | export interface IEvent { 2 | id: string; 3 | name: string; 4 | description?: string; 5 | addedAt: number; 6 | startingOn?: number; 7 | } 8 | -------------------------------------------------------------------------------- /resolvers/events/view.ts: -------------------------------------------------------------------------------- 1 | import { dynamoDbClient } from "../../dynamodb"; 2 | 3 | export default async (id: string) => { 4 | const params = { 5 | TableName: process.env.TABLE_NAME, 6 | Key: { id }, 7 | }; 8 | const GetEvents = await dynamoDbClient.get(params).promise(); 9 | return GetEvents.Item; 10 | }; 11 | -------------------------------------------------------------------------------- /resources/cognito-identity-pool.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | # The federated identity for our user pool to auth with 3 | CognitoIdentityPool: 4 | Type: AWS::Cognito::IdentityPool 5 | Properties: 6 | # Generate a name based on the stage 7 | IdentityPoolName: ${self:custom.stage}IdentityPool 8 | # Don't allow unathenticated users 9 | AllowUnauthenticatedIdentities: false 10 | # Link to our User Pool 11 | CognitoIdentityProviders: 12 | - ClientId: 13 | Ref: CognitoUserPoolClient 14 | ProviderName: 15 | Fn::GetAtt: [ "CognitoUserPool", "ProviderName" ] 16 | 17 | # IAM roles 18 | CognitoIdentityPoolRoles: 19 | Type: AWS::Cognito::IdentityPoolRoleAttachment 20 | Properties: 21 | IdentityPoolId: 22 | Ref: CognitoIdentityPool 23 | Roles: 24 | authenticated: 25 | Fn::GetAtt: [CognitoAuthRole, Arn] 26 | 27 | # IAM role used for authenticated users 28 | CognitoAuthRole: 29 | Type: AWS::IAM::Role 30 | Properties: 31 | Path: / 32 | AssumeRolePolicyDocument: 33 | Version: '2012-10-17' 34 | Statement: 35 | - Effect: 'Allow' 36 | Principal: 37 | Federated: 'cognito-identity.amazonaws.com' 38 | Action: 39 | - 'sts:AssumeRoleWithWebIdentity' 40 | Condition: 41 | StringEquals: 42 | 'cognito-identity.amazonaws.com:aud': 43 | Ref: CognitoIdentityPool 44 | 'ForAnyValue:StringLike': 45 | 'cognito-identity.amazonaws.com:amr': authenticated 46 | Policies: 47 | - PolicyName: 'CognitoAuthorizedPolicy' 48 | PolicyDocument: 49 | Version: '2012-10-17' 50 | Statement: 51 | - Effect: 'Allow' 52 | Action: 53 | - 'mobileanalytics:PutEvents' 54 | - 'cognito-sync:*' 55 | - 'cognito-identity:*' 56 | Resource: '*' 57 | 58 | # Allow users to invoke our API 59 | - Effect: 'Allow' 60 | Action: 61 | - 'execute-api:Invoke' 62 | Resource: 63 | Fn::Join: 64 | - '' 65 | - 66 | - 'arn:aws:execute-api:' 67 | - Ref: AWS::Region 68 | - ':' 69 | - Ref: AWS::AccountId 70 | - ':' 71 | - Ref: ApiGatewayRestApi 72 | - '/*' 73 | 74 | # Print out the Id of the Identity Pool that is created 75 | Outputs: 76 | IdentityPoolId: 77 | Value: 78 | Ref: CognitoIdentityPool -------------------------------------------------------------------------------- /resources/cognito-user-pool.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | CognitoUserPool: 3 | Type: AWS::Cognito::UserPool 4 | Properties: 5 | # Generate a name based on the stage 6 | UserPoolName: ${self:custom.stage}-user-pool 7 | # Set email as an alias 8 | UsernameAttributes: 9 | - email 10 | AutoVerifiedAttributes: 11 | - email 12 | 13 | CognitoUserPoolClient: 14 | Type: AWS::Cognito::UserPoolClient 15 | Properties: 16 | # Generate an app client name based on the stage 17 | ClientName: ${self:custom.stage}-user-pool-client 18 | UserPoolId: 19 | Ref: CognitoUserPool 20 | ExplicitAuthFlows: 21 | - ADMIN_NO_SRP_AUTH 22 | GenerateSecret: false 23 | 24 | # Print out the Id of the User Pool that is created 25 | Outputs: 26 | UserPoolId: 27 | Value: 28 | Ref: CognitoUserPool 29 | 30 | UserPoolClientId: 31 | Value: 32 | Ref: CognitoUserPoolClient -------------------------------------------------------------------------------- /resources/dynamodb.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | EventsGqlDynamoDbTable: 3 | Type: AWS::DynamoDB::Table 4 | Properties: 5 | TableName: ${self:custom.tableName} 6 | AttributeDefinitions: 7 | - AttributeName: id 8 | AttributeType: S 9 | KeySchema: 10 | - AttributeName: id 11 | KeyType: HASH 12 | # Set the capacity based on the stage 13 | ProvisionedThroughput: 14 | ReadCapacityUnits: ${self:custom.tableThroughput} 15 | WriteCapacityUnits: ${self:custom.tableThroughput} 16 | StreamSpecification: 17 | StreamViewType: NEW_AND_OLD_IMAGES 18 | 19 | 20 | -------------------------------------------------------------------------------- /resources/elasticsearch.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | EventsGqlElasticSearch: 3 | Type: "AWS::Elasticsearch::Domain" 4 | Properties: 5 | ElasticsearchVersion: "6.3" 6 | ElasticsearchClusterConfig: 7 | DedicatedMasterEnabled: false 8 | InstanceCount: "1" 9 | ZoneAwarenessEnabled: false 10 | InstanceType: "t2.small.elasticsearch" 11 | EBSOptions: 12 | EBSEnabled: true 13 | Iops: 0 14 | VolumeSize: 10 15 | VolumeType: "gp2" 16 | # Attention! Before you enable this lines, check out the README to avoid an open access policy 17 | AccessPolicies: 18 | Version: "2012-10-17" 19 | Statement: 20 | - 21 | Effect: "Allow" 22 | Principal: 23 | AWS: "*" 24 | Action: "es:*" 25 | Resource: "*" 26 | AdvancedOptions: 27 | rest.action.multi.allow_explicit_index: "true" 28 | -------------------------------------------------------------------------------- /resources/s3-bucket-cdn.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | WebAppS3Bucket: 3 | Type: AWS::S3::Bucket 4 | Properties: 5 | BucketName: ${self:custom.s3Bucket} 6 | AccessControl: PublicRead 7 | WebsiteConfiguration: 8 | IndexDocument: index.html 9 | ErrorDocument: index.html 10 | ## Specifying the policies to make sure all files inside the Bucket are avaialble to CloudFront 11 | WebAppS3BucketPolicy: 12 | Type: AWS::S3::BucketPolicy 13 | Properties: 14 | Bucket: 15 | Ref: WebAppS3Bucket 16 | PolicyDocument: 17 | Statement: 18 | - Sid: PublicReadGetObject 19 | Effect: Allow 20 | Principal: "*" 21 | Action: 22 | - s3:GetObject 23 | Resource: arn:aws:s3:::${self:custom.s3Bucket}/* 24 | ## Specifying the CloudFront Distribution to server your Web Application 25 | WebAppCloudFrontDistribution: 26 | Type: AWS::CloudFront::Distribution 27 | Properties: 28 | DistributionConfig: 29 | Origins: 30 | - DomainName: ${self:custom.s3Bucket}.s3.amazonaws.com 31 | ## An identifier for the origin which must be unique within the distribution 32 | Id: WebApp 33 | CustomOriginConfig: 34 | HTTPPort: 80 35 | HTTPSPort: 443 36 | OriginProtocolPolicy: https-only 37 | ## In case you want to restrict the bucket access use S3OriginConfig and remove CustomOriginConfig 38 | # S3OriginConfig: 39 | # OriginAccessIdentity: origin-access-identity/cloudfront/E127EXAMPLE51Z 40 | Enabled: 'true' 41 | ## Uncomment the following section in case you are using a custom domain 42 | # Aliases: 43 | # - mysite.example.com 44 | DefaultRootObject: index.html 45 | ## Since the Single Page App is taking care of the routing we need to make sure ever path is served with index.html 46 | ## The only exception are files that actually exist e.h. app.js, reset.css 47 | CustomErrorResponses: 48 | - ErrorCode: 404 49 | ResponseCode: 200 50 | ResponsePagePath: /index.html 51 | DefaultCacheBehavior: 52 | AllowedMethods: 53 | - DELETE 54 | - GET 55 | - HEAD 56 | - OPTIONS 57 | - PATCH 58 | - POST 59 | - PUT 60 | ## The origin id defined above 61 | TargetOriginId: WebApp 62 | ## Defining if and how the QueryString and Cookies are forwarded to the origin which in this case is S3 63 | ForwardedValues: 64 | QueryString: 'false' 65 | Cookies: 66 | Forward: none 67 | ## The protocol that users can use to access the files in the origin. To allow HTTP use `allow-all` 68 | ViewerProtocolPolicy: redirect-to-https 69 | ## The certificate to use when viewers use HTTPS to request objects. 70 | ViewerCertificate: 71 | CloudFrontDefaultCertificate: 'true' 72 | ## Uncomment the following section in case you want to enable logging for CloudFront requests 73 | # Logging: 74 | # IncludeCookies: 'false' 75 | # Bucket: mylogs.s3.amazonaws.com 76 | # Prefix: myprefix 77 | 78 | ## In order to print out the hosted domain via `serverless info` we need to define the DomainName output for CloudFormation 79 | Outputs: 80 | WebAppCloudFrontDistributionOutput: 81 | Value: 82 | Fn::GetAtt: [ WebAppCloudFrontDistribution, DomainName ] -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- 1 | # source: https://a8noghdsdb.execute-api.us-east-1.amazonaws.com/default/events 2 | # timestamp: Mon Dec 24 2018 01:01:26 GMT+0530 (India Standard Time) 3 | 4 | """ 5 | A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the 6 | `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 7 | 8601 standard for representation of dates and times using the Gregorian calendar. 8 | """ 9 | scalar DateTime 10 | 11 | type Event { 12 | id: String! 13 | name: String! 14 | description: String! 15 | addedAt: DateTime! 16 | } 17 | 18 | type Mutation { 19 | createEvent(name: String!, description: String!): Event 20 | removeProduct(id: String!): Boolean 21 | } 22 | 23 | type Query { 24 | listEvents: [Event] 25 | viewEvent(id: String!): Event 26 | } 27 | -------------------------------------------------------------------------------- /schemas/index.ts: -------------------------------------------------------------------------------- 1 | /*import { 2 | paginationToParams, 3 | dataToConnection 4 | } from "graphql-dynamodb-connections"; 5 | */ 6 | import { 7 | GraphQLSchema, 8 | GraphQLObjectType, 9 | GraphQLString, 10 | GraphQLList, 11 | GraphQLNonNull, 12 | GraphQLBoolean, 13 | } from "graphql"; 14 | 15 | import { 16 | /*GraphQLDate, 17 | GraphQLTime,*/ 18 | GraphQLDateTime, 19 | } from "graphql-iso-date"; 20 | 21 | import { IEvent } from "../resolvers/events/typings"; 22 | import addEvent from "../resolvers/events/create"; 23 | import viewEvent from "../resolvers/events/view"; 24 | import listEvents from "../resolvers/events/list"; 25 | import removeEvent from "../resolvers/events/remove"; 26 | 27 | const eventType = new GraphQLObjectType({ 28 | name: "Event", 29 | fields: { 30 | id: { type: new GraphQLNonNull(GraphQLString) }, 31 | name: { type: new GraphQLNonNull(GraphQLString) }, 32 | description: { type: new GraphQLNonNull(GraphQLString) }, 33 | addedAt: { type: new GraphQLNonNull(GraphQLDateTime) }, 34 | }, 35 | }); 36 | 37 | const schema = new GraphQLSchema({ 38 | query: new GraphQLObjectType({ 39 | name: "Query", 40 | fields: { 41 | listEvents: { 42 | type: new GraphQLList(eventType), 43 | resolve: (parent ) => { 44 | return listEvents(); 45 | }, 46 | }, 47 | viewEvent: { 48 | args: { 49 | id: { type: new GraphQLNonNull(GraphQLString) }, 50 | }, 51 | type: eventType, 52 | resolve: (parent, args: { id: string }) => { 53 | return viewEvent(args.id); 54 | }, 55 | }, 56 | }, 57 | }), 58 | 59 | mutation: new GraphQLObjectType({ 60 | name: "Mutation", 61 | fields: { 62 | createEvent: { 63 | args: { 64 | name: { type: new GraphQLNonNull(GraphQLString) }, 65 | description: { type: new GraphQLNonNull(GraphQLString) }, 66 | }, 67 | type: eventType, 68 | resolve: (parent, args: IEvent) => { 69 | return addEvent(args); 70 | }, 71 | }, 72 | removeEvent: { 73 | args: { 74 | id: { type: new GraphQLNonNull(GraphQLString) }, 75 | }, 76 | type: eventType, 77 | resolve: (parent, args: { id: string }) => { 78 | return removeEvent(args.id); 79 | }, 80 | }, 81 | }, 82 | }), 83 | }); 84 | export default schema; 85 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: aws-nodejs 2 | plugins: 3 | - serverless-webpack 4 | - serverless-dynamodb-local 5 | - serverless-plugin-offline-dynamodb-stream 6 | - serverless-s3-sync #https://github.com/k1LoW/serverless-s3-sync 7 | - serverless-jest-plugin 8 | - serverless-offline 9 | custom: 10 | # Our stage is based on what is passed in when running serverless 11 | # commands. Or fallsback to what we have set in the provider section. 12 | stage: ${opt:stage, self:provider.stage} 13 | # Set the table name here so we can use it while testing locally 14 | tableName: ${self:custom.stage}-events 15 | # Set our DynamoDB throughput for prod and all other non-prod stages. 16 | s3Bucket: webapps3bucket-${self:custom.stage} 17 | s3Sync: 18 | - bucketName: ${self:custom.s3Bucket} # required 19 | localDir: client/ # required 20 | params: # optional 21 | - index.html: 22 | CacheControl: 'no-cache' 23 | tableThroughputs: 24 | prod: 1 #more throughput on production env 25 | default: 1 26 | tableThroughput: ${self:custom.tableThroughputs.${self:custom.stage}, self:custom.tableThroughputs.default} 27 | webpack: 28 | webpackConfig: './webpack.config.js' # Name of webpack configuration file 29 | includeModules: true # Node modules configuration for packaging 30 | packager: 'npm' # Packager that will be used to package your external module 31 | dynamodb: 32 | start: 33 | port: 8000 34 | migrate: true 35 | inMemory: true 36 | noStart: true 37 | dynamodbStream: 38 | port: 8000 39 | streams: 40 | - table: EventTable 41 | functions: 42 | - processStreams 43 | jest: 44 | collectCoverage: true 45 | collectCoverageFrom: 46 | - "**/*.js" 47 | - "!**/__tests__/**" 48 | - "!**/node_modules/**" 49 | - "!**/.webpack/**" 50 | preset: "ts-jest" 51 | environment: ${file(env.yml):${self:custom.stage}, file(env.yml):default} 52 | provider: 53 | name: aws 54 | runtime: nodejs8.10 55 | stage: default 56 | iamRoleStatements: 57 | - Effect: Allow 58 | Action: 59 | - dynamodb:ListTables 60 | - dynamodb:DescribeTable 61 | - dynamodb:DescribeStream 62 | - dynamodb:ListStreams 63 | - dynamodb:GetShardIterator 64 | - dynamodb:BatchGetItem 65 | - dynamodb:GetItem 66 | - dynamodb:PutItem 67 | - dynamodb:UpdateItem 68 | - dynamodb:DeleteItem 69 | - dynamodb:Query 70 | - dynamodb:Scan 71 | - dynamodb:DescribeReservedCapacity 72 | - dynamodb:DescribeReservedCapacityOfferings 73 | - dynamodb:GetRecords 74 | Resource: 75 | Fn::Join: 76 | - "" 77 | - - "arn:aws:dynamodb:*:*:table/" 78 | - Ref: EventsGqlDynamoDbTable 79 | - Effect: Allow 80 | Action: 81 | - es:ESHttpPost 82 | - es:ESHttpPut 83 | - es:ESHttpDelete 84 | - es:ESHttpGet 85 | Resource: 86 | - { "Fn::GetAtt": ["EventsGqlElasticSearch", "DomainArn"] } 87 | - { "Fn::Join": ["", ["Fn::GetAtt": ["EventsGqlElasticSearch", "DomainArn"], "/*"]] } 88 | functions: 89 | queryEvents: 90 | handler: handler.queryEvents 91 | events: 92 | - http: 93 | path: events 94 | method: post 95 | cors: true 96 | environment: 97 | TABLE_NAME: ${self:custom.tableName} 98 | dynamo-stream-to-elasticsearch: 99 | handler: handler.processStreams 100 | memorySize: 128 101 | timeout: 60 102 | events: 103 | - stream: 104 | type: dynamodb 105 | batchSize: 100 106 | enabled: true 107 | arn: 108 | Fn::GetAtt: 109 | - EventsGqlDynamoDbTable 110 | - StreamArn 111 | environment: 112 | ELASTICSEARCH_URL: 113 | Fn::GetAtt: 114 | - EventsGqlElasticSearch 115 | - DomainEndpoint 116 | 117 | resources: 118 | # DynamoDB 119 | - ${file(resources/dynamodb.yml)} 120 | # S3 121 | - ${file(resources/s3-bucket-cdn.yml)} 122 | # Cognito 123 | - ${file(resources/cognito-user-pool.yml)} 124 | - ${file(resources/cognito-identity-pool.yml)} 125 | # elasticsearch 126 | - ${file(resources/elasticsearch.yml)} 127 | 128 | -------------------------------------------------------------------------------- /streams/process.ts: -------------------------------------------------------------------------------- 1 | import { esClient } from "../elasticSearch/elasticSearchConnect"; 2 | import { getESMappings } from "../elasticSearch/mappings"; 3 | import { extractRecordsFromDynamodbEvent, actions } from "./utils"; 4 | import { config } from "../elasticSearch/config"; 5 | 6 | export async function indexElasticSearch(event) { 7 | try { 8 | // check if indices already exist 9 | const exists = await esClient.indices.exists({ index: config.INDEX }); 10 | if (!exists) { 11 | // if not create new index and mappings for it 12 | await esClient.indices.create(getESMappings(config.INDEX)); 13 | } 14 | // extract data 15 | 16 | const dataArray = extractRecordsFromDynamodbEvent(event)[actions.INSERT] || []; // default to empty 17 | 18 | await Promise.all( 19 | dataArray.map(async (data) => { 20 | await esClient.index({ 21 | id: data.id, 22 | index: config.INDEX, 23 | body: data, 24 | }); 25 | }), 26 | ); 27 | } catch (err) { 28 | throw err; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /streams/sampleEvent.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "009e960aaa7b72d24da376226aa92c1e", 5 | "eventName": "INSERT", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "us-east-1", 9 | "dynamodb": { 10 | "ApproximateCreationDateTime": 1557032163, 11 | "Keys": { 12 | "id": { 13 | "S": "c437ed51-5d0e-4828-9f78-bcf3c40ab55e" 14 | } 15 | }, 16 | "NewImage": { 17 | "addedAt": { 18 | "N": "1557032163354" 19 | }, 20 | "name": { 21 | "S": "Random event" 22 | }, 23 | "description": { 24 | "S": "Random Description" 25 | }, 26 | "id": { 27 | "S": "c437ed51-5d0e-4828-9f78-bcf3c40ab55e" 28 | } 29 | }, 30 | "SequenceNumber": "15970400000000003743028958", 31 | "SizeBytes": 136, 32 | "StreamViewType": "NEW_AND_OLD_IMAGES" 33 | }, 34 | "eventSourceARN": "arn:aws:dynamodb:us-east-1:315050634537:table/test-events/stream/2019-05-01T16:40:18.739" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /streams/utils/index.ts: -------------------------------------------------------------------------------- 1 | 2 | function transformData(newImage) { 3 | const transformedObject = {}; 4 | Object.keys(newImage).forEach((key: string) => { 5 | const dataType = Object.keys(newImage[key])[0]; 6 | transformedObject[key] = newImage[key][dataType]; 7 | }); 8 | return transformedObject; 9 | } 10 | 11 | export function extractRecordsFromDynamodbEvent(event) { 12 | if (!event.Records || !Array.isArray(event.Records) || event.Records.length <= 0) { 13 | return null; 14 | } 15 | 16 | return event.Records.reduce((acculator, current) => { 17 | const ACTION: "INSERT" | "UPDATE" = current.eventName; 18 | const existingRecords = acculator[ACTION] || []; 19 | const existsDynamoDb = current.dynamodb && current.dynamodb.NewImage; 20 | if (existsDynamoDb) { 21 | return { ...acculator, [ACTION]: [...existingRecords, transformData(current.dynamodb.NewImage)] }; 22 | } 23 | }, {}); 24 | } 25 | 26 | export const actions = { 27 | INSERT: "INSERT", 28 | UPDATE: "UPDATE", 29 | }; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "allowJs": true, 7 | "allowUnreachableCode": false, 8 | "noUnusedParameters": true, 9 | "noUnusedLocals": true 10 | }, 11 | "files": [ 12 | "./handler.ts", 13 | ], 14 | "include": ["./*.ts","./**/*.ts"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "max-line-length": { 5 | "options": [120] 6 | }, 7 | "new-parens": true, 8 | "no-arg": true, 9 | "no-bitwise": true, 10 | "no-conditional-assignment": true, 11 | "no-consecutive-blank-lines": true, 12 | "no-any": true, 13 | "no-console": { 14 | "severity": "warning", 15 | "options": ["debug", "info", "log", "time", "timeEnd", "trace"] 16 | }, 17 | "no-unused-variable": {"severity": "warning"}, 18 | "object-literal-sort-keys": false, 19 | "ordered-imports": false 20 | }, 21 | "jsRules": { 22 | "max-line-length": { 23 | "options": [120] 24 | } 25 | }, 26 | "linterOptions": { 27 | "exclude": [ 28 | "*.json", 29 | "**/*.json" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const slsw = require("serverless-webpack"); 3 | const nodeExternals = require("webpack-node-externals"); 4 | 5 | module.exports = { 6 | entry: slsw.lib.entries, 7 | target: "node", 8 | mode: slsw.lib.webpack.isLocal ? "development" : "production", 9 | optimization: { 10 | // We no want to minimize our code. 11 | minimize: false 12 | }, 13 | performance: { 14 | // Turn off size warnings for entry points 15 | hints: false 16 | }, 17 | devtool: 'cheap-module-source-map', 18 | externals: [nodeExternals()], 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.ts?$/, 23 | use: 'babel-loader', 24 | exclude: /node_modules/ 25 | }, 26 | { 27 | test: /\.js$/, 28 | use: ["source-map-loader"], 29 | enforce: "pre" 30 | }, 31 | ] 32 | }, 33 | resolve: { 34 | extensions: [ '.tsx', '.ts', '.js' ] 35 | }, 36 | output: { 37 | libraryTarget: "commonjs2", 38 | path: path.join(__dirname, ".webpack"), 39 | filename: "[name].js", 40 | sourceMapFilename: "[file].map" 41 | } 42 | }; 43 | --------------------------------------------------------------------------------