├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── settings.json └── tasks.json ├── .yo-rc.json ├── CODE_OF_CONDUCT.md ├── DEVELOPING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docker-compose.yml ├── index.js ├── index.ts ├── package.json ├── public └── index.html ├── src ├── application.ts ├── controllers │ ├── README.md │ ├── home-page.controller.ts │ ├── index.ts │ ├── kafka-demo.controller.ts │ └── ping.controller.ts ├── datasources │ └── README.md ├── index.ts ├── models │ └── README.md ├── repositories │ └── README.md └── sequence.ts ├── test ├── README.md ├── acceptance │ ├── home-page.controller.acceptance.ts │ ├── ping.controller.acceptance.ts │ └── test-helper.ts └── mocha.opts ├── tsconfig.json ├── tslint.build.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | # Generated apidocs 63 | api-docs/ 64 | 65 | # Transpiled JavaScript files from Typescript 66 | /dist 67 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.json 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [80], 3 | "editor.tabCompletion": "on", 4 | "editor.tabSize": 2, 5 | "editor.trimAutoWhitespace": true, 6 | "editor.formatOnSave": true, 7 | 8 | "files.exclude": { 9 | "**/.DS_Store": true, 10 | "**/.git": true, 11 | "**/.hg": true, 12 | "**/.svn": true, 13 | "**/CVS": true, 14 | "dist": true, 15 | }, 16 | "files.insertFinalNewline": true, 17 | "files.trimTrailingWhitespace": true, 18 | 19 | "tslint.ignoreDefinitionFiles": true, 20 | "typescript.tsdk": "./node_modules/typescript/lib" 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Watch and Compile Project", 8 | "type": "shell", 9 | "command": "npm", 10 | "args": ["--silent", "run", "build:watch"], 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | }, 15 | "problemMatcher": "$tsc-watch" 16 | }, 17 | { 18 | "label": "Build, Test and Lint", 19 | "type": "shell", 20 | "command": "npm", 21 | "args": ["--silent", "run", "test:dev"], 22 | "group": { 23 | "kind": "test", 24 | "isDefault": true 25 | }, 26 | "problemMatcher": ["$tsc", "$tslint5"] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | LoopBack, as member project of the OpenJS Foundation, use 4 | [Contributor Covenant v2.0](https://contributor-covenant.org/version/2/0/code_of_conduct) 5 | as their code of conduct. The full text is included 6 | [below](#contributor-covenant-code-of-conduct-v2.0) in English, and translations 7 | are available from the Contributor Covenant organisation: 8 | 9 | - [contributor-covenant.org/translations](https://www.contributor-covenant.org/translations) 10 | - [github.com/ContributorCovenant](https://github.com/ContributorCovenant/contributor_covenant/tree/release/content/version/2/0) 11 | 12 | Refer to the sections on reporting and escalation in this document for the 13 | specific emails that can be used to report and escalate issues. 14 | 15 | ## Reporting 16 | 17 | ### Project Spaces 18 | 19 | For reporting issues in spaces related to LoopBack, please use the email 20 | `tsc@loopback.io`. The LoopBack Technical Steering Committee (TSC) handles CoC issues related to the spaces that it 21 | maintains. The project TSC commits to: 22 | 23 | - maintain the confidentiality with regard to the reporter of an incident 24 | - to participate in the path for escalation as outlined in the section on 25 | Escalation when required. 26 | 27 | ### Foundation Spaces 28 | 29 | For reporting issues in spaces managed by the OpenJS Foundation, for example, 30 | repositories within the OpenJS organization, use the email 31 | `report@lists.openjsf.org`. The Cross Project Council (CPC) is responsible for 32 | managing these reports and commits to: 33 | 34 | - maintain the confidentiality with regard to the reporter of an incident 35 | - to participate in the path for escalation as outlined in the section on 36 | Escalation when required. 37 | 38 | ## Escalation 39 | 40 | The OpenJS Foundation maintains a Code of Conduct Panel (CoCP). This is a 41 | foundation-wide team established to manage escalation when a reporter believes 42 | that a report to a member project or the CPC has not been properly handled. In 43 | order to escalate to the CoCP send an email to 44 | `coc-escalation@lists.openjsf.org`. 45 | 46 | For more information, refer to the full 47 | [Code of Conduct governance document](https://github.com/openjs-foundation/cross-project-council/blob/HEAD/CODE_OF_CONDUCT.md). 48 | 49 | --- 50 | 51 | ## Contributor Covenant Code of Conduct v2.0 52 | 53 | ## Our Pledge 54 | 55 | We as members, contributors, and leaders pledge to make participation in our 56 | community a harassment-free experience for everyone, regardless of age, body 57 | size, visible or invisible disability, ethnicity, sex characteristics, gender 58 | identity and expression, level of experience, education, socio-economic status, 59 | nationality, personal appearance, race, religion, or sexual identity and 60 | orientation. 61 | 62 | We pledge to act and interact in ways that contribute to an open, welcoming, 63 | diverse, inclusive, and healthy community. 64 | 65 | ## Our Standards 66 | 67 | Examples of behavior that contributes to a positive environment for our 68 | community include: 69 | 70 | - Demonstrating empathy and kindness toward other people 71 | - Being respectful of differing opinions, viewpoints, and experiences 72 | - Giving and gracefully accepting constructive feedback 73 | - Accepting responsibility and apologizing to those affected by our mistakes, 74 | and learning from the experience 75 | - Focusing on what is best not just for us as individuals, but for the overall 76 | community 77 | 78 | Examples of unacceptable behavior include: 79 | 80 | - The use of sexualized language or imagery, and sexual attention or advances of 81 | any kind 82 | - Trolling, insulting or derogatory comments, and personal or political attacks 83 | - Public or private harassment 84 | - Publishing others' private information, such as a physical or email address, 85 | without their explicit permission 86 | - Other conduct which could reasonably be considered inappropriate in a 87 | professional setting 88 | 89 | ## Enforcement Responsibilities 90 | 91 | Community leaders are responsible for clarifying and enforcing our standards of 92 | acceptable behavior and will take appropriate and fair corrective action in 93 | response to any behavior that they deem inappropriate, threatening, offensive, 94 | or harmful. 95 | 96 | Community leaders have the right and responsibility to remove, edit, or reject 97 | comments, commits, code, wiki edits, issues, and other contributions that are 98 | not aligned to this Code of Conduct, and will communicate reasons for moderation 99 | decisions when appropriate. 100 | 101 | ## Scope 102 | 103 | This Code of Conduct applies within all community spaces, and also applies when 104 | an individual is officially representing the community in public spaces. 105 | Examples of representing our community include using an official e-mail address, 106 | posting via an official social media account, or acting as an appointed 107 | representative at an online or offline event. 108 | 109 | ## Enforcement 110 | 111 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 112 | reported to the community leaders responsible for enforcement at 113 | [tsc@loopback.io](mailto:tsc@loopback.io). All complaints will be reviewed and 114 | investigated promptly and fairly. 115 | 116 | All community leaders are obligated to respect the privacy and security of the 117 | reporter of any incident. 118 | 119 | ## Enforcement Guidelines 120 | 121 | Community leaders will follow these Community Impact Guidelines in determining 122 | the consequences for any action they deem in violation of this Code of Conduct: 123 | 124 | ### 1. Correction 125 | 126 | **Community Impact**: Use of inappropriate language or other behavior deemed 127 | unprofessional or unwelcome in the community. 128 | 129 | **Consequence**: A private, written warning from community leaders, providing 130 | clarity around the nature of the violation and an explanation of why the 131 | behavior was inappropriate. A public apology may be requested. 132 | 133 | ### 2. Warning 134 | 135 | **Community Impact**: A violation through a single incident or series of 136 | actions. 137 | 138 | **Consequence**: A warning with consequences for continued behavior. No 139 | interaction with the people involved, including unsolicited interaction with 140 | those enforcing the Code of Conduct, for a specified period of time. This 141 | includes avoiding interactions in community spaces as well as external channels 142 | like social media. Violating these terms may lead to a temporary or permanent 143 | ban. 144 | 145 | ### 3. Temporary Ban 146 | 147 | **Community Impact**: A serious violation of community standards, including 148 | sustained inappropriate behavior. 149 | 150 | **Consequence**: A temporary ban from any sort of interaction or public 151 | communication with the community for a specified period of time. No public or 152 | private interaction with the people involved, including unsolicited interaction 153 | with those enforcing the Code of Conduct, is allowed during this period. 154 | Violating these terms may lead to a permanent ban. 155 | 156 | ### 4. Permanent Ban 157 | 158 | **Community Impact**: Demonstrating a pattern of violation of community 159 | standards, including sustained inappropriate behavior, harassment of an 160 | individual, or aggression toward or disparagement of classes of individuals. 161 | 162 | **Consequence**: A permanent ban from any sort of public interaction within the 163 | community. 164 | 165 | ## Attribution 166 | 167 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 168 | version 2.0, available at 169 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 170 | 171 | Community Impact Guidelines were inspired by 172 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 173 | 174 | [homepage]: https://www.contributor-covenant.org 175 | 176 | For answers to common questions about this code of conduct, see the FAQ at 177 | https://www.contributor-covenant.org/faq. Translations are available at 178 | https://www.contributor-covenant.org/translations. 179 | -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developer's Guide 2 | 3 | We use Visual Studio Code for developing LoopBack and recommend the same to our 4 | users. 5 | 6 | ## VSCode setup 7 | 8 | Install the following extensions: 9 | 10 | - [tslint](https://marketplace.visualstudio.com/items?itemName=eg2.tslint) 11 | - [prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 12 | 13 | ## Development workflow 14 | 15 | ### Visual Studio Code 16 | 17 | 1. Start the build task (Cmd+Shift+B) to run TypeScript compiler in the 18 | background, watching and recompiling files as you change them. Compilation 19 | errors will be shown in the VSCode's "PROBLEMS" window. 20 | 21 | 2. Execute "Run Rest Task" from the Command Palette (Cmd+Shift+P) to re-run the 22 | test suite and lint the code for both programming and style errors. Linting 23 | errors will be shown in VSCode's "PROBLEMS" window. Failed tests are printed 24 | to terminal output only. 25 | 26 | ### Other editors/IDEs 27 | 28 | 1. Open a new terminal window/tab and start the continous build process via 29 | `npm run build:watch`. It will run TypeScript compiler in watch mode, 30 | recompiling files as you change them. Any compilation errors will be printed 31 | to the terminal. 32 | 33 | 2. In your main terminal window/tab, run `npm run test:dev` to re-run the test 34 | suite and lint the code for both programming and style errors. You should run 35 | this command manually whenever you have new changes to test. Test failures 36 | and linter errors will be printed to the terminal. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 StrongLoop and IBM API Connect 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loopback4-example-kafka 2 | 3 | A LoopBack 4 example application for Kafka integration. 4 | 5 | See related issues: 6 | 7 | - https://github.com/strongloop/loopback-next/issues/1925 8 | - https://github.com/strongloop/loopback-next/issues/1884 9 | 10 | ## Getting started 11 | 12 | ### Start a Kafka instance 13 | 14 | ```sh 15 | docker-compose up 16 | ``` 17 | 18 | ### Start the application 19 | 20 | ```sh 21 | npm start 22 | ``` 23 | 24 | Now you can try out on http://localhost:3000/explorer. 25 | 26 | To use `curl`: 27 | 28 | - Create new topics 29 | 30 | ```sh 31 | curl -X POST "http://127.0.0.1:3000/topics" -H "accept: */*" -H "Content-Type: application/json" -d "[\"demo\"]" 32 | ``` 33 | 34 | - Publish messages to `demo` topic: 35 | 36 | ```sh 37 | curl -X POST "http://127.0.0.1:3000/topics/demo/messages" -H "accept: */*" -H "Content-Type: application/json" -d "[\"test messsage\"]" 38 | ``` 39 | 40 | - Receive messages from `demo` topic: 41 | 42 | ```sh 43 | curl -X GET "http://127.0.0.1:3000/topics/demo/messages?limit=3" -H "accept: */*" 44 | ``` 45 | 46 | [![LoopBack]()](http://loopback.io/) 47 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Security advisories 4 | 5 | Security advisories can be found on the 6 | [LoopBack website](https://loopback.io/doc/en/sec/index.html). 7 | 8 | ## Reporting a vulnerability 9 | 10 | If you think you have discovered a new security issue with any LoopBack package, 11 | **please do not report it on GitHub**. Instead, send an email to 12 | [security@loopback.io](mailto:security@loopback.io) with the following details: 13 | 14 | - Full description of the vulnerability. 15 | - Steps to reproduce the issue. 16 | - Possible solutions. 17 | 18 | If you are sending us any logs as part of the report, then make sure to redact 19 | any sensitive data from them. 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | zookeeper: 4 | image: wurstmeister/zookeeper 5 | ports: 6 | - '2181:2181' 7 | kafka: 8 | image: wurstmeister/kafka 9 | ports: 10 | - '9092:9092' 11 | environment: 12 | KAFKA_ADVERTISED_HOST_NAME: localhost 13 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 14 | volumes: 15 | - /var/run/docker.sock:/var/run/docker.sock 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const application = require('./dist'); 2 | 3 | module.exports = application; 4 | 5 | if (require.main === module) { 6 | // Run the application 7 | const config = { 8 | rest: { 9 | port: +process.env.PORT || 3000, 10 | host: process.env.HOST || 'localhost', 11 | openApiSpec: { 12 | // useful when used with OASGraph to locate your application 13 | setServersFromRequest: true, 14 | }, 15 | }, 16 | }; 17 | application.main(config).catch(err => { 18 | console.error('Cannot start the application.', err); 19 | process.exit(1); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback4-example-kafka", 3 | "version": "1.0.0", 4 | "description": "loopback4-example-kafka", 5 | "keywords": [ 6 | "loopback-application", 7 | "loopback" 8 | ], 9 | "main": "index.js", 10 | "engines": { 11 | "node": ">=8.9" 12 | }, 13 | "scripts": { 14 | "build:apidocs": "lb-apidocs", 15 | "build": "lb-tsc es2017 --outDir dist", 16 | "build:watch": "lb-tsc --watch", 17 | "clean": "lb-clean dist", 18 | "lint": "npm run prettier:check && npm run tslint", 19 | "lint:fix": "npm run tslint:fix && npm run prettier:fix", 20 | "prettier:cli": "lb-prettier \"**/*.ts\" \"**/*.js\"", 21 | "prettier:check": "npm run prettier:cli -- -l", 22 | "prettier:fix": "npm run prettier:cli -- --write", 23 | "tslint": "lb-tslint", 24 | "tslint:fix": "npm run tslint -- --fix", 25 | "pretest": "npm run clean && npm run build", 26 | "test": "lb-mocha --allow-console-logs \"dist/test\"", 27 | "posttest": "npm run lint", 28 | "test:dev": "lb-mocha --allow-console-logs dist/test/**/*.js && npm run posttest", 29 | "prestart": "npm run build", 30 | "start": "node .", 31 | "prepublishOnly": "npm run test" 32 | }, 33 | "repository": { 34 | "type": "git" 35 | }, 36 | "author": "", 37 | "license": "", 38 | "files": [ 39 | "README.md", 40 | "index.js", 41 | "index.d.ts", 42 | "dist/src", 43 | "dist/index*", 44 | "src" 45 | ], 46 | "dependencies": { 47 | "@loopback/boot": "^1.0.2", 48 | "@loopback/context": "^1.0.0", 49 | "@loopback/core": "^1.0.0", 50 | "@loopback/openapi-v3": "^1.0.2", 51 | "@loopback/repository": "^1.0.2", 52 | "@loopback/rest": "^1.1.0", 53 | "@loopback/service-proxy": "^1.0.0", 54 | "kafka-node": "^3.0.1", 55 | "uuid": "^3.3.2" 56 | }, 57 | "devDependencies": { 58 | "@loopback/build": "^1.0.0", 59 | "@loopback/testlab": "^1.0.0", 60 | "@types/kafka-node": "^2.0.7", 61 | "@types/node": "^10.11.2", 62 | "@types/uuid": "^3.4.4" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | loopback4-example-kafka 6 | 7 | 8 | 9 | 10 | 11 | 12 | 55 | 56 | 57 | 58 |
59 |

loopback4-example-kafka

60 |

Version 1.0.0

61 | 62 |

OpenAPI spec: /openapi.json

63 |

API Explorer: /explorer

64 |
65 | 66 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/application.ts: -------------------------------------------------------------------------------- 1 | import {BootMixin} from '@loopback/boot'; 2 | import {ApplicationConfig} from '@loopback/core'; 3 | import {RepositoryMixin} from '@loopback/repository'; 4 | import {RestApplication} from '@loopback/rest'; 5 | import {ServiceMixin} from '@loopback/service-proxy'; 6 | import {MySequence} from './sequence'; 7 | 8 | export class KafkaDemoApplication extends BootMixin( 9 | ServiceMixin(RepositoryMixin(RestApplication)), 10 | ) { 11 | constructor(options: ApplicationConfig = {}) { 12 | super(options); 13 | 14 | // Set up the custom sequence 15 | this.sequence(MySequence); 16 | 17 | this.projectRoot = __dirname; 18 | // Customize @loopback/boot Booter Conventions here 19 | this.bootOptions = { 20 | controllers: { 21 | // Customize ControllerBooter Conventions here 22 | dirs: ['controllers'], 23 | extensions: ['.controller.js'], 24 | nested: true, 25 | }, 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/controllers/README.md: -------------------------------------------------------------------------------- 1 | # Controllers 2 | 3 | This directory contains source files for the controllers exported by this app. 4 | 5 | To add a new empty controller, type in `lb4 controller []` from the 6 | command-line of your application's root directory. 7 | 8 | For more information, please visit 9 | [Controller generator](http://loopback.io/doc/en/lb4/Controller-generator.html). 10 | -------------------------------------------------------------------------------- /src/controllers/home-page.controller.ts: -------------------------------------------------------------------------------- 1 | import {get} from '@loopback/openapi-v3'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import {inject} from '@loopback/context'; 5 | import {RestBindings, Response} from '@loopback/rest'; 6 | 7 | export class HomePageController { 8 | private html: string; 9 | constructor(@inject(RestBindings.Http.RESPONSE) private response: Response) { 10 | this.html = fs.readFileSync( 11 | path.join(__dirname, '../../../public/index.html'), 12 | 'utf-8', 13 | ); 14 | } 15 | 16 | @get('/', { 17 | responses: { 18 | '200': { 19 | description: 'Home Page', 20 | content: {'text/html': {schema: {type: 'string'}}}, 21 | }, 22 | }, 23 | }) 24 | homePage() { 25 | this.response 26 | .status(200) 27 | .contentType('html') 28 | .send(this.html); 29 | return this.response; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ping.controller'; 2 | export * from './kafka-demo.controller'; 3 | -------------------------------------------------------------------------------- /src/controllers/kafka-demo.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | KafkaClient, 3 | HighLevelProducer, 4 | ProduceRequest, 5 | ConsumerGroup, 6 | } from 'kafka-node'; 7 | 8 | import { 9 | post, 10 | param, 11 | requestBody, 12 | get, 13 | RestBindings, 14 | Response, 15 | HttpErrors, 16 | del, 17 | } from '@loopback/rest'; 18 | import {inject} from '@loopback/context'; 19 | import uuid = require('uuid'); 20 | 21 | //tslint:disable:no-any 22 | 23 | const KAFKA_HOST = 'localhost:9092'; 24 | 25 | type FromOffset = 'earliest' | 'latest' | 'none'; 26 | interface SubscriptionRequest { 27 | topics: string[]; 28 | groupId: string; 29 | fromOffset: FromOffset; 30 | } 31 | 32 | const consumers: {[id: string]: ConsumerGroup} = {}; 33 | 34 | export class KafkaDemoController { 35 | private client: KafkaClient; 36 | private producer: HighLevelProducer; 37 | 38 | constructor( 39 | @inject('kafka.host', {optional: true}) 40 | private kafkaHost: string = KAFKA_HOST, 41 | ) { 42 | this.client = new KafkaClient({kafkaHost}); 43 | this.producer = new HighLevelProducer(this.client, {}); 44 | } 45 | 46 | /** 47 | * Wait for the producer to be ready 48 | */ 49 | private isProducerReady() { 50 | return new Promise((resolve, reject) => { 51 | this.producer.on('ready', () => resolve()); 52 | this.producer.on('error', err => reject(err)); 53 | }); 54 | } 55 | 56 | private createConsumer( 57 | groupId: string, 58 | topics: string[], 59 | fromOffset: 'earliest' | 'latest' | 'none' = 'latest', 60 | clientId = uuid.v4(), 61 | ) { 62 | const consumer = new ConsumerGroup( 63 | { 64 | kafkaHost: this.kafkaHost, 65 | groupId: groupId || 'KafkaDemoController', 66 | fromOffset, 67 | id: clientId, 68 | }, 69 | topics, 70 | ); 71 | consumers[clientId] = consumer; 72 | return consumer; 73 | } 74 | 75 | private getConsumer(clientId: string) { 76 | return consumers[clientId]; 77 | } 78 | 79 | private writeMessageToResponse( 80 | consumer: ConsumerGroup, 81 | limit: number, 82 | response: Response, 83 | ) { 84 | let count = 0; 85 | consumer.on('message', message => { 86 | count++; 87 | response.write(`id: ${message.offset}\n`); 88 | response.write('event: message\n'); 89 | response.write(`data: ${JSON.stringify(message)}\n`); 90 | if (count >= limit) { 91 | response.end(); 92 | } 93 | consumer.close(err => { 94 | if (err) 95 | console.log( 96 | 'Something is wrong when closing the consumer.', 97 | err.message, 98 | ); 99 | }); 100 | }); 101 | return response; 102 | } 103 | 104 | /** 105 | * 106 | * @param topic 107 | */ 108 | @get('/consumers/{clientId}/messages', { 109 | responses: { 110 | '200': { 111 | 'text/event-stream': { 112 | schema: { 113 | type: 'string', 114 | }, 115 | }, 116 | }, 117 | }, 118 | }) 119 | async fetch( 120 | @param.path.string('clientId') clientId: string, 121 | @param.query.string('limit') limit: number, 122 | @inject(RestBindings.Http.RESPONSE) response: Response, 123 | ) { 124 | let consumer: ConsumerGroup | undefined = undefined; 125 | if (clientId) { 126 | consumer = this.getConsumer(clientId); 127 | } 128 | if (!consumer) { 129 | throw new HttpErrors.NotFound(`Consumer ${clientId} does not exist`); 130 | } 131 | limit = +limit || 5; 132 | response.setHeader('Cache-Control', 'no-cache'); 133 | response.contentType('text/event-stream'); 134 | 135 | this.writeMessageToResponse(consumer, limit, response); 136 | return response; 137 | } 138 | 139 | /** 140 | * 141 | * @param topic 142 | */ 143 | @post('/consumers', { 144 | responses: { 145 | '200': { 146 | 'application/json': { 147 | schema: { 148 | type: 'object', 149 | properties: { 150 | clientId: {type: 'string'}, 151 | }, 152 | }, 153 | }, 154 | }, 155 | }, 156 | }) 157 | subscribe( 158 | @requestBody({ 159 | content: { 160 | 'application/json': { 161 | schema: { 162 | type: 'object', 163 | properties: { 164 | topics: {type: 'array', items: {type: 'string'}}, 165 | fromOffset: {type: 'string'}, 166 | groupId: {type: 'string'}, 167 | }, 168 | }, 169 | }, 170 | }, 171 | }) 172 | body: SubscriptionRequest, 173 | @inject(RestBindings.Http.RESPONSE) response: Response, 174 | ) { 175 | const consumer = this.createConsumer( 176 | body.groupId || 'KafkaDemoController', 177 | body.topics, 178 | body.fromOffset, 179 | ); 180 | const client = consumer.client as any; 181 | return { 182 | clientId: client.clientId, 183 | }; 184 | } 185 | 186 | @del('/consumers/{clientId}') 187 | async deleteConsumer(@param.path.string('clientId') clientId: string) { 188 | const consumer = this.getConsumer(clientId); 189 | if (consumer) { 190 | delete consumers[clientId]; 191 | return new Promise((resolve, reject) => { 192 | consumer.close(false, err => { 193 | if (err) reject(err); 194 | else resolve(); 195 | }); 196 | }); 197 | } else { 198 | throw new HttpErrors.NotFound(`Consumer ${clientId} does not exist`); 199 | } 200 | } 201 | 202 | /** 203 | * Create topics 204 | * @param topics 205 | */ 206 | @post('/topics') 207 | async createTopics( 208 | @requestBody({ 209 | content: { 210 | 'application/json': { 211 | schema: { 212 | type: 'array', 213 | items: {type: 'string'}, 214 | }, 215 | }, 216 | }, 217 | }) 218 | topics: string[], 219 | ) { 220 | await this.isProducerReady(); 221 | return new Promise((resolve, reject) => { 222 | this.producer.createTopics(topics, true, (err, data) => { 223 | if (err) reject(err); 224 | else resolve(data); 225 | }); 226 | }); 227 | } 228 | 229 | /** 230 | * Publish a message to the given topic 231 | * @param topic The topic name 232 | * @param message The message 233 | */ 234 | @post('/topics/{topic}/messages') 235 | async publish( 236 | @param.path.string('topic') topic: string, 237 | @requestBody({ 238 | content: { 239 | 'application/json': { 240 | schema: { 241 | type: 'array', 242 | items: {type: 'string'}, 243 | }, 244 | }, 245 | }, 246 | }) 247 | messages: string[], 248 | ) { 249 | await this.isProducerReady(); 250 | const req: ProduceRequest = {topic, messages}; 251 | return new Promise((resolve, reject) => { 252 | this.producer.send([req], (err, data) => { 253 | if (err) reject(err); 254 | else resolve(data); 255 | }); 256 | }); 257 | } 258 | 259 | /** 260 | * Consume messages of a given topic 261 | * @param topic The topic name 262 | */ 263 | @get('/topics/{topic}/messages', { 264 | responses: { 265 | '200': { 266 | 'text/event-stream': { 267 | schema: { 268 | type: 'string', 269 | }, 270 | }, 271 | }, 272 | }, 273 | }) 274 | async consumeMessagesOnTopics( 275 | @param.path.string('topic') topic: string, 276 | @param.query.string('limit') limit: number, 277 | @inject(RestBindings.Http.RESPONSE) response: Response, 278 | ) { 279 | let consumer = this.createConsumer('', [topic], 'none'); 280 | 281 | limit = +limit || 5; 282 | response.setHeader('Cache-Control', 'no-cache'); 283 | response.contentType('text/event-stream'); 284 | 285 | return this.writeMessageToResponse(consumer, limit, response); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/controllers/ping.controller.ts: -------------------------------------------------------------------------------- 1 | import {Request, RestBindings, get, ResponseObject} from '@loopback/rest'; 2 | import {inject} from '@loopback/context'; 3 | 4 | /** 5 | * OpenAPI response for ping() 6 | */ 7 | const PING_RESPONSE: ResponseObject = { 8 | description: 'Ping Response', 9 | content: { 10 | 'application/json': { 11 | schema: { 12 | type: 'object', 13 | properties: { 14 | greeting: {type: 'string'}, 15 | date: {type: 'string'}, 16 | url: {type: 'string'}, 17 | headers: { 18 | type: 'object', 19 | properties: { 20 | 'Content-Type': {type: 'string'}, 21 | }, 22 | additionalProperties: true, 23 | }, 24 | }, 25 | }, 26 | }, 27 | }, 28 | }; 29 | 30 | /** 31 | * A simple controller to bounce back http requests 32 | */ 33 | export class PingController { 34 | constructor(@inject(RestBindings.Http.REQUEST) private req: Request) {} 35 | 36 | // Map to `GET /ping` 37 | @get('/ping', { 38 | responses: { 39 | '200': PING_RESPONSE, 40 | }, 41 | }) 42 | ping(): object { 43 | // Reply with a greeting, the current time, the url, and request headers 44 | return { 45 | greeting: 'Hello from LoopBack', 46 | date: new Date(), 47 | url: this.req.url, 48 | headers: Object.assign({}, this.req.headers), 49 | }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/datasources/README.md: -------------------------------------------------------------------------------- 1 | # Datasources 2 | 3 | This directory contains config for datasources used by this app. 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {KafkaDemoApplication} from './application'; 2 | import {ApplicationConfig} from '@loopback/core'; 3 | 4 | export {KafkaDemoApplication}; 5 | 6 | export async function main(options: ApplicationConfig = {}) { 7 | const app = new KafkaDemoApplication(options); 8 | await app.boot(); 9 | await app.start(); 10 | 11 | const url = app.restServer.url; 12 | console.log(`Server is running at ${url}`); 13 | console.log(`Try ${url}/ping`); 14 | 15 | return app; 16 | } 17 | -------------------------------------------------------------------------------- /src/models/README.md: -------------------------------------------------------------------------------- 1 | # Models 2 | 3 | This directory contains code for models provided by this app. 4 | -------------------------------------------------------------------------------- /src/repositories/README.md: -------------------------------------------------------------------------------- 1 | # Repositories 2 | 3 | This directory contains code for repositories provided by this app. 4 | -------------------------------------------------------------------------------- /src/sequence.ts: -------------------------------------------------------------------------------- 1 | import {inject} from '@loopback/context'; 2 | import { 3 | FindRoute, 4 | InvokeMethod, 5 | ParseParams, 6 | Reject, 7 | RequestContext, 8 | RestBindings, 9 | Send, 10 | SequenceHandler, 11 | } from '@loopback/rest'; 12 | 13 | const SequenceActions = RestBindings.SequenceActions; 14 | 15 | export class MySequence implements SequenceHandler { 16 | constructor( 17 | @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, 18 | @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, 19 | @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, 20 | @inject(SequenceActions.SEND) public send: Send, 21 | @inject(SequenceActions.REJECT) public reject: Reject, 22 | ) {} 23 | 24 | async handle(context: RequestContext) { 25 | try { 26 | const {request, response} = context; 27 | const route = this.findRoute(request); 28 | const args = await this.parseParams(request, route); 29 | const result = await this.invoke(route, args); 30 | this.send(response, result); 31 | } catch (err) { 32 | this.reject(context, err); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | Please place your tests in this folder. 4 | -------------------------------------------------------------------------------- /test/acceptance/home-page.controller.acceptance.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018. All Rights Reserved. 2 | // Node module: @loopback/example-shopping 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | import {Client} from '@loopback/testlab'; 7 | import {KafkaDemoApplication} from '../..'; 8 | import {setupApplication} from './test-helper'; 9 | 10 | describe('HomePageController', () => { 11 | let app: KafkaDemoApplication; 12 | let client: Client; 13 | 14 | before('setupApplication', async () => { 15 | ({app, client} = await setupApplication()); 16 | }); 17 | 18 | after(async () => { 19 | await app.stop(); 20 | }); 21 | 22 | it('exposes a default home page', async () => { 23 | await client 24 | .get('/') 25 | .expect(200) 26 | .expect('Content-Type', /text\/html/); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/acceptance/ping.controller.acceptance.ts: -------------------------------------------------------------------------------- 1 | import {Client, expect} from '@loopback/testlab'; 2 | import {KafkaDemoApplication} from '../..'; 3 | import {setupApplication} from './test-helper'; 4 | 5 | describe('PingController', () => { 6 | let app: KafkaDemoApplication; 7 | let client: Client; 8 | 9 | before('setupApplication', async () => { 10 | ({app, client} = await setupApplication()); 11 | }); 12 | 13 | after(async () => { 14 | await app.stop(); 15 | }); 16 | 17 | it('invokes GET /ping', async () => { 18 | const res = await client.get('/ping?msg=world').expect(200); 19 | expect(res.body).to.containEql({greeting: 'Hello from LoopBack'}); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/acceptance/test-helper.ts: -------------------------------------------------------------------------------- 1 | import {KafkaDemoApplication} from '../..'; 2 | import { 3 | createRestAppClient, 4 | givenHttpServerConfig, 5 | Client, 6 | } from '@loopback/testlab'; 7 | 8 | export async function setupApplication(): Promise { 9 | const app = new KafkaDemoApplication({ 10 | rest: givenHttpServerConfig(), 11 | }); 12 | 13 | await app.boot(); 14 | await app.start(); 15 | 16 | const client = createRestAppClient(app); 17 | 18 | return {app, client}; 19 | } 20 | 21 | export interface AppWithClient { 22 | app: KafkaDemoApplication; 23 | client: Client; 24 | } 25 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --require source-map-support/register 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "extends": "./node_modules/@loopback/build/config/tsconfig.common.json", 4 | "include": [ 5 | "src", 6 | "test", 7 | "index.ts" 8 | ], 9 | "exclude": [ 10 | "node_modules/**", 11 | "packages/*/node_modules/**", 12 | "**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tslint.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tslint", 3 | "extends": ["./node_modules/@loopback/build/config/tslint.build.json"] 4 | } 5 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tslint", 3 | "extends": ["./node_modules/@loopback/build/config/tslint.common.json"] 4 | } 5 | --------------------------------------------------------------------------------