├── .gitignore ├── LICENSE.txt ├── README.md ├── caching-service ├── Dockerfile ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── redis │ │ └── demos │ │ └── services │ │ └── caching │ │ ├── CachingServiceApplication.java │ │ ├── RestStatusController.java │ │ ├── config │ │ └── ConnectorConfig.java │ │ ├── listener │ │ └── CDCEventListener.java │ │ └── service │ │ ├── RedisCacheService.java │ │ └── WebServiceCachingService.java │ └── resources │ ├── application-prod.properties │ └── application.properties ├── comments-service ├── Dockerfile ├── README.md ├── commentsService.js ├── config.dev.json ├── config.prod.json ├── package-lock.json ├── package.json └── server.js ├── db-to-streams-service ├── Dockerfile ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── redis │ │ └── demos │ │ └── services │ │ └── db │ │ └── events │ │ └── streams │ │ ├── RedisStreamsCDCPublisher.java │ │ ├── RestStatusController.java │ │ ├── config │ │ └── ConnectorConfig.java │ │ ├── listener │ │ └── CDCEventListener.java │ │ └── service │ │ └── RedisStreamsDebeziumProducer.java │ └── resources │ ├── application-prod.properties │ └── application.properties ├── docker-compose.yml ├── docs └── demoscript │ ├── 01-introduction.md │ ├── 02-configuration-with-redis.md │ ├── 03-caching-web-service-calls.md │ ├── 04-extending-your-application-comments.md │ ├── 05-cdc-with-debezium-and-streams.md │ ├── 06-consume-streams-to-redish-hash.md │ └── 07-consume-streams-to-redish-graph.md ├── kubernetes ├── README.md ├── redis-demo-application-deployment.yaml ├── redis-demo-cluster-crd.yaml └── redis-demo-movie-database-crd.yaml ├── mysql-database ├── Dockerfile ├── import-data.sql └── mysql.cnf ├── notifications-service-node ├── Dockerfile ├── config.dev.json ├── config.prod.json ├── package-lock.json ├── package.json └── server.js ├── pom.xml ├── redis-gears-recipes └── movies-to-pubsub.py ├── redis-server └── Dockerfile ├── sql-rest-api ├── Dockerfile ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── redis │ │ └── demos │ │ └── debezium │ │ └── sql │ │ ├── Application.java │ │ ├── controllers │ │ ├── ActorsAPIController.java │ │ ├── MoviesAPIController.java │ │ └── RestStatusController.java │ │ └── services │ │ └── DataGeneratorService.java │ └── resources │ ├── application-prod.properties │ └── application.properties ├── streams-to-redisearch-service ├── Dockerfile ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── redis │ │ └── demos │ │ └── services │ │ └── search │ │ └── service │ │ ├── RestStatusController.java │ │ ├── SearchServiceApplication.java │ │ ├── StreamsToRediSearch.java │ │ ├── schemas │ │ ├── ActorsSchema.java │ │ ├── CommentsSchema.java │ │ ├── KeysPrefix.java │ │ └── MoviesSchema.java │ │ └── util │ │ └── AutoCompleter.java │ └── resources │ ├── application-prod.properties │ └── application.properties ├── streams-to-redisgraph-service ├── Dockerfile ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── redis │ │ └── demos │ │ └── services │ │ └── graph │ │ └── service │ │ ├── GraphServiceApplication.java │ │ ├── RedisStreamToGraphService.java │ │ └── RestStatusController.java │ └── resources │ ├── application-prod.properties │ └── application.properties └── ui-redis-front-end └── redis-front ├── .gitignore ├── Dockerfile ├── README.md ├── babel.config.js ├── cypress.json ├── nginx.conf ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── imgs │ ├── autocomplete-service.png │ ├── cdc-service-1.png │ ├── cdc-service-2.png │ ├── forkme_left_red.svg │ ├── gears-pubsub.png │ ├── graph-service.png │ ├── overal-archi.png │ ├── rdbms-service.png │ ├── redis-logo-red-white-rgb.png │ ├── redis-logo.svg │ ├── redis.svg │ ├── redislabs.png │ └── ws-caching.png ├── index.html └── logo.png ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ ├── ActorList.vue │ ├── ActorMainComponent.vue │ ├── Comments.vue │ ├── GenericObjectList.vue │ ├── MovieList.vue │ ├── ServicesConfiguration.vue │ └── ServicesParent.vue ├── main.ts ├── repositories │ ├── CacheInvalidatorRepository.js │ ├── CommentsRepository.js │ ├── DataStreamsProducerRepository.js │ ├── DataStreamsRedisHashSyncRepository.js │ ├── DataStreamsToGraphRepository.js │ ├── RDBMSRepository.js │ ├── Repository.js │ └── RepositoryFactory.js ├── router │ └── index.ts ├── shims-tsx.d.ts ├── shims-vue.d.ts └── views │ ├── About.vue │ ├── ActorForm.vue │ ├── Actors.vue │ ├── Aggregations.vue │ ├── Autocomplete.vue │ ├── Home.vue │ ├── MovieForm.vue │ ├── Movies.vue │ ├── Posts.vue │ ├── SearchActors.vue │ ├── SearchMovies.vue │ ├── SearchMoviesFaceted.vue │ ├── Services.vue │ └── Statistics.vue ├── tests ├── e2e │ ├── .eslintrc.js │ ├── plugins │ │ └── index.js │ ├── specs │ │ └── test.js │ └── support │ │ ├── commands.js │ │ └── index.js └── unit │ └── example.spec.ts ├── tsconfig.json └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ## Intellij 3 | .idea/ 4 | *.iml 5 | .classpath 6 | 7 | # Compiled class file 8 | *.class 9 | 10 | # Log file 11 | *.log 12 | 13 | # BlueJ files 14 | *.ctxt 15 | 16 | # Mobile Tools for Java (J2ME) 17 | .mtj.tmp/ 18 | 19 | # Package Files # 20 | *.jar 21 | *.war 22 | *.nar 23 | *.ear 24 | *.zip 25 | *.tar.gz 26 | *.rar 27 | 28 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 29 | hs_err_pid* 30 | 31 | # datafile 32 | *.dat 33 | 34 | # Logs 35 | logs/ 36 | 37 | # Maven 38 | target/ 39 | pom.xml.tag 40 | pom.xml.releaseBackup 41 | pom.xml.versionsBackup 42 | pom.xml.next 43 | release.properties 44 | dependency-reduced-pom.xml 45 | buildNumber.properties 46 | .mvn/timing.properties 47 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 48 | .mvn/wrapper/maven-wrapper.jar 49 | 50 | .metadata 51 | bin/ 52 | tmp/ 53 | *.tmp 54 | *.bak 55 | *.swp 56 | *~.nib 57 | local.properties 58 | .settings/ 59 | .loadpath 60 | .recommenders 61 | 62 | # External tool builders 63 | .externalToolBuilders/ 64 | 65 | # Locally stored "Eclipse launch configurations" 66 | *.launch 67 | 68 | # PyDev specific (Python IDE for Eclipse) 69 | *.pydevproject 70 | 71 | # CDT-specific (C/C++ Development Tooling) 72 | .cproject 73 | 74 | # CDT- autotools 75 | .autotools 76 | 77 | # Java annotation processor (APT) 78 | .factorypath 79 | 80 | # PDT-specific (PHP Development Tools) 81 | .buildpath 82 | 83 | # sbteclipse plugin 84 | .target 85 | 86 | # Tern plugin 87 | .tern-project 88 | 89 | # TeXlipse plugin 90 | .texlipse 91 | 92 | # STS (Spring Tool Suite) 93 | .springBeans 94 | 95 | # Code Recommenders 96 | .recommenders/ 97 | 98 | # Annotation Processing 99 | .apt_generated/ 100 | .apt_generated_test/ 101 | 102 | # Scala IDE specific (Scala & Java development for Eclipse) 103 | .cache-main 104 | .scala_dependencies 105 | .worksheet 106 | .project 107 | 108 | # Vue JS 109 | .DS_Store 110 | node_modules/ 111 | /dist/ 112 | 113 | # local env files 114 | .env.local 115 | .env.*.local 116 | 117 | # Log files 118 | npm-debug.log* 119 | yarn-debug.log* 120 | yarn-error.log* 121 | 122 | # Editor directories and files 123 | .idea 124 | .vscode 125 | *.suo 126 | *.ntvs* 127 | *.njsproj 128 | *.sln 129 | *.sw* 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microservices with Redis 2 | 3 | This project shows how you can modernize a legacy application that use RDBMS with Redis. 4 | 5 | * Caching: Call Web Service and cache the result in Redis 6 | * CDC to RediStreams: Capture MySQL transactionsd and send them to Redis Streams 7 | * Use RediSearch to index relational data and provides autocomplete feature 8 | * Store and Index data in Redis: copy the events from Streams and put them in a Redis Hash (Movie/Actors), this could be used as a cache or maine datastore 9 | * See how to run queries on Redis hashes using RediSearch commands (filter, sort, aggregate, and tull text search) 10 | * Extend MySQL Legacy model with movie comments store in Hash and queried using RediSearch commands 11 | * Push relationnoal data into RedisGraph 12 | * a Web frontend developped with Vue.js 13 | 14 | ![Archi](./ui-redis-front-end/redis-front/public/imgs/overal-archi.png) 15 | 16 | 17 | If you want to use the Web Service cache demo that call the OMDB API you must: 18 | 19 | 1. Generate a key here: [http://www.omdbapi.com/](http://www.omdbapi.com/) *do not forge to activate it, you will receive an email) 20 | 21 | 2. When the applications is ready go to the "Services" page and enter the key in the configuration screen, this will save the key in a Redis Hash (lool at `ms:config` during the demo) 22 | 23 | 24 | ## Build and Run with Docker 25 | 26 | 27 | ``` 28 | $ mvn clean package 29 | 30 | $ docker-compose up --build 31 | 32 | ``` 33 | 34 | Cleanup 35 | 36 | ``` 37 | 38 | $ docker-compose down -v --rmi local --remove-orphans 39 | 40 | ``` 41 | 42 | ## Deploy to Kubernetes 43 | 44 | You can also deploy the application to Kubernetes, see [Kubernetes Readme](./kubernetes/README.md) -------------------------------------------------------------------------------- /caching-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | 3 | COPY target/*.jar /app.jar 4 | 5 | ENV REDIS_HOST=redis-service 6 | ENV REDIS_PORT=6379 7 | ENV REDIS_PASSWORD= 8 | ENV MYSQL_ROOT_PASSWORD=debezium 9 | ENV MYSQL_USER=mysqluser 10 | ENV MYSQL_PASSWORD=mysqlpw 11 | 12 | EXPOSE 8084 13 | ENTRYPOINT ["java","-jar","/app.jar","--spring.profiles.active=prod"] 14 | 15 | 16 | -------------------------------------------------------------------------------- /caching-service/README.md: -------------------------------------------------------------------------------- 1 | # Redis Cache Invalidation Service 2 | 3 | Demonstration: 4 | 5 | [![](http://img.youtube.com/vi/UbIp92_CTCQ/0.jpg)](http://www.youtube.com/watch?v=UbIp92_CTCQ "CDC in Action") 6 | 7 | The current data model and cache is really simple (and not realistic): 8 | * 1 row = 1 key in Redis 9 | 10 | Just this first release as a proof of concept. 11 | 12 | 13 | ### Building and Running the Service 14 | 15 | #### 1- Start MySQL with Debezium sample dataset 16 | 17 | Run this command to start MySQL in a Docker container. 18 | 19 | ``` 20 | $ docker run -it --rm --name mysql \ 21 | -p 3306:3306 \ 22 | -e MYSQL_ROOT_PASSWORD=debezium \ 23 | -e MYSQL_USER=mysqluser \ 24 | -e MYSQL_PASSWORD=mysqlpw \ 25 | debezium/example-mysql:1.1 26 | ``` 27 | 28 | This container is configured to support [Debezium MySQL Connector](https://debezium.io/documentation/reference/1.0/connectors/mysql.html). 29 | 30 | 31 | Connect to MySQL Command Line client: 32 | 33 | ``` 34 | $ docker run -it --rm --name mysqlterm \ 35 | --link mysql \ 36 | --rm mysql:5.7 \ 37 | sh -c 'exec mysql -h"$MYSQL_PORT_3306_TCP_ADDR" -P"$MYSQL_PORT_3306_TCP_PORT" -umysqluser -pmysqlpw' 38 | ``` 39 | 40 | and run the following command: 41 | 42 | ```json 43 | mysql> GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'mysqluser'@'%'; 44 | Query OK, 0 rows affected (0.00 sec) 45 | 46 | 47 | mysql> use inventory 48 | 49 | mysql> SELECT * FROM customers; 50 | ``` 51 | 52 | (Note: I need to check, since it should not be necessary since it is present in the [SQL script](https://github.com/debezium/docker-images/blob/master/examples/mysql/1.1/inventory.sql#L12).) 53 | 54 | #### 2- Start Redis is not already present 55 | 56 | Let's start Redis, for example with Docker: 57 | 58 | ``` 59 | $ docker run -it --rm --name redis-server \ 60 | -p 6379:6379 \ 61 | redis 62 | ``` 63 | 64 | Connect to Redis using redis-cli, Redis Insight or any other tool. For example usind Docker: 65 | 66 | ``` 67 | $ docker exec -it redis-server redis-cli 68 | ``` 69 | 70 | 71 | run the following command to check that the Redis database is empty: 72 | 73 | ``` 74 | 127.0.0.1:6379> SCAN 0 COUNT 1000 75 | ``` 76 | 77 | 78 | #### 3- Run the Cache Invalidation Service 79 | 80 | You if you are not using the default ports, and running the databases locally open 81 | the `./src/main/resources/application.properties` file and edit the properties. 82 | 83 | 84 | Run the application using the following command: 85 | 86 | ``` 87 | $ mvn spring-boot:run 88 | ``` 89 | 90 | During the initial sync the content of the `inventory.customers` and `inventory.orders` is pushed into Redis. 91 | 92 | Go back into `redis-cli`, and Scan the db again: 93 | 94 | ``` 95 | 127.0.0.1:6379> SCAN 0 COUNT 1000 96 | ``` 97 | 98 | You can look into one of the key using: 99 | 100 | ``` 101 | 127.0.0.1:6379> HGETALL inventory:customers:id:1001 102 | ``` 103 | 104 | 105 | **Update a MySQL Record** 106 | 107 | In the MySQL CLI do run this update command: 108 | 109 | ``` 110 | mysql> UPDATE customers SET email='sally@redis-demo.com' WHERE id = 1001; 111 | ``` 112 | 113 | **Check Redis** 114 | 115 | ``` 116 | 127.0.0.1:6379> HGETALL inventory:customers:id:1001 117 | ``` 118 | 119 | Email has been updated (in fact the whole Hash has been replaced) 120 | 121 | 122 | **Create a customer MySQL Record** 123 | 124 | In the MySQL CLI do run this update command: 125 | 126 | ``` 127 | mysql> INSERT INTO customers VALUES (1005, 'John', 'Doe', 'jdoe@demo.com'); 128 | ``` 129 | 130 | **Check Redis** 131 | 132 | ``` 133 | 127.0.0.1:6379> HGETALL inventory:customers:id:1005 134 | ``` 135 | 136 | You see the new customer. 137 | 138 | **Delete customer in MySQL** 139 | 140 | In the MySQL CLI do run this update command: 141 | 142 | ``` 143 | mysql> DELETE FROM customers WHERE id = 1005; 144 | ``` 145 | 146 | **Check Redis** 147 | 148 | ``` 149 | 127.0.0.1:6379> HGETALL inventory:customers:id:1005 150 | ``` 151 | The key `inventory:customers:id:1005` is not present in Redis anymore. 152 | 153 | ---- 154 | 155 | You can change the configuration to: 156 | * no save any data in Redis from the CDC Event, and only use Redis Service to delete updated/deleted keys `redis.setValue=false` 157 | * only send data when a record is updated not created `redis.setOnInsert=false` -------------------------------------------------------------------------------- /caching-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | cache-invalidation-cdc-debezium 7 | io.redis.demos 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | caching-service 13 | Spring Boot app that listen to CDC event and update Redis 14 | 15 | 16 | 17 | 18 | 19 | 20 | io.debezium 21 | debezium-api 22 | ${version.debezium} 23 | 24 | 25 | 26 | io.debezium 27 | debezium-embedded 28 | ${version.debezium} 29 | 30 | 31 | org.slf4j 32 | slf4j-log4j12 33 | 34 | 35 | 36 | 37 | 38 | 39 | io.debezium 40 | debezium-connector-mysql 41 | ${version.debezium} 42 | 43 | 44 | 45 | redis.clients 46 | jedis 47 | ${version.jedis} 48 | 49 | 50 | 51 | org.apache.httpcomponents 52 | httpclient 53 | 4.5.10 54 | 55 | 56 | 57 | com.fasterxml.jackson.core 58 | jackson-core 59 | 2.10.3 60 | 61 | 62 | 63 | com.fasterxml.jackson.core 64 | jackson-databind 65 | 2.10.5.1 66 | 67 | 68 | 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-starter-web 73 | 74 | 75 | 76 | org.projectlombok 77 | lombok 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | org.springframework.boot 87 | spring-boot-maven-plugin 88 | 89 | 90 | 91 | org.apache.maven.plugins 92 | maven-compiler-plugin 93 | 3.8.1 94 | 95 | 1.8 96 | 1.8 97 | 98 | 99 | org.projectlombok 100 | lombok 101 | 1.18.12 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /caching-service/src/main/java/io/redis/demos/services/caching/CachingServiceApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Tugdual Grall 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package io.redis.demos.services.caching; 19 | 20 | import lombok.extern.slf4j.Slf4j; 21 | import org.springframework.boot.SpringApplication; 22 | import org.springframework.boot.autoconfigure.SpringBootApplication; 23 | 24 | @Slf4j 25 | @SpringBootApplication 26 | public class CachingServiceApplication { 27 | 28 | public static void main(String[] args) { 29 | SpringApplication.run(CachingServiceApplication.class, args); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /caching-service/src/main/java/io/redis/demos/services/caching/RestStatusController.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.caching; 2 | 3 | import io.redis.demos.services.caching.listener.CDCEventListener; 4 | import io.redis.demos.services.caching.service.WebServiceCachingService; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | import javax.inject.Inject; 9 | import java.io.IOException; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | @RequestMapping({"/api/1.0/caching/", "/"}) 14 | @RestController 15 | @CrossOrigin("*") 16 | @Slf4j 17 | @org.springframework.context.annotation.Configuration 18 | public class RestStatusController { 19 | 20 | @Inject 21 | CDCEventListener cdcEventListener; 22 | 23 | @Inject WebServiceCachingService webServiceCachingService; 24 | 25 | @GetMapping("/status") 26 | public Map status() { 27 | Map result = new HashMap<>(); 28 | result.put("service", "CachingServiceApplication"); 29 | result.put("status", cdcEventListener.getState()); 30 | result.put("version", "1.0"); 31 | return result; 32 | } 33 | 34 | @GetMapping("/start") 35 | public Map start() throws IOException { 36 | Map result = new HashMap<>(); 37 | cdcEventListener.startDebezium(); 38 | result.put("service", "CachingServiceApplication"); 39 | result.put("action", "start"); 40 | return result; 41 | } 42 | 43 | @GetMapping("/stop") 44 | public Map stop() throws IOException { 45 | Map result = new HashMap<>(); 46 | cdcEventListener.stopDebezium(); 47 | result.put("service", "CachingServiceApplication"); 48 | result.put("action", "stop"); 49 | return result; 50 | } 51 | 52 | 53 | @GetMapping("/ratings/{id}") 54 | public Map getRatings( 55 | @PathVariable(name = "id") String imdbId, 56 | @RequestParam(name="cache", defaultValue = "1") String withCache) throws IOException { 57 | Map result = new HashMap<>(); 58 | 59 | Map resultWsCall = webServiceCachingService.getRatings(imdbId, withCache.equals("1")); 60 | result.putAll(resultWsCall); 61 | 62 | return result; 63 | } 64 | 65 | 66 | @PostMapping("/configuration/omdb_api") 67 | public Map saveOmdbApiKey(@RequestParam(name="key") String key) throws IOException { 68 | Map result = new HashMap<>(); 69 | webServiceCachingService.saveOMDBAPIKey(key); 70 | result.put("status", "OK"); 71 | result.put("msg", "OMDB API Key successfully saved"); 72 | return result; 73 | } 74 | 75 | @GetMapping("/configuration/omdb_api") 76 | public Map getOmdbApiKey() throws IOException { 77 | Map result = new HashMap<>(); 78 | result.put("key", webServiceCachingService.OMDB_API_KEY); 79 | result.put("value", webServiceCachingService.getOMDBAPIKey()); 80 | return result; 81 | } 82 | 83 | @GetMapping("/stats/omdb_api") 84 | public Map getOmdbApiStats() throws IOException { 85 | return webServiceCachingService.getOMDBAPIStats(); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /caching-service/src/main/java/io/redis/demos/services/caching/config/ConnectorConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Tugdual Grall 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package io.redis.demos.services.caching.config; 19 | 20 | import org.springframework.beans.factory.annotation.Value; 21 | import org.springframework.context.annotation.Bean; 22 | import org.springframework.context.annotation.Configuration; 23 | 24 | import java.util.Properties; 25 | 26 | @Configuration 27 | public class ConnectorConfig { 28 | 29 | /** 30 | * Student Database details. 31 | */ 32 | @Value("${database.hostname}") 33 | private String databaseHostname; 34 | 35 | @Value("${database.name}") 36 | private String databaseName; 37 | 38 | @Value("${database.port}") 39 | private String databasePort; 40 | 41 | @Value("${database.user}") 42 | private String databaseUser; 43 | 44 | @Value("${database.password}") 45 | private String rdbmsDBPassword; 46 | 47 | @Value("${database.password}") 48 | private String databasePassword; 49 | 50 | @Value("${database.server.id}") 51 | private String databaseServerId; 52 | 53 | @Value("${database.server.name}") 54 | private String databaseServerName; 55 | 56 | @Value("${table.whitelist}") 57 | private String tableWhitelist; 58 | 59 | 60 | @Bean 61 | public io.debezium.config.Configuration createConnectorConfig() { 62 | Properties props = new Properties(); 63 | 64 | props.setProperty("name", "cacheserviceengine"); 65 | props.setProperty("connector.class", "io.debezium.connector.mysql.MySqlConnector"); 66 | 67 | props.setProperty("database.hostname", databaseHostname); 68 | props.setProperty("database.name", databaseName); 69 | props.setProperty("database.port", databasePort); 70 | props.setProperty("database.user", databaseUser); 71 | props.setProperty("database.password", databasePassword); 72 | props.setProperty("database.server.id", databaseServerId); 73 | props.setProperty("database.server.name", databaseServerName); 74 | props.setProperty("database.history", "io.debezium.relational.history.FileDatabaseHistory"); 75 | props.setProperty("database.history.file.filename", "./dbhistory.dat"); 76 | props.setProperty("table.whitelist", tableWhitelist); 77 | 78 | props.setProperty("offset.storage", "org.apache.kafka.connect.storage.FileOffsetBackingStore"); 79 | props.setProperty("offset.storage.file.filename", "./offsets.dat"); 80 | props.setProperty("offset.flush.interval.ms", "5000"); 81 | 82 | return io.debezium.config.Configuration.from(props); 83 | } 84 | 85 | 86 | 87 | } 88 | -------------------------------------------------------------------------------- /caching-service/src/main/java/io/redis/demos/services/caching/listener/CDCEventListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Tugdual Grall 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package io.redis.demos.services.caching.listener; 19 | 20 | import io.debezium.config.Configuration; 21 | import io.debezium.data.Envelope.Operation; 22 | import static io.debezium.data.Envelope.FieldName.*; 23 | 24 | import io.debezium.embedded.Connect; 25 | import io.debezium.engine.DebeziumEngine; 26 | import lombok.extern.slf4j.Slf4j; 27 | import org.apache.commons.lang3.tuple.Pair; 28 | import org.apache.kafka.connect.data.Field; 29 | import org.apache.kafka.connect.data.Struct; 30 | import org.apache.kafka.connect.source.SourceRecord; 31 | import org.springframework.stereotype.Component; 32 | 33 | import java.io.IOException; 34 | import java.util.HashMap; 35 | import java.util.List; 36 | import java.util.Map; 37 | import java.util.Properties; 38 | import java.util.concurrent.ExecutorService; 39 | import java.util.concurrent.Executors; 40 | import java.util.stream.Collectors; 41 | 42 | import io.redis.demos.services.caching.service.RedisCacheService; 43 | 44 | import static java.util.stream.Collectors.toMap; 45 | 46 | @Slf4j 47 | @Component 48 | @org.springframework.context.annotation.Configuration 49 | public class CDCEventListener { 50 | 51 | private String topicName; 52 | private String status="STOPPED"; 53 | 54 | 55 | // Thread for the Debezium engine 56 | private ExecutorService executor = Executors.newSingleThreadExecutor(); 57 | DebeziumEngine engine = null; 58 | 59 | 60 | // Service layer to interact with Redis 61 | private final RedisCacheService redisCacheService; 62 | private final Properties configAsProperties; 63 | 64 | public CDCEventListener(Configuration config, RedisCacheService service) throws IOException { 65 | topicName = config.getString("database.server.name") +"."+ config.getString("database.name") +"."; 66 | redisCacheService = service; 67 | configAsProperties = config.asProperties(); 68 | } 69 | 70 | 71 | public void startDebezium() throws IOException { 72 | log.info("Starting Debezium...."); 73 | try (DebeziumEngine start = DebeziumEngine.create(Connect.class) 74 | .using(configAsProperties) 75 | .notifying(record -> { 76 | handleEvent(record); 77 | }).build()) 78 | { 79 | engine = start; 80 | executor = Executors.newSingleThreadExecutor(); 81 | executor.execute(engine); 82 | status = "RUNNING"; 83 | } 84 | } 85 | 86 | public void stopDebezium(){ 87 | log.info("Stopping Debezium...."); 88 | try { 89 | engine.close(); 90 | executor.shutdown(); 91 | executor = null; 92 | } catch (Exception e) { 93 | log.error( e.getMessage() ); 94 | } finally { 95 | status = "STOPPED"; 96 | } 97 | } 98 | 99 | public String getState() { 100 | return this.status; 101 | } 102 | 103 | /** 104 | * Capture CDC Event if event is from the proper DB/Table send it to @RedisCacheService 105 | * @param record 106 | */ 107 | private void handleEvent(SourceRecord record) { 108 | 109 | // check of the events is sent to the proper topic that is the server-name and database 110 | if (record != null && record.topic() != null && topicName != null && record.topic().startsWith( topicName )) { 111 | Struct payload = (Struct) record.value(); 112 | 113 | if (payload != null && payload.getString("op") != null) { 114 | String tableName = null; 115 | String id = null; 116 | String structureType = AFTER; 117 | 118 | // prepare body 119 | Operation op = Operation.forCode(payload.getString("op")); 120 | if (op == Operation.DELETE) { 121 | structureType = BEFORE; 122 | } 123 | 124 | // prepare header 125 | Struct sourcePayload = (Struct) payload.get(SOURCE); 126 | Map cdcHeader = new HashMap<>(); 127 | cdcHeader.put("source.db", sourcePayload.getString("db")); 128 | cdcHeader.put("source.table", sourcePayload.getString("table")); 129 | cdcHeader.put("source.operation", op ); 130 | 131 | List fieldNames = record.keySchema().fields().stream().map(field -> field.name()).collect(Collectors.toList()); 132 | cdcHeader.put("source.key.fields", fieldNames); 133 | 134 | Struct messagePayload = (Struct) payload.get(structureType); 135 | Map cdcPayload = messagePayload.schema().fields().stream() 136 | .map(Field::name) 137 | .filter(fieldName -> messagePayload.get(fieldName) != null) 138 | .map(fieldName -> Pair.of(fieldName, messagePayload.get(fieldName))) 139 | .collect(toMap(Pair::getKey, Pair::getValue)); 140 | 141 | // send a CDC event with header (db, table names and key fields) and body (values) 142 | Map cdcEvent = new HashMap<>(); 143 | cdcEvent.put("header", cdcHeader); 144 | cdcEvent.put("body", cdcPayload); 145 | this.redisCacheService.updateRedis(cdcEvent); 146 | } 147 | } 148 | } 149 | 150 | 151 | 152 | } -------------------------------------------------------------------------------- /caching-service/src/main/java/io/redis/demos/services/caching/service/RedisCacheService.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.caching.service; 2 | 3 | import io.debezium.data.Envelope.Operation; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.stereotype.Service; 7 | import redis.clients.jedis.Jedis; 8 | import redis.clients.jedis.JedisPool; 9 | import redis.clients.jedis.JedisPoolConfig; 10 | 11 | import javax.annotation.PostConstruct; 12 | import java.net.URI; 13 | import java.net.URISyntaxException; 14 | import java.util.LinkedHashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.stream.Collectors; 18 | 19 | @Slf4j 20 | @Service 21 | public class RedisCacheService { 22 | 23 | @Value("${redis.host}") 24 | private String redisHost; 25 | 26 | @Value("${redis.port}") 27 | private int redisPort; 28 | 29 | @Value("${redis.password}") 30 | private String redisPassword; 31 | 32 | // Should the system set the value when a table is updated ? 33 | @Value("${redis.setValue}") 34 | private String setValue; 35 | 36 | // If setValue, should the system create new entry when a record is created 37 | @Value("${redis.setOnInsert}") 38 | private String setOnInsert; 39 | 40 | private JedisPool jedisPool; 41 | 42 | public RedisCacheService() { 43 | } 44 | 45 | @PostConstruct 46 | private void afterConstruct(){ 47 | log.info("Create Jedis Pool with {}:{} ", redisHost, redisPort); 48 | if (redisPassword != null && redisPassword.trim().isEmpty()) { 49 | redisPassword = null; 50 | } 51 | jedisPool = new JedisPool(new JedisPoolConfig(), redisHost, redisPort, 5000, redisPassword ); 52 | } 53 | 54 | 55 | /** 56 | * When a new event is coming : 57 | * - Extract the db, table and id of the record 58 | * - Using redis key format db:table:id 59 | * - Depending of the Redis plugins configuration: 60 | * - Unlink, update or create the key (that will be a Hash) 61 | * @param cdcEvent 62 | */ 63 | public void updateRedis(Map cdcEvent) { 64 | 65 | System.out.println(cdcEvent); 66 | 67 | Map header = (Map) cdcEvent.get("header"); 68 | Map body = (Map) cdcEvent.get("body"); 69 | String db = header.get("source.db").toString(); 70 | String table = header.get("source.table").toString(); 71 | Operation operation = (Operation)header.get("source.operation"); 72 | 73 | List keyFields = (List)header.get("source.key.fields"); 74 | List keyValues = keyFields.stream().map(fieldName -> fieldName +":"+ body.get(fieldName).toString()).collect(Collectors.toList()); 75 | 76 | String id = String.join(":", keyValues); // create unique id as redis Key 77 | 78 | // Connect to Jedis and invalidate the key 79 | try (Jedis jedis = jedisPool.getResource()) { 80 | String key = db +":"+ table +":"+ id; 81 | 82 | if ( ! setValue.equalsIgnoreCase("true")) { 83 | log.info("Invalidate cache value {}", key); 84 | jedis.unlink(key); 85 | } else { 86 | 87 | if(operation == Operation.DELETE ) { 88 | log.info("Delete delete key {}", key); 89 | jedis.unlink(key); 90 | 91 | } else { 92 | if (operation == Operation.UPDATE || 93 | (operation == Operation.CREATE && setOnInsert.equalsIgnoreCase("true")) 94 | ){ 95 | log.info("Update or create key {}}", key); 96 | // Basic String conversion 97 | Map bodyAsString = body.entrySet().stream() 98 | .filter(m -> m.getKey() != null && m.getValue() !=null) 99 | .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString())); 100 | 101 | jedis.hset(key, bodyAsString); 102 | } 103 | } 104 | } 105 | 106 | } 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /caching-service/src/main/resources/application-prod.properties: -------------------------------------------------------------------------------- 1 | 2 | ## Database properties 3 | database.name=inventory 4 | database.hostname=mysql-debezium-service 5 | database.port=3306 6 | database.user=debezium 7 | database.password=dbz 8 | database.server.id=87934 9 | database.server.name=redis-cache-service 10 | table.whitelist=inventory.customers, inventory.orders 11 | 12 | ## Redis properties 13 | redis.host=redis-service 14 | redis.port=6379 15 | redis.password= 16 | redis.setValue=true 17 | redis.setOnInsert=true 18 | 19 | ## Logging properties 20 | logging.file=logs/cache-invalidator.log 21 | logging.level.org.springframework.web=INFO 22 | 23 | ## SpringBoot Config 24 | server.port=8084 25 | 26 | ## OMDB API 27 | omdb.api= 28 | -------------------------------------------------------------------------------- /caching-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | ## Database properties 3 | database.name=inventory 4 | database.hostname=localhost 5 | database.port=3306 6 | database.user=debezium 7 | database.password=dbz 8 | database.server.id=87644 9 | database.server.name=redis-cache-service-dev 10 | table.whitelist=inventory.customers, inventory.orders 11 | 12 | ## Redis properties 13 | redis.host=localhost 14 | redis.port=6379 15 | redis.password= 16 | 17 | redis.setValue=true 18 | redis.setOnInsert=true 19 | 20 | ## Logging properties 21 | logging.file=logs/cache-invalidator.log 22 | logging.level.org.springframework.web=INFO 23 | 24 | ## SpringBoot Config 25 | server.port=8084 26 | 27 | ## OMDB API 28 | omdb.api= 29 | -------------------------------------------------------------------------------- /comments-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | COPY . . 9 | 10 | ENV REDIS_HOST=redis-service 11 | ENV REDIS_PORT=6379 12 | ENV REDIS_PASSWORD= 13 | 14 | EXPOSE 8086 15 | 16 | CMD [ "node", "server.js", "--CONF_FILE", "./config.prod.json" ] 17 | -------------------------------------------------------------------------------- /comments-service/README.md: -------------------------------------------------------------------------------- 1 | ## Movie Comment Service 2 | 3 | This service is use to, create, list and delete comments associated to a movie. 4 | 5 | This service uses RediSearch queries to find comments associated to a movie and sort them by date. 6 | 7 | Note: When a movie is delete, the comments are not deleted. This should/could be done usign a Gears recipe. 8 | 9 | -------------------------------------------------------------------------------- /comments-service/commentsService.js: -------------------------------------------------------------------------------- 1 | 2 | const redis = require('redis'); 3 | const redisearch = require('redis-redisearch'); 4 | const nconf = require('nconf'); 5 | 6 | 7 | const indexName = process.env.REDIS_INDEX || 'ms:search:index:comments:movies'; 8 | const commentsKeyPrefix = process.env.REDIS_COMMENT_PREFIX || 'ms:comments:'; 9 | 10 | nconf.argv(); 11 | nconf.env(); 12 | 13 | nconf.file({ file: (nconf.get('CONF_FILE') || './config.dev.json' ) }); 14 | 15 | const redisHost = process.env.REDIS_HOST || nconf.get("REDIS_HOST"); 16 | const redisPort = process.env.REDIS_PORT || nconf.get("REDIS_PORT"); 17 | const redisPassword = process.env.REDIS_PASSWORD || nconf.get("REDIS_PASSWORD"); 18 | 19 | 20 | let redisUrl = `redis://${redisHost}:${redisPort}` 21 | console.log(`\t Redis : ${redisUrl}`); 22 | 23 | if (redisPassword) { 24 | redisUrl = `redis://default:${redisPassword}@${redisHost}:${redisPort}` 25 | } 26 | 27 | 28 | redisearch(redis); 29 | const client = redis.createClient(redisUrl); 30 | 31 | 32 | const CommentService = function () { 33 | 34 | const _getComment = function(id, callback) { 35 | // using hgetall, since the hash size is limited 36 | client.hgetall(id, function(err, res) { 37 | callback( err, res ); 38 | }); 39 | } 40 | 41 | const _deleteComment = function(id, callback) { 42 | // using hgetall, since the hash size is limited 43 | client.del(id, function(err, res) { 44 | callback( err, res ); 45 | }); 46 | } 47 | 48 | 49 | const _addMovieComment = function(comment, callback) { 50 | const ts = Date.now(); 51 | const key = `${commentsKeyPrefix}movie:${comment.movie_id}:${ts}` 52 | 53 | comment.timestamp = ts; 54 | 55 | const values = [ 56 | "movie_id" , comment.movie_id, 57 | "user_id" , comment.user_id, 58 | "comment" , comment.comment, 59 | "rating" , comment.rating, 60 | "timestamp" , comment.timestamp, 61 | ]; 62 | client.hmset(key, values, function(err, res) { 63 | callback( err, { "id" : key, "comment" : comment } ); 64 | }); 65 | } 66 | 67 | /** 68 | * Retrieve the list of comments for a movie 69 | * @param {*} id 70 | * @param {*} options 71 | * @param {*} callback 72 | */ 73 | const _getMovieComments = function(id, options, callback) { 74 | 75 | let offset = 0; // default values 76 | let limit = 10; // default value 77 | 78 | const queryString = `@movie_id:[${id} ${id}]` 79 | 80 | // prepare the "native" FT.SEARCH call 81 | // FT.SEARCH IDX_NAME queryString [options] 82 | const searchParams = [ 83 | indexName, // name of the index 84 | queryString, // query string 85 | 'WITHSCORES' // return the score 86 | ]; 87 | 88 | // if limit add the parameters 89 | if (options.offset || options.limit) { 90 | offset = options.offset || 0; 91 | limit = options.limit || 10 92 | searchParams.push('LIMIT'); 93 | searchParams.push(offset); 94 | searchParams.push(limit); 95 | } 96 | // if sortby add the parameters 97 | if (options.sortBy) { 98 | searchParams.push('SORTBY'); 99 | searchParams.push(options.sortBy); 100 | searchParams.push((options.ascending) ? 'ASC' : 'DESC'); 101 | } 102 | 103 | client.ft_search( 104 | searchParams, 105 | function (err, searchResult) { 106 | 107 | const totalNumberOfDocs = searchResult[0]; 108 | const result = { 109 | meta: { 110 | totalResults: totalNumberOfDocs, 111 | offset, 112 | limit, 113 | queryString, 114 | }, 115 | docs: [], 116 | } 117 | 118 | // create JSON document from n/v pairs 119 | for (let i = 1; i <= searchResult.length - 1; i++) { 120 | const doc = { 121 | meta: { 122 | score: Number(searchResult[i + 1]), 123 | id: searchResult[i] 124 | } 125 | }; 126 | i = i + 2; 127 | doc.fields = {}; 128 | const fields = searchResult[i] 129 | if (fields) { 130 | for (let j = 0, len = fields.length; j < len; j++) { 131 | const idxKey = j; 132 | const idxValue = idxKey + 1; 133 | j++; 134 | doc.fields[fields[idxKey]] = fields[idxValue]; 135 | 136 | // To make it easier let's format the timestamp 137 | if (fields[idxKey] == "timestamp") { 138 | const date = new Date(parseInt(fields[idxValue])); 139 | doc.fields["dateAsString"] = date.toDateString()+" - "+date.toLocaleTimeString() ; 140 | } 141 | } 142 | } 143 | result.docs.push(doc); 144 | } 145 | 146 | callback(err, result); 147 | } 148 | ); 149 | } 150 | 151 | return { 152 | getComment: _getComment, 153 | getMovieComments: _getMovieComments, 154 | addMovieComment: _addMovieComment, 155 | deleteComment: _deleteComment, 156 | }; 157 | } 158 | 159 | module.exports = CommentService; 160 | 161 | -------------------------------------------------------------------------------- /comments-service/config.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "PORT": "8086", 3 | "REDIS_HOST" : "localhost", 4 | "REDIS_PORT" : 6379, 5 | "REDIS_PASSWORD" : "" 6 | } -------------------------------------------------------------------------------- /comments-service/config.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "PORT": "8086", 3 | "REDIS_HOST" : "redis-service", 4 | "REDIS_PORT" : 6379, 5 | "REDIS_PASSWORD" : "" 6 | } -------------------------------------------------------------------------------- /comments-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movie-comments-service", 3 | "version": "1.0.0", 4 | "description": "Commenting service for Redis Movie Database application", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "engines": { 11 | "node": ">=8.9.4" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/redis-developer/redis-microservices-demo.git" 16 | }, 17 | "keywords": [ 18 | "redis", 19 | "redisearch", 20 | "node" 21 | ], 22 | "author": "Tugdual Grall", 23 | "license": "Apache-2.0", 24 | "bugs": { 25 | "url": "https://github.com/redis-developer/redis-microservices-demo/issues" 26 | }, 27 | "homepage": "https://github.com/redis-developer/redis-microservices-demo#readme", 28 | "dependencies": { 29 | "cors": "^2.8.5", 30 | "express": "^4.17.1", 31 | "redis": "^3.1.1", 32 | "redis-redisearch": "stockholmux/node_redis-redisearch", 33 | "nconf": "^0.10.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /comments-service/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const app = express(); 4 | const nconf = require('nconf'); 5 | 6 | nconf.argv(); 7 | nconf.env(); 8 | 9 | nconf.file({ file: (nconf.get('CONF_FILE') || './config.dev.json' ) }); 10 | console.log(`... starting node host :\n\t port: ${nconf.get("PORT")}`); 11 | 12 | const serverPort = nconf.get('PORT') || 8086; 13 | 14 | 15 | const CommentsService = require('./commentsService'); 16 | const searchService = new CommentsService(); 17 | 18 | app.use(cors()); 19 | app.use(express.json()) 20 | 21 | app.get('/api/1.0/comments/:id', (req, res) => { 22 | const id = req.params.id; 23 | searchService.getComment(id, function (err, result) { 24 | res.json(result); 25 | }); 26 | }); 27 | 28 | app.delete('/api/1.0/comments/:id', (req, res) => { 29 | const id = req.params.id; 30 | searchService.deleteComment(id, function (err, result) { 31 | res.json(result); 32 | }); 33 | }); 34 | 35 | app.post('/api/1.0/comments/movie/:id', (req, res) => { 36 | searchService.addMovieComment(req.body, function (err, result) { 37 | res.json(result); 38 | }); 39 | }); 40 | 41 | app.get('/api/1.0/comments/movie/:id', (req, res) => { 42 | const id = req.params.id; 43 | const offset = Number((req.query.offset) ? req.query.offset : '0'); 44 | const limit = Number((req.query.limit) ? req.query.limit : '10'); 45 | const sortBy = req.query.sortby; 46 | const ascending = req.query.ascending; 47 | 48 | const options = { 49 | offset, 50 | limit 51 | }; 52 | 53 | if (sortBy) { 54 | options.sortBy = sortBy; 55 | options.ascending = true; // if sorted by default it is ascending 56 | } else { 57 | options.sortBy = "timestamp"; 58 | options.ascending = false; 59 | } 60 | 61 | if (ascending) { 62 | options.ascending = (ascending == 1 || ascending.toLocaleLowerCase() === 'true'); 63 | } 64 | 65 | // Retrive the services 66 | searchService.getMovieComments(id, options, function (err, result) { 67 | res.json(result); 68 | }); 69 | }); 70 | 71 | 72 | app.get('/api/1.0/', (req, res) => { 73 | res.json({ status: 'started' }); 74 | }); 75 | 76 | 77 | app.get('/', (req, res) => { 78 | res.send('Comment Service Node Node REST Server Started'); 79 | }); 80 | 81 | app.listen(serverPort, () => { 82 | console.log(`Comment Service Node listening at http://localhost:`+ nconf.get('PORT')); 83 | }); 84 | 85 | -------------------------------------------------------------------------------- /db-to-streams-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | 3 | COPY target/*.jar /app.jar 4 | 5 | ENV REDIS_HOST=redis-service 6 | ENV REDIS_PORT=6379 7 | ENV REDIS_PASSWORD= 8 | 9 | ENV DATABASE_HOSTNAME=app-mysql 10 | ENV DATABASE_PORT=3306 11 | ENV DATABASE_USER=debezium 12 | ENV DATABASE_PASSWORD=dbz 13 | 14 | EXPOSE 8082 15 | ENTRYPOINT ["java","-jar","/app.jar","--spring.profiles.active=prod"] 16 | 17 | 18 | -------------------------------------------------------------------------------- /db-to-streams-service/README.md: -------------------------------------------------------------------------------- 1 | # Debezium to Streams 2 | 3 | This service is used to capture changes in the RDBMS and put all events on a streams 4 | 5 | 6 | --- 7 | Below the structures that are generated by Debezium for Insert, Update and Delete operations: 8 | 9 | * INSERT 10 | ``` 11 | Struct{ 12 | after=Struct{actor_id=9000,first_name=John,last_name=Doe,dob=1973}, 13 | source=Struct{version=1.1.0.Final,connector=mysql,name=redis-stream-service-dev,ts_ms=1586575902000,db=inventory,table=actors,server_id=223344,file=mysql-bin.000003,pos=359,row=0,thread=5},op=c,ts_ms=1586575902270} 14 | ``` 15 | 16 | 17 | * UPDATE 18 | ``` 19 | Struct{ 20 | before=Struct{actor_id=9000,first_name=John,last_name=Doe,dob=1973}, 21 | after=Struct{actor_id=9000,first_name=Johnny,last_name=Doe,dob=1973}, 22 | source=Struct{version=1.1.0.Final,connector=mysql,name=redis-stream-service-dev,ts_ms=1586576003000,db=inventory,table=actors,server_id=223344,file=mysql-bin.000003,pos=649,row=0,thread=5},op=u,ts_ms=1586576003261} 23 | ``` 24 | 25 | 26 | *DELETE 27 | 28 | ``` 29 | Struct{ 30 | before=Struct{actor_id=9000,first_name=Johnny,last_name=Grall,dob=1973}, 31 | source=Struct{version=1.1.0.Final,connector=mysql,name=redis-stream-service-dev,ts_ms=1586576052000,db=inventory,table=actors,server_id=223344,file=mysql-bin.000003,pos=963,row=0,thread=5},op=d,ts_ms=1586576052199} 32 | ``` -------------------------------------------------------------------------------- /db-to-streams-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | cache-invalidation-cdc-debezium 7 | io.redis.demos 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | db-to-streams-service 13 | 14 | 15 | 16 | 17 | io.debezium 18 | debezium-api 19 | ${version.debezium} 20 | 21 | 22 | 23 | io.debezium 24 | debezium-embedded 25 | ${version.debezium} 26 | 27 | 28 | org.slf4j 29 | slf4j-log4j12 30 | 31 | 32 | 33 | 34 | 35 | 36 | io.debezium 37 | debezium-connector-mysql 38 | ${version.debezium} 39 | 40 | 41 | 42 | redis.clients 43 | jedis 44 | ${version.jedis} 45 | 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-web 51 | 52 | 53 | 54 | org.projectlombok 55 | lombok 56 | 57 | 58 | 59 | 60 | 61 | 62 | snapshots-repo 63 | https://oss.sonatype.org/content/repositories/snapshots 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.springframework.boot 73 | spring-boot-maven-plugin 74 | 75 | 76 | 77 | org.apache.maven.plugins 78 | maven-compiler-plugin 79 | 3.8.1 80 | 81 | 1.8 82 | 1.8 83 | 84 | 85 | org.projectlombok 86 | lombok 87 | 1.18.12 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | dev 98 | 99 | true 100 | 101 | 102 | dev 103 | 104 | 105 | 106 | prod 107 | 108 | prod 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /db-to-streams-service/src/main/java/io/redis/demos/services/db/events/streams/RedisStreamsCDCPublisher.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.db.events.streams; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @Slf4j 8 | @SpringBootApplication 9 | public class RedisStreamsCDCPublisher { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(RedisStreamsCDCPublisher.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /db-to-streams-service/src/main/java/io/redis/demos/services/db/events/streams/RestStatusController.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.db.events.streams; 2 | 3 | 4 | import io.redis.demos.services.db.events.streams.listener.CDCEventListener; 5 | import org.springframework.web.bind.annotation.CrossOrigin; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import javax.inject.Inject; 11 | import java.io.IOException; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | @RestController 16 | @RequestMapping("/api/1.0/db-to-streams-service/") 17 | @CrossOrigin("*") 18 | @org.springframework.context.annotation.Configuration 19 | public class RestStatusController { 20 | 21 | @Inject 22 | io.debezium.config.Configuration config; 23 | 24 | @Inject 25 | CDCEventListener cdcEventListener; 26 | 27 | @GetMapping("/status") 28 | public Map status() { 29 | 30 | Map result = new HashMap<>(); 31 | 32 | result.put("service", "RedisStreamsCDCPublisher"); 33 | result.put("status", cdcEventListener.getState() ); 34 | result.put("version", "1.0"); 35 | 36 | return result; 37 | } 38 | 39 | @GetMapping("/start") 40 | public Map start() throws IOException { 41 | Map result = new HashMap<>(); 42 | cdcEventListener.startDebezium(); 43 | result.put("service", "RedisStreamsCDCPublisher.start"); 44 | result.put("action", "OK"); 45 | return result; 46 | } 47 | 48 | @GetMapping("/stop") 49 | public Map stop() throws IOException { 50 | Map result = new HashMap<>(); 51 | cdcEventListener.stopDebezium(); 52 | result.put("service", "RedisStreamsCDCPublisher.stop"); 53 | result.put("action", "OK"); 54 | return result; 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /db-to-streams-service/src/main/java/io/redis/demos/services/db/events/streams/config/ConnectorConfig.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.db.events.streams.config; 2 | 3 | 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import java.util.Properties; 9 | 10 | @Configuration 11 | public class ConnectorConfig { 12 | 13 | /** 14 | * Student Database details. 15 | */ 16 | @Value("${database.hostname}") 17 | private String databaseHostname; 18 | 19 | @Value("${database.name}") 20 | private String databaseName; 21 | 22 | @Value("${database.port}") 23 | private String databasePort; 24 | 25 | @Value("${database.user}") 26 | private String databaseUser; 27 | 28 | @Value("${database.password}") 29 | private String rdbmsDBPassword; 30 | 31 | @Value("${database.password}") 32 | private String databasePassword; 33 | 34 | @Value("${database.server.id}") 35 | private String databaseServerId; 36 | 37 | @Value("${database.server.name}") 38 | private String databaseServerName; 39 | 40 | @Value("${table.whitelist}") 41 | private String tableWhitelist; 42 | 43 | 44 | @Bean 45 | public io.debezium.config.Configuration createConnectorConfig() { 46 | Properties props = new Properties(); 47 | 48 | props.setProperty("name", "streamsserviceengine"); 49 | props.setProperty("connector.class", "io.debezium.connector.mysql.MySqlConnector"); 50 | 51 | props.setProperty("database.hostname", databaseHostname); 52 | props.setProperty("database.name", databaseName); 53 | props.setProperty("database.port", databasePort); 54 | props.setProperty("database.user", databaseUser); 55 | props.setProperty("database.password", databasePassword); 56 | props.setProperty("database.server.id", databaseServerId); 57 | props.setProperty("database.server.name", databaseServerName); 58 | props.setProperty("database.history", "io.debezium.relational.history.FileDatabaseHistory"); 59 | props.setProperty("database.history.file.filename", "./dbhistory-cdc-sync.dat"); 60 | props.setProperty("table.whitelist", tableWhitelist); 61 | 62 | props.setProperty("offset.storage", "org.apache.kafka.connect.storage.FileOffsetBackingStore"); 63 | props.setProperty("offset.storage.file.filename", "./offsets.dat"); 64 | props.setProperty("offset.flush.interval.ms", "5000"); 65 | 66 | return io.debezium.config.Configuration.from(props); 67 | } 68 | 69 | 70 | 71 | } 72 | -------------------------------------------------------------------------------- /db-to-streams-service/src/main/java/io/redis/demos/services/db/events/streams/listener/CDCEventListener.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.db.events.streams.listener; 2 | 3 | import io.debezium.config.Configuration; 4 | import io.debezium.data.Envelope; 5 | import io.debezium.embedded.Connect; 6 | import io.debezium.engine.DebeziumEngine; 7 | import io.redis.demos.services.db.events.streams.service.RedisStreamsDebeziumProducer; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.apache.commons.lang3.tuple.Pair; 10 | import org.apache.kafka.connect.data.Field; 11 | import org.apache.kafka.connect.data.Struct; 12 | import org.apache.kafka.connect.source.SourceRecord; 13 | import org.springframework.stereotype.Component; 14 | 15 | import java.io.IOException; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.Properties; 20 | import java.util.concurrent.ExecutorService; 21 | import java.util.concurrent.Executors; 22 | import java.util.stream.Collectors; 23 | 24 | import static io.debezium.data.Envelope.FieldName.*; 25 | import static java.util.stream.Collectors.toMap; 26 | 27 | @Slf4j 28 | @Component 29 | @org.springframework.context.annotation.Configuration 30 | public class CDCEventListener { 31 | 32 | private String topicName; 33 | private String status="STOPPED"; 34 | 35 | // Thread for the Debezium engine 36 | private ExecutorService executor = Executors.newSingleThreadExecutor(); 37 | DebeziumEngine engine = null; 38 | 39 | // Service layer to interact with Redis 40 | private final RedisStreamsDebeziumProducer redisStreamsService; 41 | private final Properties configAsProperties; 42 | 43 | public CDCEventListener(Configuration config, RedisStreamsDebeziumProducer service) throws IOException { 44 | topicName = config.getString("database.server.name") + "." + config.getString("database.name") + "."; 45 | redisStreamsService = service; 46 | configAsProperties = config.asProperties(); 47 | } 48 | 49 | 50 | public void startDebezium() throws IOException { 51 | log.info("Starting Debezium...."); 52 | try (DebeziumEngine start = DebeziumEngine.create(Connect.class).using(configAsProperties) 53 | .notifying(record -> { 54 | handleEvent(record); 55 | }).build()) 56 | { 57 | engine = start; 58 | executor = Executors.newSingleThreadExecutor(); 59 | executor.execute(engine); 60 | status = "RUNNING"; 61 | } 62 | 63 | } 64 | 65 | public void stopDebezium(){ 66 | log.info("Stopping Debezium...."); 67 | try { 68 | engine.close(); 69 | executor.shutdown(); 70 | executor = null; 71 | } catch (Exception e) { 72 | log.error( e.getMessage() ); 73 | } finally { 74 | status = "STOPPED"; 75 | } 76 | } 77 | 78 | public String getState() { 79 | return this.status; 80 | } 81 | 82 | /** 83 | * Capture CDC Event if event is from the proper DB/Table send it to @RedisCacheService 84 | * 85 | * @param record 86 | */ 87 | private void handleEvent(SourceRecord record) { 88 | 89 | // check of the events is sent to the proper topic that is the server-name and database 90 | if ( record != null && record.topic() != null && record.topic().startsWith(topicName)) { 91 | Struct payload = (Struct) record.value(); 92 | 93 | if (payload != null && payload.getString("op") != null) { 94 | String tableName = null; 95 | String id = null; 96 | String structureType = AFTER; 97 | 98 | // prepare body 99 | Envelope.Operation op = Envelope.Operation.forCode(payload.getString("op")); 100 | if (op == Envelope.Operation.DELETE) { 101 | structureType = BEFORE; 102 | } 103 | 104 | 105 | // prepare header 106 | Struct sourcePayload = (Struct) payload.get(SOURCE); 107 | Map cdcHeader = new HashMap<>(); 108 | cdcHeader.put("source.db", sourcePayload.getString("db")); 109 | cdcHeader.put("source.table", sourcePayload.getString("table")); 110 | cdcHeader.put("source.operation", op); 111 | 112 | List fieldNames = record.keySchema().fields().stream().map(field -> field.name()).collect(Collectors.toList()); 113 | cdcHeader.put("source.key.fields", fieldNames); 114 | 115 | Map cdcPayload = getCDCEventAsMap( structureType, payload ); 116 | 117 | // send a CDC event with header (db, table names and key fields) and body (values) 118 | Map cdcEvent = new HashMap<>(); 119 | cdcEvent.put("header", cdcHeader); 120 | cdcEvent.put("body", cdcPayload); 121 | 122 | // when UPDATE we should add another structure to allow consumer to understand the value before change 123 | if (op == Envelope.Operation.UPDATE) { 124 | Map cdcPayloadBefore = getCDCEventAsMap( BEFORE , payload ); 125 | cdcEvent.put("before", cdcPayloadBefore); 126 | } 127 | this.redisStreamsService.publishEventToStreams(cdcEvent); 128 | } 129 | } 130 | } 131 | 132 | /** 133 | * Helper method that transform the Debezium Structure into a simple Map 134 | * @param operation 135 | * @param payload 136 | * @return 137 | */ 138 | private Map getCDCEventAsMap(String operation, Struct payload) { 139 | Struct messagePayload = (Struct) payload.get(operation); 140 | Map cdcPayload = messagePayload.schema().fields().stream().map(Field::name) 141 | .filter(fieldName -> messagePayload.get(fieldName) != null) 142 | .map(fieldName -> Pair.of(fieldName, messagePayload.get(fieldName))) 143 | .collect(toMap(Pair::getKey, Pair::getValue)); 144 | return cdcPayload; 145 | } 146 | 147 | } 148 | 149 | 150 | -------------------------------------------------------------------------------- /db-to-streams-service/src/main/java/io/redis/demos/services/db/events/streams/service/RedisStreamsDebeziumProducer.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.db.events.streams.service; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.stereotype.Component; 6 | import redis.clients.jedis.Jedis; 7 | import redis.clients.jedis.JedisPool; 8 | import redis.clients.jedis.JedisPoolConfig; 9 | import redis.clients.jedis.StreamEntryID; 10 | 11 | import javax.annotation.PostConstruct; 12 | import java.net.URI; 13 | import java.net.URISyntaxException; 14 | import java.util.LinkedHashMap; 15 | import java.util.Map; 16 | import java.util.stream.Collectors; 17 | 18 | @Slf4j 19 | @Component 20 | public class RedisStreamsDebeziumProducer { 21 | 22 | @Value("${redis.host}") 23 | private String redisHost; 24 | 25 | @Value("${redis.port}") 26 | private int redisPort; 27 | 28 | @Value("${redis.password}") 29 | private String redisPassword; 30 | 31 | // Should the system set the value when a table is updated ? 32 | @Value("${redis.setValue}") 33 | private String setValue; 34 | 35 | // If setValue, should the system create new entry when a record is created 36 | @Value("${redis.setOnInsert}") 37 | private String setOnInsert; 38 | 39 | private JedisPool jedisPool; 40 | 41 | public RedisStreamsDebeziumProducer() { 42 | } 43 | 44 | @PostConstruct 45 | private void afterConstruct(){ 46 | log.info("Create Jedis Pool with {}:{} ", redisHost, redisPort); 47 | if (redisPassword != null && redisPassword.trim().isEmpty()) { 48 | redisPassword = null; 49 | } 50 | jedisPool = new JedisPool(new JedisPoolConfig(), redisHost, redisPort, 5000, redisPassword ); 51 | 52 | } 53 | 54 | 55 | /** 56 | * When a new event is coming : 57 | * - Extract the db, table and id of the record 58 | * - Using redis key format db:table:id 59 | * - Depending of the Redis plugins configuration: 60 | * - Unlink, update or create the key (that will be a Hash) 61 | * @param cdcEvent 62 | */ 63 | public void publishEventToStreams(Map cdcEvent) { 64 | 65 | Map header = (Map) cdcEvent.get("header"); 66 | Map body = (Map) cdcEvent.get("body"); 67 | 68 | String db = header.get("source.db").toString(); 69 | String table = header.get("source.table").toString(); 70 | 71 | Map headerAsString = header.entrySet().stream() 72 | .filter(m -> m.getKey() != null && m.getValue() !=null) 73 | .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString())); 74 | 75 | Map bodyAsString = body.entrySet().stream() 76 | .filter(m -> m.getKey() != null && m.getValue() !=null) 77 | .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString())); 78 | 79 | // merge the maps into a Map keeping the attribute in order for easy reading 80 | Map message = new LinkedHashMap<>(); 81 | message.putAll(headerAsString); 82 | message.putAll(bodyAsString); 83 | 84 | // if update create addition information with "before" changes fields 85 | if ( cdcEvent.containsKey("before") ) { 86 | Map bodyBefore = (Map) cdcEvent.get("before"); 87 | 88 | Map beforeAsString = bodyBefore.entrySet().stream() 89 | .filter(m -> m.getKey() != null && m.getValue() !=null) 90 | .collect(Collectors.toMap( 91 | e -> { return "before:"+ e.getKey(); } , 92 | e -> e.getValue().toString()) 93 | ); 94 | message.putAll(beforeAsString); 95 | } 96 | 97 | // Connect to Jedis and invalidate the key 98 | try (Jedis jedis = jedisPool.getResource()) { 99 | String key = "events:"+ db +":"+ table ; 100 | jedis.xadd( key, StreamEntryID.NEW_ENTRY, message ); 101 | } 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /db-to-streams-service/src/main/resources/application-prod.properties: -------------------------------------------------------------------------------- 1 | 2 | ## Database properties 3 | database.name=inventory 4 | database.hostname=app-mysql 5 | database.port=3306 6 | database.user=debezium 7 | database.password=dbz 8 | database.server.id=85223 9 | database.server.name=redis-stream-service 10 | table.whitelist=inventory.actors, inventory.movies, inventory.theaters 11 | 12 | ## Redis properties 13 | redis.host=redis-service 14 | redis.port=6379 15 | redis.password= 16 | 17 | redis.setValue=true 18 | redis.setOnInsert=true 19 | 20 | ## Logging properties 21 | logging.file=logs/data-streams-producer.log 22 | logging.level.org.springframework.web=INFO 23 | 24 | 25 | ## SpringBoot Config 26 | server.port=8082 27 | -------------------------------------------------------------------------------- /db-to-streams-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | ## Database properties 3 | database.name=inventory 4 | database.hostname=127.0.0.1 5 | database.port=3306 6 | database.user=debezium 7 | database.password=dbz 8 | database.server.id=85282 9 | database.server.name=redis-stream-service-dev 10 | table.whitelist=inventory.actors, inventory.movies, inventory.theaters 11 | 12 | ## Redis properties 13 | redis.host=localhost 14 | redis.port=6379 15 | redis.password= 16 | 17 | redis.setValue=true 18 | redis.setOnInsert=true 19 | 20 | ## Logging properties 21 | logging.file=logs/data-streams-producer.log 22 | logging.level.org.springframework.web=INFO 23 | 24 | 25 | ## SpringBoot Config 26 | server.port=8082 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | 4 | app-redis: 5 | image: redislabs/redismod:latest 6 | ports: 7 | - "6379:6379" 8 | networks: 9 | - redis-microservices-network 10 | restart: always 11 | 12 | app-mysql: 13 | build: 14 | context: ./mysql-database 15 | dockerfile: Dockerfile 16 | ports: 17 | - "3306:3306" 18 | environment: 19 | - MYSQL_ROOT_PASSWORD=debezium 20 | - MYSQL_USER=mysqluser 21 | - MYSQL_PASSWORD=mysqlpw 22 | networks: 23 | - redis-microservices-network 24 | restart: always 25 | 26 | app-db-to-streams: 27 | build: 28 | context: ./db-to-streams-service 29 | dockerfile: Dockerfile 30 | ports: 31 | - "8082:8082" 32 | environment: 33 | - REDIS_HOST=app-redis 34 | - REDIS_PORT=6379 35 | - REDIS_PASSWORD= 36 | networks: 37 | - redis-microservices-network 38 | restart: always 39 | depends_on: 40 | - app-mysql 41 | - app-redis 42 | 43 | app-streams-to-redisgraph: 44 | build: 45 | context: ./streams-to-redisgraph-service 46 | dockerfile: Dockerfile 47 | ports: 48 | - "8083:8083" 49 | environment: 50 | - REDIS_HOST=app-redis 51 | - REDIS_PORT=6379 52 | - REDIS_PASSWORD= 53 | networks: 54 | - redis-microservices-network 55 | restart: always 56 | depends_on: 57 | - app-mysql 58 | - app-redis 59 | 60 | app-sql-rest-api: 61 | build: 62 | context: ./sql-rest-api 63 | dockerfile: Dockerfile 64 | ports: 65 | - "8081:8081" 66 | environment: 67 | - SPRING_DATASOURCE_URL=jdbc:mysql://app-mysql:3306/inventory 68 | - SPRING_DATASOURCE_USERNAME=mysqluser 69 | - SPRING_DATASOURCE_PASSWORD=mysqlpw 70 | networks: 71 | - redis-microservices-network 72 | depends_on: 73 | - app-mysql 74 | restart: always 75 | 76 | app-caching: 77 | build: 78 | context: ./caching-service 79 | dockerfile: Dockerfile 80 | ports: 81 | - "8084:8084" 82 | environment: 83 | - REDIS_HOST=app-redis 84 | - REDIS_PORT=6379 85 | - REDIS_PASSWORD= 86 | networks: 87 | - redis-microservices-network 88 | depends_on: 89 | - app-mysql 90 | - app-redis 91 | restart: always 92 | 93 | app-streams-to-redis-hashes: 94 | build: 95 | context: ./streams-to-redisearch-service 96 | dockerfile: Dockerfile 97 | ports: 98 | - "8085:8085" 99 | environment: 100 | - REDIS_HOST=app-redis 101 | - REDIS_PORT=6379 102 | - REDIS_PASSWORD= 103 | networks: 104 | - redis-microservices-network 105 | depends_on: 106 | - app-redis 107 | restart: always 108 | 109 | app-comments: 110 | build: 111 | context: ./comments-service 112 | dockerfile: Dockerfile 113 | ports: 114 | - "8086:8086" 115 | environment: 116 | - REDIS_HOST=app-redis 117 | - REDIS_PORT=6379 118 | - REDIS_PASSWORD= 119 | networks: 120 | - redis-microservices-network 121 | depends_on: 122 | - app-redis 123 | restart: always 124 | 125 | ws-notifications-service: 126 | build: 127 | context: ./notifications-service-node 128 | dockerfile: Dockerfile 129 | ports: 130 | - "8888:8888" 131 | environment: 132 | - REDIS_HOST=app-redis 133 | - REDIS_PORT=6379 134 | - REDIS_PASSWORD= 135 | networks: 136 | - redis-microservices-network 137 | depends_on: 138 | - app-redis 139 | restart: always 140 | 141 | app-frontend: 142 | build: 143 | context: ./ui-redis-front-end/redis-front 144 | dockerfile: Dockerfile 145 | ports: 146 | - "8080:80" 147 | networks: 148 | - redis-microservices-network 149 | depends_on: 150 | - app-redis 151 | - app-mysql 152 | - app-streams-to-redisgraph 153 | - app-streams-to-redis-hashes 154 | - app-db-to-streams 155 | - app-caching 156 | - app-sql-rest-api 157 | restart: always 158 | 159 | networks: 160 | redis-microservices-network: 161 | driver: bridge -------------------------------------------------------------------------------- /docs/demoscript/01-introduction.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | This pages explained how to use the demonstration to discover variois features and uses cases of Redis. 4 | 5 | 6 | ![Archi](../..//ui-redis-front-end/redis-front/public/imgs/overal-archi.png) 7 | 8 | 9 | 10 | --- 11 | Next: [Configuration with Redis](02-configuration-with-redis.md) -------------------------------------------------------------------------------- /docs/demoscript/02-configuration-with-redis.md: -------------------------------------------------------------------------------- 1 | 2 | ## Configuration with Redis 3 | 4 | Redis can be used to share some configuration between services/application. 5 | 6 | In this demonstration the service `caching-service` is used to store: 7 | 8 | * the OMBD API Key 9 | * a counter to increment the number of call to the API 10 | 11 | 12 | ### Script 13 | 14 | 15 | 1. Open RedisInsight 16 | 2. Database is empty 17 | 3. Go in the "Services" page in the RMDB application 18 | 4. Save your OMDB API key in the form (eg: `72a28845`) 19 | 5. Go back to RedisInsight, and list the keys 20 | 21 | As you can see the `ms:config` hash is created. 22 | 23 | You can imagine adding new fields to this hash for more application configuration options, for example `ENV:DEMO`. 24 | 25 | Let's now use the the OMDB API key to call the Web service and cache the result.. 26 | 27 | 28 | --- 29 | Next: [Caching Web Services Calls](03-caching-web-service-calls,md) 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/demoscript/03-caching-web-service-calls.md: -------------------------------------------------------------------------------- 1 | 2 | ## Caching Web Services Calls 3 | 4 | In the demonstration, Redis is used to cache the result of OMDB API calls. 5 | 6 | This Web service is used to get the ratings for the selected movie, and you can select to cache or not the result in Redis. 7 | 8 | 9 | ### Script 10 | 11 | 1. Go to the Movies list 12 | 2. Click on the first movie 13 | 3. The ratings are visible (under the submit button) 14 | 3. Look at the elapsed time and the number of calls 15 | 4. Click on the green button to call the service again (since it is not cache) 16 | 5. Enable caching for the web service using the sliding button 17 | 6. Click on the green button again, you will see that it is faster and the number of calls does not increase. 18 | 7. Go to RedisInsight an look at the keys, you will see a key for the movie rating 19 | 8. You can see the TTL of each key too. 20 | 21 | 22 | 23 | --- 24 | Next: [Extending your application (comments)](04-extending-your-application-comments.md) -------------------------------------------------------------------------------- /docs/demoscript/04-extending-your-application-comments.md: -------------------------------------------------------------------------------- 1 | 2 | ## Extending your application (comments) 3 | 4 | Support you do not have access to the RDMS Schema (not allowed to create new tables), but you still want to add features to the application, for example: 5 | 6 | * adding comments and rating to movie catalog 7 | 8 | Using Redis you and a new micro service you can do it easily. (see `comments-service`) 9 | 10 | ### Script 11 | 12 | 13 | 1. Go on a movie 14 | 2. Show that 'Comments' tab at the bottom of the page 15 | 3. Enter a small comment 16 | 4. Save the comment 17 | 5. Add another one 18 | 6. See that the comments are sorted by creation date descending 19 | 7. Go to RedisInsight 20 | 21 | The comment service use a RediSearch index to query the data an retrieve the comment by movie and date. 22 | 23 | 24 | In the Redis CLI or RedisInsight you can look at the index and run the following commands: 25 | 26 | 27 | Get the list of indexes 28 | 29 | ``` 30 | > FT._LIST 31 | ``` 32 | 33 | Look at comments index info 34 | 35 | ``` 36 | > FT.INFO "ms:search:index:comments:movies" 37 | ``` 38 | 39 | List all comments for movie #1 (Guardians of the Galaxy) order by time stamp descending: 40 | 41 | ``` 42 | > FT.SEARCH "ms:search:index:comments:movies" "@movie_id:[1 1]" SORTBY timestamp DESC 43 | ``` 44 | 45 | As you can see it is possible to filter and sort Redis data by value (hash fields), and it is very easy! 46 | 47 | --- 48 | Next: [CDC with Debezium and Redis Streams](05-cdc-with-debezium-and-streams.md) -------------------------------------------------------------------------------- /docs/demoscript/05-cdc-with-debezium-and-streams.md: -------------------------------------------------------------------------------- 1 | 2 | ## CDC with Debezium and Redis Streams 3 | 4 | Some applications wants to react to transactions for multiple reasons: 5 | 6 | * capture business events and process them for a specific new use case 7 | * refresh data into a cache 8 | * push data into a different datamodel (graph) to query/process them differently 9 | 10 | You can find many different CDC (Change Data Catpure) tools on the market. This demonstration use Debezium to capture MySQL transaction. 11 | 12 | To provide a very flexible architecture and decouple the event source (RDBMS transactions) each event will be sent to a Redis Streams. Then developers can create new services (sink) with the events. 13 | 14 | ### Script 15 | 16 | 1. Go to RedisInsight, as you can see in the streams screen you do not have any yet 17 | 2. Go to the `Services` in the sample application 18 | 3. Click start on the first step at the left. This will start the process that capture all transactions and push them into Redis Streams 19 | 4. Go back to RedisInsight and refresh the Streams page, you will see 3 streams, one by MySQL table: Movies, Actors and Theaters (not used in this app) 20 | 5. Look at the last message of the `"events:inventory:actors"` streams. (add more Colums) 21 | 6. You can see that all transactions are send as simple message 22 | 7. Go to the Actor screen inthe demonstration and modify the first name of an actor, and click save. (this will modify the record in MySQL) 23 | 8. Go back to RedisInsight, and you can see that a new message has been send to the streams. 24 | 25 | 26 | Let's now consume the messages to build new datamodel/store data into Redis. 27 | 28 | --- 29 | Next: [Consume Streams and push data into Redis Hashes](06-consume-streams-to-redish-hash.md) -------------------------------------------------------------------------------- /docs/demoscript/06-consume-streams-to-redish-hash.md: -------------------------------------------------------------------------------- 1 | 2 | ## Consume Streams and push data into Redis Hashes 3 | 4 | Let's now consume the messages from the streams and create/update entries in Redis Hashes indexed using RediSearch. 5 | 6 | ### Script 7 | 8 | 1. Go to the Services screen 9 | 2. Start the "Redis Streams to RediSearch/Hashes" 10 | 3. You have now many hashes in Redis, go to RedisInsight and you will see the new keys. 11 | 4. Actors, and Movies are indexed and you can now do queries on it: 12 | 1. Click on Search, run some queries: 13 | 14 | `*` 15 | 16 | `star war` 17 | 18 | `star war -jedi` 19 | 5. Click on Movies (faceted) to look at the benefits of the fast search 20 | 6. You can copy the query from the screen in RedisInsight CLI or Search screen. 21 | 22 | Other queries: 23 | ``` 24 | FT.SEARCH ms:search:index:movies "wars -Strip -Sith" RETURN 1 title 25 | ``` 26 | 27 | ``` 28 | FT.SEARCH ms:search:index:movies "redis " RETURN 2 title plot 29 | ``` 30 | 31 | ``` 32 | FT.AGGREGATE ms:search:index:movies: "@release_year:2015" GROUPBY 1 @genre REDUCE COUNT 0 AS sum SORTBY 2 @genre ASC MAX 10 33 | ``` 34 | 35 | The aggregation query is used in 2 places in the demonstration: 36 | 37 | * in the "Statistics" screen 38 | * in the list of categories with the number of movies in the Faceted Search screen 39 | 40 | 41 | You can learn more about Redis query and indexing capabilities in the [RediSearch tutorial](https://github.com/RediSearch/redisearch-getting-started/blob/master/docs/001-introduction.md). 42 | 43 | --- 44 | Next: [Consume Streams and push data into RedisGraph](07-consume-streams-to-redish-graph.md) -------------------------------------------------------------------------------- /docs/demoscript/07-consume-streams-to-redish-graph.md: -------------------------------------------------------------------------------- 1 | 2 | ## Consume Streams and push data into RedisGraph 3 | 4 | Let's now consume the messages from the streams and create/update entries in RedisGraph. 5 | 6 | ### Script 7 | 8 | 1. Go to the Services screen 9 | 2. Start the "Redis Streams to RediGraph" 10 | 3. The `imdb` graph is created and populated 11 | 4. Go to RedisInsight in the RedisGraph screen and run some queries 12 | 13 | 14 | **Find the actor with `actor_id = 1`** 15 | 16 | ``` 17 | MATCH (a:actor{actor_id:1}) RETURN a 18 | 19 | ``` 20 | 21 | Click on the node to see the properties, right clic to select the attribute to show by default. 22 | 23 | Double click to navigate in movies, actor, ... 24 | 25 | 26 | 27 | **Find the actor_id = 1 and all the movies he has acted in** 28 | 29 | ``` 30 | MATCH (a:actor{actor_id:1})-[:acted_in]->(m:movie) RETURN a,m 31 | ``` 32 | 33 | 34 | **Find all movies with Tom Cruise** 35 | 36 | ``` 37 | MATCH (a:actor{first_name:'Tom', last_name:'Cruise'})-[r:acted_in]->(m:movie) RETURN m,a 38 | 39 | ``` 40 | 41 | **Count the number of movies by actors ('join')** 42 | 43 | ``` 44 | MATCH (a:actor)-[:acted_in]->(m:movie) RETURN a.first_name, a.last_name, count(*) AS movies order by movies DESC 45 | ``` 46 | 47 | 48 | 49 | 50 | **Find actors and movies that played with Tom Cruise** 51 | 52 | ``` 53 | MATCH (a:actor)-[:acted_in]->(m:movie), 54 | (other:actor)-[:acted_in]->(m) 55 | WHERE a.last_name = 'Cruise' 56 | AND other.last_name <> 'Cruise' 57 | RETURN other.first_name, other.last_name, m.title 58 | ``` 59 | 60 | 61 | -------------------------------------------------------------------------------- /kubernetes/redis-demo-cluster-crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: app.redislabs.com/v1 2 | kind: RedisEnterpriseCluster 3 | metadata: 4 | name: "redis-enterprise" 5 | spec: 6 | # Add fields here 7 | nodes: 3 8 | persistentSpec: 9 | enabled: true 10 | uiServiceType: LoadBalancer 11 | username: admin@demo.com 12 | servicesRiggerSpec: 13 | databaseServiceType: load_balancer,cluster_ip,headless 14 | --- 15 | -------------------------------------------------------------------------------- /kubernetes/redis-demo-movie-database-crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: app.redislabs.com/v1alpha1 2 | kind: RedisEnterpriseDatabase 3 | metadata: 4 | name: movie-database 5 | spec: 6 | modulesList: 7 | - name: graph 8 | version: 2.0.19 9 | - name: search 10 | version: 2.0.0 11 | memorySize: 200MB 12 | persistence: aofEverySecond 13 | replication: true 14 | redisEnterpriseCluster: 15 | name: redis-enterprise 16 | -------------------------------------------------------------------------------- /mysql-database/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mysql:5.7 2 | 3 | LABEL maintainer="Debezium Community & Tugdual Grall" 4 | 5 | ENV MYSQL_ROOT_PASSWORD=debezium 6 | ENV MYSQL_USER=mysqluser 7 | ENV MYSQL_PASSWORD=mysqlpw 8 | 9 | EXPOSE 3306 10 | 11 | COPY mysql.cnf /etc/mysql/conf.d/ 12 | COPY import-data.sql /docker-entrypoint-initdb.d/ 13 | 14 | 15 | -------------------------------------------------------------------------------- /mysql-database/mysql.cnf: -------------------------------------------------------------------------------- 1 | # For advice on how to change settings please see 2 | # http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html 3 | 4 | [mysqld] 5 | # 6 | # Remove leading # and set to the amount of RAM for the most important data 7 | # cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%. 8 | # innodb_buffer_pool_size = 128M 9 | # 10 | # Remove leading # to turn on a very important data integrity option: logging 11 | # changes to the binary log between backups. 12 | # log_bin 13 | # 14 | # Remove leading # to set options mainly useful for reporting servers. 15 | # The server defaults are faster for transactions and fast SELECTs. 16 | # Adjust sizes as needed, experiment to find the optimal values. 17 | # join_buffer_size = 128M 18 | # sort_buffer_size = 2M 19 | # read_rnd_buffer_size = 2M 20 | skip-host-cache 21 | skip-name-resolve 22 | #datadir=/var/lib/mysql 23 | #socket=/var/lib/mysql/mysql.sock 24 | #secure-file-priv=/var/lib/mysql-files 25 | user=mysql 26 | 27 | # Disabling symbolic-links is recommended to prevent assorted security risks 28 | symbolic-links=0 29 | 30 | #log-error=/var/log/mysqld.log 31 | #pid-file=/var/run/mysqld/mysqld.pid 32 | 33 | # ---------------------------------------------- 34 | # Enable the binlog for replication & CDC 35 | # ---------------------------------------------- 36 | 37 | # Enable binary replication log and set the prefix, expiration, and log format. 38 | # The prefix is arbitrary, expiration can be short for integration tests but would 39 | # be longer on a production system. Row-level info is required for ingest to work. 40 | # Server ID is required, but this will vary on production systems 41 | server-id = 223344 42 | log_bin = mysql-bin 43 | expire_logs_days = 1 44 | binlog_format = row 45 | 46 | -------------------------------------------------------------------------------- /notifications-service-node/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | COPY . . 9 | 10 | EXPOSE 8888 11 | 12 | CMD [ "node", "server.js", "--CONF_FILE", "./config.prod.json" ] 13 | 14 | 15 | -------------------------------------------------------------------------------- /notifications-service-node/config.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "PORT": "8888", 3 | "REDIS_HOST" : "localhost", 4 | "REDIS_PORT" : "6379", 5 | "REDIS_PASSWORD" : "" 6 | } -------------------------------------------------------------------------------- /notifications-service-node/config.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "PORT": "8888", 3 | "REDIS_HOST" : "redis-service", 4 | "REDIS_PORT" : "6379", 5 | "REDIS_PASSWORD" : "" 6 | } -------------------------------------------------------------------------------- /notifications-service-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notifications-service-node", 3 | "version": "1.0.0", 4 | "description": "WebSocket Server for Redis Microservice Demo Application", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "nconf": "^0.10.0", 13 | "redis": "^3.1.1", 14 | "ws": "^7.2.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /notifications-service-node/server.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | nconf = require('nconf'), 3 | redis = require('redis'); 4 | 5 | nconf.argv(); 6 | nconf.env(); 7 | 8 | nconf.file({ file: (nconf.get('CONF_FILE') || './config.dev.json' ) }); 9 | console.log(`... starting node host :\n\t port: ${nconf.get("PORT")} \n\t Redis : ${nconf.get("REDIS_HOST")}:${nconf.get("REDIS_PORT")}`); 10 | 11 | var db = redis.createClient(nconf.get("REDIS_PORT"), nconf.get("REDIS_HOST")); 12 | if (nconf.get('REDIS_PASSWORD')) { 13 | db.auth(nconf.get("REDIS_PASSWORD")); 14 | } 15 | 16 | // Subscribe to all microservices notifications channel 17 | db.subscribe('ms:notifications'); 18 | 19 | const wss = new WebSocket.Server({ port: nconf.get('PORT') }); 20 | 21 | wss.on('connection', function connection(ws) { 22 | 23 | ws.on('message', function incoming(message) { 24 | console.log('received: %s', message); 25 | }); 26 | 27 | db.on('message', function(channel, message) { 28 | ws.send(message); 29 | }); 30 | 31 | }); 32 | 33 | 34 | console.log('listening at ws://localhost:' + nconf.get('PORT')); -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.2.5.RELEASE 10 | 11 | 12 | 13 | io.redis.demos 14 | cache-invalidation-cdc-debezium 15 | pom 16 | 1.0-SNAPSHOT 17 | 18 | caching-service 19 | db-to-streams-service 20 | sql-rest-api 21 | streams-to-redisearch-service 22 | streams-to-redisgraph-service 23 | 24 | 25 | 26 | 1.8 27 | 3.4.1 28 | 2.2.0 29 | 2.0.0 30 | 1.1.0.Final 31 | 2.2.5.RELEASE 32 | 1 33 | 34 | 35 | 36 | 37 | 38 | snapshots-repo 39 | https://oss.sonatype.org/content/repositories/snapshots 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | org.apache.maven.plugins 48 | maven-compiler-plugin 49 | 3.8.1 50 | 51 | 1.8 52 | 1.8 53 | 54 | 55 | org.projectlombok 56 | lombok 57 | 1.18.12 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /redis-gears-recipes/movies-to-pubsub.py: -------------------------------------------------------------------------------- 1 | gb = GearsBuilder('StreamReader') 2 | gb.foreach(lambda x: execute('PUBLISH', 'ms:notifications', '{ "id":"'+ x["value"]['movie_id'] +'", "title":"'+ x["value"]['title'] +'", "type":"movie"}')) 3 | gb.register("events:inventory:movies") 4 | -------------------------------------------------------------------------------- /redis-server/Dockerfile: -------------------------------------------------------------------------------- 1 | # a customized version of https://hub.docker.com/r/redislabs/redismod/dockerfile 2 | 3 | FROM redislabs/redisearch:2.0.5 as redisearch 4 | FROM redislabs/redisgraph:2.2.11 as redisgraph 5 | FROM redislabs/redisgears:1.0.5 6 | 7 | ENV LD_LIBRARY_PATH /usr/lib/redis/modules 8 | ENV REDISGRAPH_DEPS libgomp1 9 | 10 | WORKDIR /data 11 | RUN set -ex; \ 12 | apt-get update; \ 13 | apt-get install -y --no-install-recommends ${REDISGRAPH_DEPS}; 14 | 15 | COPY --from=redisearch ${LD_LIBRARY_PATH}/redisearch.so ${LD_LIBRARY_PATH}/ 16 | COPY --from=redisgraph ${LD_LIBRARY_PATH}/redisgraph.so ${LD_LIBRARY_PATH}/ 17 | 18 | # ENV PYTHONPATH /usr/lib/redis/modules/deps/cpython/Lib 19 | ENTRYPOINT ["redis-server"] 20 | CMD ["--loadmodule", "/usr/lib/redis/modules/redisearch.so", \ 21 | "--loadmodule", "/usr/lib/redis/modules/redisgraph.so", \ 22 | "--loadmodule", "/var/opt/redislabs/lib/modules/redisgears.so", \ 23 | "PythonHomeDir", "/opt/redislabs/lib/modules/python3", \ 24 | "--appendonly", "yes" ] 25 | 26 | -------------------------------------------------------------------------------- /sql-rest-api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | 3 | COPY target/*.jar /app.jar 4 | 5 | ENV SPRING_DATASOURCE_URL=jdbc:mysql://app-mysql:3306/inventory 6 | ENV SPRING_DATASOURCE_USERNAME=mysqluser 7 | ENV SPRING_DATASOURCE_PASSWORD=mysqlpw 8 | 9 | EXPOSE 8081 10 | ENTRYPOINT ["java","-jar","/app.jar","--spring.profiles.active=prod"] 11 | 12 | 13 | -------------------------------------------------------------------------------- /sql-rest-api/README.md: -------------------------------------------------------------------------------- 1 | # SQL Data Generator 2 | 3 | This project is simply used to generate SQL Queries that, insert, update, delete data in MySQL (or ohter DB), 4 | to generate event in Debezium & Redis -------------------------------------------------------------------------------- /sql-rest-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | cache-invalidation-cdc-debezium 7 | io.redis.demos 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | sql-rest-api 13 | 14 | 15 | 16 | 17 | mysql 18 | mysql-connector-java 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-jdbc 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-web 30 | 31 | 32 | 33 | org.projectlombok 34 | lombok 35 | 36 | 37 | 38 | javax.inject 39 | javax.inject 40 | ${version.javax.inject} 41 | 42 | 43 | 44 | 45 | 46 | 47 | snapshots-repo 48 | https://oss.sonatype.org/content/repositories/snapshots 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-maven-plugin 59 | 60 | 61 | 62 | org.apache.maven.plugins 63 | maven-compiler-plugin 64 | 3.8.1 65 | 66 | 1.8 67 | 1.8 68 | 69 | 70 | org.projectlombok 71 | lombok 72 | 1.18.12 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /sql-rest-api/src/main/java/io/redis/demos/debezium/sql/Application.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.debezium.sql; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.CommandLineRunner; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.jdbc.core.JdbcTemplate; 9 | 10 | import java.util.concurrent.Executors; 11 | import java.util.concurrent.ScheduledExecutorService; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | @Slf4j 15 | @SpringBootApplication 16 | public class Application implements CommandLineRunner { 17 | 18 | 19 | public static void main(String[] args) { 20 | SpringApplication.run(Application.class, args); 21 | } 22 | 23 | @Override 24 | public void run(String... args) throws Exception { 25 | log.info("Start Application..."); 26 | log.info("V101"); 27 | } 28 | 29 | 30 | } 31 | -------------------------------------------------------------------------------- /sql-rest-api/src/main/java/io/redis/demos/debezium/sql/controllers/ActorsAPIController.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.debezium.sql.controllers; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.jdbc.core.JdbcTemplate; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.concurrent.Executors; 13 | import java.util.concurrent.ScheduledExecutorService; 14 | import java.util.concurrent.TimeUnit; 15 | 16 | @Slf4j 17 | @CrossOrigin(origins = "*") 18 | @RequestMapping("/api/1.0/sql-rest-api/actors") 19 | @RestController 20 | public class ActorsAPIController { 21 | 22 | @Autowired 23 | JdbcTemplate jdbcTemplate; 24 | 25 | 26 | @GetMapping 27 | public List> getActors( 28 | @RequestParam(name="last_name", defaultValue="1") String orderBy, 29 | @RequestParam(name="page", defaultValue="1") int page) { 30 | List> actors = new ArrayList<>(); 31 | int offset = (page - 1) * 50; 32 | actors = jdbcTemplate.queryForList("SELECT * FROM actors ORDER BY ? LIMIT 50 OFFSET ?", orderBy, offset ); 33 | return actors; 34 | } 35 | 36 | @GetMapping("/{id}") 37 | public Map findById(@PathVariable Long id) { 38 | Map actor = new HashMap<>(); 39 | actor = jdbcTemplate.queryForMap("SELECT * FROM actors where actor_id = ?", id ); 40 | return actor; 41 | } 42 | 43 | @PostMapping("/") 44 | public Map save(@RequestBody Map record) { 45 | Map actor = new HashMap<>(); 46 | int rows = jdbcTemplate.update( 47 | "UPDATE actors SET first_name = ?, last_name = ?, dob = ? WHERE actor_id = ?", 48 | record.get("first_name"), 49 | record.get("last_name"), 50 | record.get("dob"), 51 | record.get("actor_id") 52 | ); 53 | 54 | log.info("Actor Updated {} - {} row", record, rows); 55 | return record; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /sql-rest-api/src/main/java/io/redis/demos/debezium/sql/controllers/MoviesAPIController.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.debezium.sql.controllers; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.jdbc.core.JdbcTemplate; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | @Slf4j 14 | @CrossOrigin(origins = "*") 15 | @RequestMapping("/api/1.0/sql-rest-api/movies") 16 | @RestController 17 | public class MoviesAPIController { 18 | 19 | @Autowired 20 | JdbcTemplate jdbcTemplate; 21 | 22 | 23 | @GetMapping 24 | public List> getMovies( 25 | @RequestParam(name="title", defaultValue="1") String orderBy, 26 | @RequestParam(name="page", defaultValue="1") int page) { 27 | List> movies = new ArrayList<>(); 28 | int offset = (page - 1) * 50; 29 | movies = jdbcTemplate.queryForList("SELECT * FROM movies ORDER BY ? LIMIT 50 OFFSET ?", orderBy, offset ); 30 | return movies; 31 | } 32 | 33 | 34 | @GetMapping("/{id}") 35 | public Map findById(@PathVariable Long id) { 36 | Map movie = new HashMap<>(); 37 | movie = jdbcTemplate.queryForMap("SELECT * FROM movies where movie_id = ?", id ); 38 | return movie; 39 | } 40 | 41 | @GetMapping("/{id}/actors") 42 | public List> getMovieActors(@PathVariable Long id) { 43 | log.info("getMovieActors - {}", id); 44 | List> actors = new ArrayList<>(); 45 | String query = "SELECT actors.* FROM actors, movies_actors "+ 46 | "WHERE movies_actors.movie_id = ? " + 47 | "AND movies_actors.actor_id = actors.actor_id"; 48 | actors = jdbcTemplate.queryForList(query, id ); 49 | return actors; 50 | } 51 | 52 | 53 | @PostMapping("/") 54 | public Map save(@RequestBody Map record) { 55 | log.info(record.toString()); 56 | Map actor = new HashMap<>(); 57 | int rows = jdbcTemplate.update( 58 | "UPDATE movies SET title = ?, genre = ?, votes = ?, rating = ?, release_year = ? , plot = ? WHERE movie_id = ?", 59 | record.get("title"), 60 | record.get("genre"), 61 | record.get("votes"), 62 | record.get("rating"), 63 | record.get("release_year"), 64 | record.get("plot"), 65 | record.get("movie_id") 66 | ); 67 | 68 | log.info("Movie Updated {} - {} row", record, rows); 69 | return record; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /sql-rest-api/src/main/java/io/redis/demos/debezium/sql/controllers/RestStatusController.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.debezium.sql.controllers; 2 | 3 | import io.redis.demos.debezium.sql.services.DataGeneratorService; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.jdbc.core.JdbcTemplate; 7 | import org.springframework.web.bind.annotation.CrossOrigin; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import javax.inject.Inject; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import java.util.concurrent.Executors; 16 | import java.util.concurrent.ScheduledExecutorService; 17 | import java.util.concurrent.TimeUnit; 18 | 19 | @Slf4j 20 | @CrossOrigin(origins = "*") 21 | @RequestMapping("/api/1.0/sql-rest-api/") 22 | @RestController 23 | public class RestStatusController { 24 | 25 | @Autowired 26 | JdbcTemplate jdbcTemplate; 27 | 28 | @Inject 29 | DataGeneratorService dataGeneratorService; 30 | 31 | ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); 32 | 33 | @GetMapping("/status") 34 | public Map status() { 35 | 36 | Map result = new HashMap<>(); 37 | 38 | result.put("service", "DataOperationGeneratorApplication"); 39 | result.put("status", dataGeneratorService.getState()); 40 | result.put("version", "1.0"); 41 | 42 | return result; 43 | } 44 | 45 | @GetMapping("/start") 46 | public Map start() { 47 | dataGeneratorService.start(); 48 | Map result = new HashMap<>(); 49 | result.put("service", "DataOperationGeneratorApplication"); 50 | result.put("operation", "start"); 51 | return result; 52 | } 53 | 54 | @GetMapping("/stop") 55 | public Map stop() { 56 | dataGeneratorService.stop(); 57 | Map result = new HashMap<>(); 58 | result.put("service", "DataOperationGeneratorApplication"); 59 | result.put("operation", "start"); 60 | return result; 61 | } 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /sql-rest-api/src/main/java/io/redis/demos/debezium/sql/services/DataGeneratorService.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.debezium.sql.services; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.jdbc.core.JdbcTemplate; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.Map; 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.ScheduledExecutorService; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | @Slf4j 14 | @Service 15 | public class DataGeneratorService { 16 | 17 | @Autowired 18 | JdbcTemplate jdbcTemplate; 19 | 20 | private String status="STOPPED"; 21 | ScheduledExecutorService executorService = null; 22 | 23 | public DataGeneratorService() { 24 | } 25 | 26 | public void start(){ 27 | log.info("Starting data generator...."); 28 | executorService = Executors.newSingleThreadScheduledExecutor(); 29 | executorService.scheduleAtFixedRate(this::callBusinessLogic, 5, 10, TimeUnit.SECONDS ); 30 | status="RUNNING"; 31 | } 32 | 33 | public void stop(){ 34 | log.info("Stopping data generator...."); 35 | executorService.shutdownNow(); 36 | executorService = null; 37 | status="STOPPED"; 38 | } 39 | 40 | public String getState() { 41 | return this.status; 42 | } 43 | 44 | 45 | private void callBusinessLogic() { 46 | log.info("=== CALL BUSINESS LOGIC ==="); 47 | 48 | executorService.schedule(() -> 49 | createNewCustomer(1005, "John", "Doe", "jdoe@email.com"), 50 | 3 , TimeUnit.SECONDS); 51 | 52 | executorService.schedule(() -> 53 | addNewOrder(1002, 10, 109, "2019-04-01"), 54 | 6 , TimeUnit.SECONDS); 55 | 56 | 57 | executorService.schedule(() -> 58 | createNewCustomer(1006, "Jane", "Simon", "jsimon@email.com"), 59 | 12 , TimeUnit.SECONDS); 60 | 61 | executorService.schedule(() -> 62 | addNewOrder(1001, 8, 101, "2019-04-03"), 63 | 12 , TimeUnit.SECONDS); 64 | 65 | 66 | executorService.schedule(() -> 67 | createNewCustomer(1007, "Richard", "Dreyfus", "rdreyfus@email.com"), 68 | 15 , TimeUnit.SECONDS); 69 | 70 | 71 | executorService.schedule(() -> 72 | addNewOrder(1001, 8, 101, "2019-04-03"), 73 | 19 , TimeUnit.SECONDS); 74 | } 75 | 76 | private void createNewCustomer( int id, String firstName, String lastName, String email ) { 77 | log.info(" == Delete/Create Customers =="); 78 | 79 | // Delete customer 1005, then recreate it 80 | jdbcTemplate.execute( String.format("DELETE FROM customers WHERE id = %s ", id) ); 81 | jdbcTemplate.execute(String.format("INSERT INTO customers (id, first_name, last_name, email) "+ 82 | "VALUES (%s, '%s', '%s', '%s')", id, firstName, lastName, email)); 83 | 84 | 85 | } 86 | 87 | private void addNewOrder( int purchaser, int quantity, int product_id, String orderDate ) { 88 | log.info(" == Create Order =="); 89 | 90 | jdbcTemplate.execute( 91 | String.format("INSERT INTO orders (order_date, purchaser, quantity, product_id) VALUES ('%s', %s, %s, %s);", 92 | orderDate, purchaser, quantity, product_id)); 93 | 94 | 95 | } 96 | 97 | 98 | } 99 | -------------------------------------------------------------------------------- /sql-rest-api/src/main/resources/application-prod.properties: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## MySQL 4 | spring.datasource.url=jdbc:mysql://app-mysql:3306/inventory 5 | spring.datasource.username=mysqluser 6 | spring.datasource.password=mysqlpw 7 | 8 | 9 | server.port=8081 10 | -------------------------------------------------------------------------------- /sql-rest-api/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## MySQL 4 | spring.datasource.url=jdbc:mysql://app-mysql:3306/inventory 5 | spring.datasource.username=mysqluser 6 | spring.datasource.password=mysqlpw 7 | 8 | 9 | server.port=8081 10 | 11 | 12 | -------------------------------------------------------------------------------- /streams-to-redisearch-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | 3 | COPY target/*.jar /app.jar 4 | 5 | ENV REDIS_HOST=redis-service 6 | ENV REDIS_PORT=6379 7 | ENV REDIS_PASSWORD= 8 | 9 | EXPOSE 8085 10 | ENTRYPOINT ["java","-jar","/app.jar","--spring.profiles.active=prod"] 11 | 12 | 13 | -------------------------------------------------------------------------------- /streams-to-redisearch-service/README.md: -------------------------------------------------------------------------------- 1 | # Redis Streams to RediSearch 2 | 3 | This service: 4 | 5 | * Reads various Redis Streams and update RediSearch index and/or Suggestion key 6 | * Exposes the search as REST API Endpoint 7 | 8 | 9 | ## Sample Queries: 10 | 11 | Once you have consumes the data into the index and suggestion you can do the following commands in Redis Insight or CLI. 12 | 13 | Keys for Suggestion: 14 | 15 | * `"ms:search:suggest:movies"` 16 | * `"ms:search:suggest:actors"` 17 | 18 | Full text indices: 19 | 20 | * `"ms:search:index:movies"` 21 | * `"ms:search:index:actors"` 22 | 23 | ### Sample Autocomplete queries: 24 | 25 | ``` 26 | > FT.SUGGET "ms:search:suggest:movies" jo 27 | 28 | > FT.SUGGET "ms:search:suggest:movies" jo FUZZY 29 | 30 | > FT.SUGGET "ms:search:suggest:actors" jo 31 | 32 | > FT.SUGGET "ms:search:suggest:actors" jo FUZZY 33 | 34 | ``` 35 | 36 | You can test the same queries using the UI, or a REST call: 37 | 38 | ``` 39 | $ curl hhttp://localhost:8085/api/1.0/data-streams-to-autocomplete/actors/autocomplete?q=Jo 40 | 41 | $ http://localhost:8085/api/1.0/data-streams-to-autocomplete/movies/autocomplete?q=Jo 42 | ``` 43 | 44 | ### Full Text Query 45 | 46 | ``` 47 | 48 | > FT.SEARCH "ms:search:index:movies" "star" LIMIT 0 10 49 | 50 | 51 | > FT.SEARCH "ms:search:index:actors" "john" LIMIT 0 10 52 | 53 | 54 | ``` 55 | 56 | **Aggregations** 57 | 58 | ``` 59 | > FT.AGGREGATE "ms:search:index:movies" "*" GROUPBY 1 @release_year REDUCE COUNT 0 AS releases 60 | 61 | ``` -------------------------------------------------------------------------------- /streams-to-redisearch-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | cache-invalidation-cdc-debezium 7 | io.redis.demos 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | streams-to-redisearch-service 13 | 14 | 15 | 16 | 17 | 18 | com.redislabs 19 | jredisearch 20 | ${version.jredisearch} 21 | 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | 31 | org.projectlombok 32 | lombok 33 | 34 | 35 | 36 | javax.inject 37 | javax.inject 38 | ${version.javax.inject} 39 | 40 | 41 | 42 | 43 | 44 | 45 | snapshots-repo 46 | https://oss.sonatype.org/content/repositories/snapshots 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-maven-plugin 57 | 58 | 59 | 60 | org.apache.maven.plugins 61 | maven-compiler-plugin 62 | 3.8.1 63 | 64 | 1.8 65 | 1.8 66 | 67 | 68 | org.projectlombok 69 | lombok 70 | 1.18.12 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /streams-to-redisearch-service/src/main/java/io/redis/demos/services/search/service/RestStatusController.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.search.service; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.web.bind.annotation.*; 5 | 6 | import javax.inject.Inject; 7 | import java.util.*; 8 | 9 | @Slf4j 10 | @CrossOrigin(origins = "*") 11 | @RequestMapping("/api/1.0/search-service/") 12 | @RestController 13 | public class RestStatusController { 14 | 15 | @Inject StreamsToRediSearch redisService; 16 | 17 | @GetMapping("/{object}/autocomplete") 18 | public List> autoComplete(@PathVariable(name = "object")String object, @RequestParam(name="q")String term) { 19 | return redisService.suggest(object, term); 20 | } 21 | 22 | @GetMapping("/search/{object}") 23 | public Map search(@PathVariable(name = "object")String object, @RequestParam(name="q")String term) { 24 | return redisService.search(object, term); 25 | } 26 | 27 | @GetMapping("/search-with-pagination") 28 | public Map searchWithPagination( 29 | @RequestParam(name="q")String query, 30 | @RequestParam(name="offset", defaultValue="0")int offset, 31 | @RequestParam(name="limit", defaultValue="10")int limit, 32 | @RequestParam(name="sortby", defaultValue="")String sortBy, 33 | @RequestParam(name="ascending", defaultValue="true")boolean ascending) { 34 | return redisService.searchWithPagination(query, offset, limit, sortBy,ascending); 35 | } 36 | 37 | @GetMapping("/status") 38 | public Map status() { 39 | Map result = new HashMap<>(); 40 | result.put("service", "SearchServiceApplication"); 41 | result.put("status", redisService.getState()); 42 | result.put("suggest", String.valueOf(redisService.isSuggest())); 43 | result.put("fulltext", String.valueOf(redisService.isFulltext())); 44 | result.put("version", "1.0"); 45 | return result; 46 | } 47 | 48 | @GetMapping("/start") 49 | public Map start() { 50 | Map result = new HashMap<>(); 51 | result.put("service", "SearchServiceApplication"); 52 | result.put("action", "start"); 53 | Map call = redisService.processStream(); 54 | result.putAll(call); 55 | return result; 56 | } 57 | 58 | @GetMapping("/stop") 59 | public Map stop() { 60 | Map result = new HashMap<>(); 61 | result.put("service", "SearchServiceApplication"); 62 | result.put("action", "stop"); 63 | Map call = redisService.stopProcessStream(); 64 | result.putAll(call); 65 | return result; 66 | } 67 | 68 | @GetMapping("/config/{object}/index-info") 69 | public Map getInfoIndex(@PathVariable(name = "object")String object) { 70 | return redisService.getInfoIndex(object); 71 | } 72 | 73 | @GetMapping("/config/fulltext") 74 | public Map configureFullText() { 75 | Map result = new HashMap<>(); 76 | result.put("service", "SearchServiceApplication"); 77 | result.put("action", "configureFullText"); 78 | Map call = redisService.changeFullText(); 79 | result.putAll(call); 80 | return result; 81 | } 82 | 83 | @GetMapping("/config/autocomplete") 84 | public Map configureAutocomplete() { 85 | Map result = new HashMap<>(); 86 | result.put("service", "SearchServiceApplication"); 87 | result.put("action", "configureFullText"); 88 | Map call = redisService.changeSuggest(); 89 | result.putAll(call); 90 | return result; 91 | } 92 | 93 | @GetMapping("/stats/movies/all") 94 | public Map allStats(@RequestParam(name="sort", defaultValue="year")String orderBy) { 95 | Map result = new HashMap<>(); 96 | Map stats = redisService.getMovieByYear(orderBy, 100); 97 | result.put("movieByYear", stats); 98 | stats = redisService.getMovieByGenre("genre", 100); 99 | result.put("movieByGenre", stats); 100 | return result; 101 | } 102 | 103 | @GetMapping("/movies/genres") 104 | public Map getAllGenres() { 105 | Map result = new HashMap<>(); 106 | result = redisService.getAllGenres(); 107 | return result; 108 | } 109 | 110 | @GetMapping("/movies/group_by/{field}") 111 | public Map getMovieGroupBy(@PathVariable("field") String field) { 112 | return redisService.getMovieGroupBy(field); 113 | } 114 | 115 | @GetMapping("/movies/{id}") 116 | public Map getMovieById(@PathVariable("id") String id) { 117 | return redisService.getMovieById(id); 118 | } 119 | 120 | @GetMapping("/actors/{id}") 121 | public Map getActorById(@PathVariable("id") String id) { 122 | return redisService.getActorById(id); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /streams-to-redisearch-service/src/main/java/io/redis/demos/services/search/service/SearchServiceApplication.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.search.service; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @Slf4j 8 | @SpringBootApplication 9 | public class SearchServiceApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(SearchServiceApplication.class, args); 13 | } 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /streams-to-redisearch-service/src/main/java/io/redis/demos/services/search/service/schemas/ActorsSchema.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.search.service.schemas; 2 | 3 | import io.redisearch.Schema; 4 | 5 | public class ActorsSchema { 6 | 7 | public final static String ACTOR_ID = "actor_id"; 8 | public final static String FIRST_NAME = "first_name"; 9 | public final static String LAST_NAME = "last_name"; 10 | public final static String DOB = "dob"; 11 | 12 | public static Schema getSchema() { 13 | return new Schema() 14 | .addTextField(FIRST_NAME, 5.0) 15 | .addTextField(LAST_NAME, 5.0); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /streams-to-redisearch-service/src/main/java/io/redis/demos/services/search/service/schemas/CommentsSchema.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.search.service.schemas; 2 | 3 | import io.redisearch.Schema; 4 | 5 | public class CommentsSchema { 6 | 7 | public final static String MOVIE_ID = "movie_id"; 8 | public final static String TIMESTAMP = "timestamp"; 9 | public final static String USER_ID = "user_id"; 10 | public final static String COMMENT = "comment"; 11 | public final static String RATING = "rating"; 12 | 13 | public static Schema getSchema() { 14 | return new Schema() 15 | .addNumericField(MOVIE_ID) 16 | .addSortableNumericField(TIMESTAMP) 17 | .addSortableTagField(USER_ID, ",") 18 | .addTextField(COMMENT, 1.0) 19 | .addSortableNumericField(RATING); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /streams-to-redisearch-service/src/main/java/io/redis/demos/services/search/service/schemas/KeysPrefix.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.search.service.schemas; 2 | 3 | abstract public class KeysPrefix { 4 | 5 | public final static String DOC_PREFIX = "ms:docs:"; 6 | public final static String SUGGEST_PREFIX = "ms:search:suggest:"; 7 | public final static String SEARCH_INDEX_PREFIX = "ms:search:index:"; 8 | 9 | 10 | public String getKeyForDoc(String item, Object id) { 11 | return DOC_PREFIX.concat(item).concat(":").concat(id.toString()); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /streams-to-redisearch-service/src/main/java/io/redis/demos/services/search/service/schemas/MoviesSchema.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.search.service.schemas; 2 | 3 | import io.redisearch.Schema; 4 | import io.redisearch.client.Client; 5 | import redis.clients.jedis.Jedis; 6 | import redis.clients.jedis.exceptions.JedisDataException; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.Objects; 11 | 12 | public class MoviesSchema { 13 | 14 | public final static String MOVIE_ID = "movie_id"; 15 | public final static String TITLE = "title"; 16 | public final static String GENRE = "genre"; 17 | public final static String VOTES = "votes"; 18 | public final static String RATING = "rating"; 19 | public final static String RELEASE_YEAR = "release_year"; 20 | public final static String PLOT = "plot"; 21 | public final static String POSTER = "poster"; 22 | 23 | public static Schema getSchema() { 24 | return new Schema() 25 | .addTextField(TITLE, 5.0) 26 | .addSortableTextField(PLOT, 1.0) 27 | .addSortableTagField(GENRE, ",") 28 | .addSortableNumericField(RELEASE_YEAR) 29 | .addSortableNumericField(RATING) 30 | .addSortableNumericField(VOTES); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /streams-to-redisearch-service/src/main/java/io/redis/demos/services/search/service/util/AutoCompleter.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.search.service.util; 2 | 3 | import redis.clients.jedis.commands.ProtocolCommand; 4 | import redis.clients.jedis.util.SafeEncoder; 5 | 6 | /** 7 | * TODO: Abstract auto-complete commands 8 | */ 9 | public class AutoCompleter { 10 | 11 | public enum Command implements ProtocolCommand { 12 | SUGADD("FT.SUGADD"), 13 | SUGGET("FT.SUGGET"), 14 | SUGDEL("FT.SUGDEL"), 15 | SUGLEN("FT.SUGLEN"); 16 | 17 | private final byte[] raw; 18 | 19 | Command(String alt) { 20 | raw = SafeEncoder.encode(alt); 21 | } 22 | 23 | @Override 24 | public byte[] getRaw() { 25 | return raw; 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /streams-to-redisearch-service/src/main/resources/application-prod.properties: -------------------------------------------------------------------------------- 1 | 2 | ## Redis Graph & Streams properties 3 | ## TODO : support multiple DB 4 | 5 | redis.host=redis-service 6 | redis.port=6379 7 | redis.password= 8 | 9 | redis.streams=events:inventory:movies,events:inventory:actors 10 | redis.groupname=cdc.data.autocomplete 11 | redis.consumer=cons:1 12 | 13 | # The suffix is used to automatically build the autocomplete service 14 | redis.autocomplete.key=ms:search:suggest:movies,ms:search:suggest:actors 15 | redis.search.index=ms:search:index:movies,ms:search:index:actors 16 | 17 | 18 | 19 | 20 | server.port=8085 21 | -------------------------------------------------------------------------------- /streams-to-redisearch-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | ## Redis Search & Streams properties 3 | ## TODO : support multiple DB 4 | redis.host=localhost 5 | redis.port=6379 6 | redis.password= 7 | 8 | redis.streams=events:inventory:movies,events:inventory:actors 9 | redis.groupnameprefix=cdc.search 10 | redis.consumer=cons:1 11 | 12 | # The suffix is used to automatically build the autocomplete service 13 | redis.autocomplete.key=ms:search:suggest:movies,ms:search:suggest:actors 14 | redis.search.index=ms:search:index:movies,ms:search:index:actors 15 | 16 | server.port=8085 17 | 18 | -------------------------------------------------------------------------------- /streams-to-redisgraph-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | 3 | COPY target/*.jar /app.jar 4 | 5 | ENV REDIS_HOST=redis-service 6 | ENV REDIS_PORT=6379 7 | ENV REDIS_PASSWORD= 8 | 9 | EXPOSE 8083 10 | ENTRYPOINT ["java","-jar","/app.jar","--spring.profiles.active=prod"] 11 | 12 | 13 | -------------------------------------------------------------------------------- /streams-to-redisgraph-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | cache-invalidation-cdc-debezium 7 | io.redis.demos 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | streams-to-redisgraph-service 13 | 14 | 15 | 16 | 17 | com.redislabs 18 | jredisgraph 19 | ${version.jredisgraph} 20 | 21 | 22 | 23 | 24 | mysql 25 | mysql-connector-java 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-jdbc 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-web 37 | 38 | 39 | 40 | org.projectlombok 41 | lombok 42 | 43 | 44 | 45 | javax.inject 46 | javax.inject 47 | ${version.javax.inject} 48 | 49 | 50 | 51 | 52 | 53 | 54 | snapshots-repo 55 | https://oss.sonatype.org/content/repositories/snapshots 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-maven-plugin 66 | 67 | 68 | 69 | org.apache.maven.plugins 70 | maven-compiler-plugin 71 | 3.8.1 72 | 73 | 1.8 74 | 1.8 75 | 76 | 77 | org.projectlombok 78 | lombok 79 | 1.18.12 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /streams-to-redisgraph-service/src/main/java/io/redis/demos/services/graph/service/GraphServiceApplication.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.graph.service; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.boot.CommandLineRunner; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | 8 | @Slf4j 9 | @SpringBootApplication 10 | public class GraphServiceApplication implements CommandLineRunner { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(GraphServiceApplication.class, args); 14 | } 15 | 16 | @Override 17 | public void run(String... args) throws Exception { 18 | log.info("=== Application Started ==="); 19 | 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /streams-to-redisgraph-service/src/main/java/io/redis/demos/services/graph/service/RestStatusController.java: -------------------------------------------------------------------------------- 1 | package io.redis.demos.services.graph.service; 2 | 3 | 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.web.bind.annotation.*; 6 | 7 | import javax.inject.Inject; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | @Slf4j 12 | @CrossOrigin(origins = "*") 13 | @RequestMapping("/api/1.0/graph-service/") 14 | @RestController 15 | public class RestStatusController { 16 | 17 | @Inject 18 | RedisStreamToGraphService redisService; 19 | 20 | @GetMapping("/status") 21 | public Map status() { 22 | 23 | Map result = new HashMap<>(); 24 | 25 | result.put("service", "GraphServiceApplication"); 26 | result.put("status", redisService.getState()); 27 | result.put("version", "1.0"); 28 | 29 | return result; 30 | } 31 | 32 | @GetMapping("/start") 33 | public Map start() { 34 | Map result = new HashMap<>(); 35 | result.put("service", "GraphServiceApplication"); 36 | result.put("action", "start"); 37 | Map call = redisService.processStream(); 38 | result.putAll(call); 39 | return result; 40 | } 41 | 42 | @GetMapping("/stop") 43 | public Map stop() { 44 | Map result = new HashMap<>(); 45 | result.put("service", "GraphServiceApplication"); 46 | result.put("action", "stop"); 47 | Map call = redisService.stopProcessStream(); 48 | result.putAll(call); 49 | return result; 50 | } 51 | 52 | @GetMapping("/relationship/{type}/{id}") 53 | public Map findRelationshipsFromRDBMS(@PathVariable String type, @PathVariable int id) { 54 | Map result = new HashMap<>(); 55 | result.put("service", "GraphServiceApplication"); 56 | result.put("action", "Refresh Relationships"); 57 | redisService.createRelationFromMySQL(type, id); 58 | return result; 59 | } 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /streams-to-redisgraph-service/src/main/resources/application-prod.properties: -------------------------------------------------------------------------------- 1 | 2 | ## Redis Graph & Streams properties 3 | ## TODO : support multiple DB 4 | redis.host=redis-service 5 | redis.port=6379 6 | redis.password= 7 | 8 | redis.streams=events:inventory:movies,events:inventory:actors 9 | redis.groupname=cdc.data.graph 10 | redis.consumer=cons:1 11 | redis.graphname=imdb 12 | 13 | 14 | ## MySQL 15 | spring.datasource.url=jdbc:mysql://app-mysql:3306/inventory 16 | spring.datasource.username=mysqluser 17 | spring.datasource.password=mysqlpw 18 | 19 | 20 | server.port=8083 21 | -------------------------------------------------------------------------------- /streams-to-redisgraph-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | ## Redis Graph & Streams properties 3 | ## TODO : support multiple DB 4 | 5 | redis.host=localhost 6 | redis.port=6379 7 | redis.password= 8 | 9 | redis.streams=events:inventory:movies,events:inventory:actors 10 | redis.groupname=cdc.data.graph 11 | redis.consumer=cons:1 12 | redis.graphname=imdb 13 | 14 | 15 | ## MySQL 16 | spring.datasource.url=jdbc:mysql://127.0.0.1:3306/inventory 17 | spring.datasource.username=mysqluser 18 | spring.datasource.password=mysqlpw 19 | 20 | server.port=8083 21 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | /tests/e2e/videos/ 6 | /tests/e2e/screenshots/ 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/Dockerfile: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM node:lts-alpine as build-stage 3 | WORKDIR /app 4 | COPY package*.json ./ 5 | RUN npm install 6 | COPY . . 7 | RUN npx browserslist@latest --update-db 8 | RUN npm run build 9 | 10 | # production stage 11 | FROM nginx:stable-alpine as production-stage 12 | COPY --from=build-stage /app/dist /usr/share/nginx/html 13 | COPY nginx.conf /etc/nginx 14 | EXPOSE 80 15 | CMD ["nginx", "-g", "daemon off;","-c","/etc/nginx/nginx.conf"] 16 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/README.md: -------------------------------------------------------------------------------- 1 | # redis-front 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Run your unit tests 19 | ``` 20 | npm run test:unit 21 | ``` 22 | 23 | ### Run your end-to-end tests 24 | ``` 25 | npm run test:e2e 26 | ``` 27 | 28 | ### Lints and fixes files 29 | ``` 30 | npm run lint 31 | ``` 32 | 33 | ### Customize configuration 34 | See [Configuration Reference](https://cli.vuejs.org/config/). 35 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | #user nobody; 3 | worker_processes 1; 4 | 5 | #error_log logs/error.log; 6 | #error_log logs/error.log notice; 7 | #error_log logs/error.log info; 8 | 9 | #pid logs/nginx.pid; 10 | 11 | 12 | events { 13 | worker_connections 1024; 14 | } 15 | 16 | 17 | http { 18 | include mime.types; 19 | default_type application/octet-stream; 20 | 21 | #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 22 | sendfile on; 23 | keepalive_timeout 65; 24 | 25 | server { 26 | listen 80; 27 | server_name localhost; 28 | 29 | location / { 30 | root /usr/share/nginx/html; 31 | index index.html index.htm; 32 | try_files $uri $uri/ /index.html?/$request_uri; 33 | } 34 | 35 | location /api/comments/ { 36 | proxy_pass http://app-comments:8086/api/1.0/comments/; 37 | proxy_http_version 1.1; 38 | proxy_set_header Upgrade $http_upgrade; 39 | proxy_set_header Connection 'upgrade'; 40 | proxy_set_header Host $host; 41 | proxy_cache_bypass $http_upgrade; 42 | } 43 | 44 | location /api/fulltext/ { 45 | proxy_pass http://app-streams-to-redis-hashes:8085/api/1.0/search-service/; 46 | proxy_http_version 1.1; 47 | proxy_set_header Upgrade $http_upgrade; 48 | proxy_set_header Connection 'upgrade'; 49 | proxy_set_header Host $host; 50 | proxy_cache_bypass $http_upgrade; 51 | } 52 | 53 | location /api/graph/ { 54 | proxy_pass http://app-streams-to-redisgraph:8083/api/1.0/graph-service/; 55 | proxy_http_version 1.1; 56 | proxy_set_header Upgrade $http_upgrade; 57 | proxy_set_header Connection 'upgrade'; 58 | proxy_set_header Host $host; 59 | proxy_cache_bypass $http_upgrade; 60 | } 61 | 62 | location /api/db-to-streams-service/ { 63 | proxy_pass http://app-db-to-streams:8082/api/1.0/db-to-streams-service/; 64 | proxy_http_version 1.1; 65 | proxy_set_header Upgrade $http_upgrade; 66 | proxy_set_header Connection 'upgrade'; 67 | proxy_set_header Host $host; 68 | proxy_cache_bypass $http_upgrade; 69 | } 70 | 71 | location /api/caching/ { 72 | proxy_pass http://app-caching:8084/api/1.0/caching/; 73 | proxy_http_version 1.1; 74 | proxy_set_header Upgrade $http_upgrade; 75 | proxy_set_header Connection 'upgrade'; 76 | proxy_set_header Host $host; 77 | proxy_cache_bypass $http_upgrade; 78 | } 79 | 80 | location /api/legacy/ { 81 | proxy_pass http://app-sql-rest-api:8081/api/1.0/sql-rest-api/; 82 | proxy_http_version 1.1; 83 | proxy_set_header Upgrade $http_upgrade; 84 | proxy_set_header Connection 'upgrade'; 85 | proxy_set_header Host $host; 86 | proxy_cache_bypass $http_upgrade; 87 | } 88 | 89 | # location /notifications/ { 90 | # proxy_pass http://websocket; 91 | # proxy_http_version 1.1; 92 | # proxy_set_header Upgrade $http_upgrade; 93 | # proxy_set_header Connection 'upgrade'; 94 | # proxy_set_header Host $host; 95 | # proxy_cache_bypass $http_upgrade; 96 | # } 97 | 98 | error_page 500 502 503 504 /50x.html; 99 | location = /50x.html { 100 | root html; 101 | } 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-front", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "test:unit": "vue-cli-service test:unit", 9 | "test:e2e": "vue-cli-service test:e2e", 10 | "lint": "vue-cli-service lint" 11 | }, 12 | "dependencies": { 13 | "axios": "^0.21.1", 14 | "bootstrap": "^4.4.1", 15 | "bootstrap-vue": "^2.11.0", 16 | "core-js": "^3.6.4", 17 | "d3": "^5.15.1", 18 | "lodash": "^4.17.19", 19 | "moment": "^2.24.0", 20 | "underscore": "^1.10.2", 21 | "vue": "^2.6.11", 22 | "vue-bootstrap-typeahead": "^0.2.6", 23 | "vue-class-component": "^7.2.3", 24 | "vue-property-decorator": "^8.4.1", 25 | "vue-router": "^3.1.6", 26 | "vue-slider-component": "^3.2.5" 27 | }, 28 | "devDependencies": { 29 | "@types/chai": "^4.2.11", 30 | "@types/mocha": "^5.2.4", 31 | "@typescript-eslint/eslint-plugin": "^2.26.0", 32 | "@typescript-eslint/parser": "^2.26.0", 33 | "@vue/cli-plugin-babel": "~4.5.10", 34 | "@vue/cli-plugin-e2e-cypress": "~4.5.10", 35 | "@vue/cli-plugin-eslint": "~4.5.10", 36 | "@vue/cli-plugin-router": "~4.5.10", 37 | "@vue/cli-plugin-typescript": "~4.5.10", 38 | "@vue/cli-plugin-unit-mocha": "~4.5.10", 39 | "@vue/cli-service": "~4.5.10", 40 | "@vue/eslint-config-typescript": "^5.0.2", 41 | "@vue/test-utils": "1.0.0-beta.31", 42 | "chai": "^4.1.2", 43 | "eslint": "^6.7.2", 44 | "eslint-plugin-vue": "^6.2.2", 45 | "typescript": "~3.9.3", 46 | "vue-template-compiler": "^2.6.11" 47 | }, 48 | "eslintConfig": { 49 | "root": true, 50 | "env": { 51 | "node": true 52 | }, 53 | "extends": [ 54 | "plugin:vue/essential", 55 | "eslint:recommended", 56 | "@vue/typescript/recommended" 57 | ], 58 | "parserOptions": { 59 | "ecmaVersion": 2020 60 | }, 61 | "rules": { 62 | "@typescript-eslint/camelcase": "off" 63 | }, 64 | "overrides": [ 65 | { 66 | "files": [ 67 | "**/__tests__/*.{j,t}s?(x)", 68 | "**/tests/unit/**/*.spec.{j,t}s?(x)" 69 | ], 70 | "env": { 71 | "mocha": true 72 | } 73 | } 74 | ] 75 | }, 76 | "browserslist": [ 77 | "> 1%", 78 | "last 2 versions", 79 | "not dead" 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-microservices-demo/1cb16fd781ae154f373bb43b2f01ce181e656779/ui-redis-front-end/redis-front/public/favicon.ico -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/public/imgs/autocomplete-service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-microservices-demo/1cb16fd781ae154f373bb43b2f01ce181e656779/ui-redis-front-end/redis-front/public/imgs/autocomplete-service.png -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/public/imgs/cdc-service-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-microservices-demo/1cb16fd781ae154f373bb43b2f01ce181e656779/ui-redis-front-end/redis-front/public/imgs/cdc-service-1.png -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/public/imgs/cdc-service-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-microservices-demo/1cb16fd781ae154f373bb43b2f01ce181e656779/ui-redis-front-end/redis-front/public/imgs/cdc-service-2.png -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/public/imgs/gears-pubsub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-microservices-demo/1cb16fd781ae154f373bb43b2f01ce181e656779/ui-redis-front-end/redis-front/public/imgs/gears-pubsub.png -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/public/imgs/graph-service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-microservices-demo/1cb16fd781ae154f373bb43b2f01ce181e656779/ui-redis-front-end/redis-front/public/imgs/graph-service.png -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/public/imgs/overal-archi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-microservices-demo/1cb16fd781ae154f373bb43b2f01ce181e656779/ui-redis-front-end/redis-front/public/imgs/overal-archi.png -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/public/imgs/rdbms-service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-microservices-demo/1cb16fd781ae154f373bb43b2f01ce181e656779/ui-redis-front-end/redis-front/public/imgs/rdbms-service.png -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/public/imgs/redis-logo-red-white-rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-microservices-demo/1cb16fd781ae154f373bb43b2f01ce181e656779/ui-redis-front-end/redis-front/public/imgs/redis-logo-red-white-rgb.png -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/public/imgs/redis-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/public/imgs/redis.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 48 | 50 | 80 | 82 | 88 | 89 | 90 | 92 | 96 | 98 | 104 | 110 | 116 | 122 | 128 | 134 | 135 | 136 | 141 | 146 | 152 | 157 | 162 | 163 | 169 | 175 | 176 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/public/imgs/redislabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-microservices-demo/1cb16fd781ae154f373bb43b2f01ce181e656779/ui-redis-front-end/redis-front/public/imgs/redislabs.png -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/public/imgs/ws-caching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-microservices-demo/1cb16fd781ae154f373bb43b2f01ce181e656779/ui-redis-front-end/redis-front/public/imgs/ws-caching.png -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-microservices-demo/1cb16fd781ae154f373bb43b2f01ce181e656779/ui-redis-front-end/redis-front/public/logo.png -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/App.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 116 | 117 | 160 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-microservices-demo/1cb16fd781ae154f373bb43b2f01ce181e656779/ui-redis-front-end/redis-front/src/assets/logo.png -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/components/ActorList.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 55 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/components/ActorMainComponent.vue: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 55 | 56 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/components/Comments.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/components/GenericObjectList.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 25 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/components/MovieList.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/components/ServicesConfiguration.vue: -------------------------------------------------------------------------------- 1 | 2 | 48 | 49 | 87 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import 'bootstrap/dist/css/bootstrap.css' 5 | import 'bootstrap-vue/dist/bootstrap-vue.css' 6 | import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' 7 | 8 | // Install BootstrapVue 9 | Vue.use(BootstrapVue) 10 | // Optionally install the BootstrapVue icon components plugin 11 | Vue.use(IconsPlugin) 12 | 13 | Vue.config.productionTip = false; 14 | 15 | new Vue({ 16 | router, 17 | render: h => h(App), 18 | }).$mount('#app') 19 | 20 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/repositories/CacheInvalidatorRepository.js: -------------------------------------------------------------------------------- 1 | import Repository from "./Repository"; 2 | 3 | // Check proxy configuration 4 | const baseDomain = ""; 5 | const baseURL = `${baseDomain}`; 6 | const resource = baseURL + "/api/caching/"; 7 | export default { 8 | 9 | async status() { 10 | let retValue = {}; 11 | try { 12 | //retValue = 13 | retValue = await Repository.get(`${resource}/status`); 14 | } 15 | catch(e) { 16 | console.log(`Service ${baseDomain} not available `); 17 | retValue.status = "ERROR"; 18 | } 19 | return retValue; 20 | }, 21 | 22 | start() { 23 | return Repository.get(`${resource}/start`); 24 | }, 25 | 26 | stop() { 27 | return Repository.get(`${resource}/stop`); 28 | }, 29 | 30 | getRatings(id, withCache) { 31 | let cache = 0; 32 | if (withCache) { 33 | cache=1; 34 | } 35 | return Repository.get(`${resource}/ratings/${id}?cache=${cache}`); 36 | }, 37 | 38 | saveOmdbApiKeyRatings(key) { 39 | return Repository.post(`${resource}/configuration/omdb_api?key=${key}`); 40 | }, 41 | 42 | getOmdbApiKeyRatings() { 43 | return Repository.get(`${resource}/configuration/omdb_api`); 44 | }, 45 | 46 | getOmdbApiStats() { 47 | return Repository.get(`${resource}/stats/omdb_api`); 48 | }, 49 | 50 | getServiceInfo(){ 51 | return resource; 52 | } 53 | 54 | }; 55 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/repositories/CommentsRepository.js: -------------------------------------------------------------------------------- 1 | import Repository from "./Repository"; 2 | 3 | // look in the proxy configuration for mapping 4 | const baseDomain = ""; 5 | const baseURL = `${baseDomain}`; 6 | const resource = baseURL + "/api/comments"; 7 | export default { 8 | 9 | 10 | search(item, term) { 11 | return Repository.get(`${resource}/search/${item}?q=${term}`); 12 | }, 13 | 14 | 15 | getCommentById(id){ 16 | return Repository.get(`${resource}/${id}`); 17 | }, 18 | 19 | deleteCommentById(id){ 20 | return Repository.delete(`${resource}/${id}`); 21 | }, 22 | 23 | getMovieComment(id){ 24 | return Repository.get(`${resource}/movie/${id}`); 25 | }, 26 | 27 | saveNewComment(comment){ 28 | return Repository.post(`${resource}/movie/${comment.movie_id}`, comment); 29 | }, 30 | 31 | getServiceInfo(){ 32 | return resource; 33 | } 34 | 35 | 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/repositories/DataStreamsProducerRepository.js: -------------------------------------------------------------------------------- 1 | import Repository from "./Repository"; 2 | 3 | // CHeck proxy config 4 | const baseDomain = ""; 5 | const baseURL = `${baseDomain}`; 6 | const resource = baseURL + "/api/db-to-streams-service"; 7 | export default { 8 | 9 | async status() { 10 | let retValue = {}; 11 | try { 12 | //retValue = 13 | retValue = await Repository.get(`${resource}/status`); 14 | } 15 | catch(e) { 16 | console.log(`Service ${baseDomain} not available `); 17 | retValue.status = "ERROR"; 18 | } 19 | return retValue; 20 | }, 21 | 22 | start() { 23 | return Repository.get(`${resource}/start`); 24 | }, 25 | 26 | stop() { 27 | return Repository.get(`${resource}/stop`); 28 | }, 29 | 30 | getServiceInfo(){ 31 | return resource; 32 | } 33 | 34 | 35 | 36 | }; 37 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/repositories/DataStreamsRedisHashSyncRepository.js: -------------------------------------------------------------------------------- 1 | import Repository from "./Repository"; 2 | 3 | // look in the proxy configuration for mapping 4 | const baseDomain = ""; 5 | const baseURL = `${baseDomain}`; 6 | const resource = baseURL + "/api/fulltext/"; 7 | export default { 8 | 9 | async status() { 10 | let retValue = {}; 11 | try { 12 | //retValue = 13 | retValue = await Repository.get(`${resource}/status`); 14 | } 15 | catch(e) { 16 | console.log(`Service ${baseDomain} not available `); 17 | retValue.status = "ERROR"; 18 | } 19 | return retValue; 20 | }, 21 | 22 | start() { 23 | return Repository.get(`${resource}/start`); 24 | }, 25 | 26 | stop() { 27 | return Repository.get(`${resource}/stop`); 28 | }, 29 | 30 | autocomplete(item, term) { 31 | return Repository.get(`${resource}/${item}/autocomplete?q=${term}`); 32 | }, 33 | 34 | search(item, term) { 35 | return Repository.get(`${resource}/search/${item}?q=${term}`); 36 | }, 37 | 38 | searchWithPagination(queryString, page, perPage) { 39 | const offset = perPage * page; 40 | const limit = perPage; 41 | return Repository.get(`${resource}/search-with-pagination/?q=${encodeURIComponent(queryString)}&offset=${offset}&limit=${limit}`); 42 | }, 43 | 44 | getMovieGroupBy(field) { 45 | return Repository.get(`${resource}/movies/group_by/${field}`); 46 | }, 47 | 48 | getMovieById(id){ 49 | return Repository.get(`${resource}movies/${id}`); 50 | }, 51 | 52 | getActorById(id){ 53 | return Repository.get(`${resource}actors/${id}`); 54 | }, 55 | changeConfigFlag(config) { 56 | return Repository.get(`${resource}/config/${config}`); 57 | }, 58 | 59 | getMovieStats(){ 60 | return Repository.get(`${resource}/stats/movies/all`); 61 | }, 62 | 63 | getServiceInfo(){ 64 | return resource; 65 | } 66 | 67 | 68 | 69 | }; 70 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/repositories/DataStreamsToGraphRepository.js: -------------------------------------------------------------------------------- 1 | import Repository from "./Repository"; 2 | 3 | // Chck proxy configuration 4 | const baseDomain = ""; 5 | const baseURL = `${baseDomain}`; 6 | const resource = baseURL + "/api/graph"; 7 | export default { 8 | 9 | async status() { 10 | let retValue = {}; 11 | try { 12 | //retValue = 13 | retValue = await Repository.get(`${resource}/status`); 14 | } 15 | catch(e) { 16 | console.log(`Service ${baseDomain} not available `); 17 | retValue.status = "ERROR"; 18 | } 19 | return retValue; 20 | }, 21 | 22 | start() { 23 | return Repository.get(`${resource}/start`); 24 | }, 25 | 26 | stop() { 27 | return Repository.get(`${resource}/stop`); 28 | }, 29 | 30 | refreshMovieActors(id) { 31 | return Repository.get(`${resource}/relationship/movies/${id}`); 32 | }, 33 | 34 | refreshActorsMovies(id) { 35 | return Repository.get(`${resource}/relationship/actors/${id}`); 36 | }, 37 | 38 | getServiceInfo(){ 39 | return resource; 40 | } 41 | 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/repositories/RDBMSRepository.js: -------------------------------------------------------------------------------- 1 | import Repository from "./Repository"; 2 | 3 | // Checl proxy configuration 4 | const baseDomain = ""; 5 | const baseURL = `${baseDomain}`; 6 | const resource = baseURL + "/api/legacy/"; 7 | export default { 8 | 9 | async status() { 10 | let retValue = {}; 11 | try { 12 | //retValue = 13 | retValue = await Repository.get(`${resource}/status`); 14 | } 15 | catch(e) { 16 | console.log(`Service ${baseDomain} not available `); 17 | retValue.status = "ERROR"; 18 | } 19 | return retValue; 20 | }, 21 | 22 | start() { 23 | return Repository.get(`${resource}/start`); 24 | }, 25 | 26 | stop() { 27 | return Repository.get(`${resource}/stop`); 28 | }, 29 | 30 | findActors() { 31 | return Repository.get(`${resource}/actors`); 32 | }, 33 | 34 | findActorById(id) { 35 | return Repository.get(`${resource}/actors/${id}`); 36 | }, 37 | 38 | updateActor(payload) { 39 | return Repository.post(`${resource}/actors/`, payload); 40 | }, 41 | 42 | findMovies() { 43 | return Repository.get(`${resource}/movies`); 44 | }, 45 | 46 | findMovieById(id) { 47 | return Repository.get(`${resource}/movies/${id}`); 48 | }, 49 | 50 | findMovieActors(id) { 51 | return Repository.get(`${resource}/movies/${id}/actors`); 52 | }, 53 | 54 | updateMovie(payload) { 55 | return Repository.post(`${resource}/movies/`, payload); 56 | }, 57 | 58 | getServiceInfo(){ 59 | return resource; 60 | } 61 | 62 | }; 63 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/repositories/Repository.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | // You can use your own logic to set your local or production domain 4 | //const baseDomain = "https://jsonplaceholder.typicode.com"; 5 | // The base URL is empty this time due we are using the jsonplaceholder API 6 | //const baseURL = `${baseDomain}`; 7 | 8 | export default axios.create({ 9 | //baseURL 10 | }); 11 | 12 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/repositories/RepositoryFactory.js: -------------------------------------------------------------------------------- 1 | import RDBMSRepository from "./RDBMSRepository"; 2 | import CacheInvalidatorRepository from "./CacheInvalidatorRepository"; 3 | import DataStreamsToGraphRepository from "./DataStreamsToGraphRepository"; 4 | 5 | import DataStreamsProducerRepository from "./DataStreamsProducerRepository"; 6 | import DataStreamsRedisHashSyncRepository from "./DataStreamsRedisHashSyncRepository"; 7 | 8 | import CommentsRepository from "./CommentsRepository"; 9 | 10 | 11 | const repositories = { 12 | rdbmsRepository: RDBMSRepository, 13 | cacheInvalidatorRepository: CacheInvalidatorRepository, 14 | 15 | dataStreamsToGraphRepository: DataStreamsToGraphRepository, 16 | dataStreamsProducerRepository: DataStreamsProducerRepository, 17 | dataStreamsRedisHashSyncRepository: DataStreamsRedisHashSyncRepository, 18 | 19 | commentsRepository: CommentsRepository, 20 | 21 | }; 22 | 23 | export const RepositoryFactory = { 24 | get: name => repositories[name] 25 | }; 26 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter, { RouteConfig } from 'vue-router' 3 | import Home from '../views/Home.vue' 4 | 5 | Vue.use(VueRouter) 6 | 7 | const routes: Array = [ 8 | { 9 | path: '/', 10 | name: 'Home', 11 | component: Home 12 | }, 13 | { 14 | path: '/about', 15 | name: 'About', 16 | component: () => import('../views/About.vue') 17 | }, 18 | { 19 | path: '/services', 20 | name: 'Services', 21 | component: () => import('../views/Services.vue') 22 | }, 23 | { 24 | path: '/actors', 25 | name: 'Actors', 26 | component: () => import('../views/Actors.vue') 27 | }, 28 | { 29 | path: '/actors/:id', 30 | name: 'ActorForm', 31 | component: () => import('../views/ActorForm.vue') 32 | }, 33 | { 34 | path: '/movies', 35 | name: 'Movies', 36 | component: () => import('../views/Movies.vue') 37 | }, 38 | { 39 | path: '/movies/:id', 40 | name: 'MovieForm', 41 | component: () => import('../views/MovieForm.vue') 42 | }, 43 | 44 | { 45 | path: '/autocomplete/', 46 | name: 'Autocomplete', 47 | component: () => import('../views/Autocomplete.vue') 48 | }, 49 | 50 | { 51 | path: '/search/actors', 52 | name: 'SearchActors', 53 | component: () => import('../views/SearchActors.vue') 54 | }, 55 | 56 | { 57 | path: '/search/movies', 58 | name: 'SearchMovies', 59 | component: () => import('../views/SearchMovies.vue') 60 | }, 61 | 62 | { 63 | path: '/search/movies/faceted', 64 | name: 'SearchMoviesFaceted', 65 | component: () => import('../views/SearchMoviesFaceted.vue') 66 | }, 67 | 68 | { 69 | path: '/aggregations/', 70 | name: 'Aggregations', 71 | component: () => import('../views/Aggregations.vue') 72 | }, 73 | 74 | { 75 | path: '/stats/', 76 | name: 'Stats', 77 | component: () => import('../views/Statistics.vue') 78 | }, 79 | 80 | ] 81 | 82 | const router = new VueRouter({ 83 | mode: 'history', 84 | base: process.env.BASE_URL, 85 | routes 86 | }) 87 | 88 | export default router 89 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/views/ActorForm.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 94 | 95 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/views/Actors.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/views/Aggregations.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/views/Autocomplete.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 183 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 79 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/views/Movies.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/views/Posts.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/views/SearchActors.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 94 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/views/SearchMovies.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 113 | 114 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/views/Services.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/src/views/Statistics.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 173 | 174 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/tests/e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'cypress' 4 | ], 5 | env: { 6 | mocha: true, 7 | 'cypress/globals': true 8 | }, 9 | rules: { 10 | strict: 'off' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/tests/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | // https://docs.cypress.io/guides/guides/plugins-guide.html 3 | 4 | // if you need a custom webpack configuration you can uncomment the following import 5 | // and then use the `file:preprocessor` event 6 | // as explained in the cypress docs 7 | // https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples 8 | 9 | // /* eslint-disable import/no-extraneous-dependencies, global-require */ 10 | // const webpack = require('@cypress/webpack-preprocessor') 11 | 12 | module.exports = (on, config) => { 13 | // on('file:preprocessor', webpack({ 14 | // webpackOptions: require('@vue/cli-service/webpack.config'), 15 | // watchOptions: {} 16 | // })) 17 | 18 | return Object.assign({}, config, { 19 | fixturesFolder: 'tests/e2e/fixtures', 20 | integrationFolder: 'tests/e2e/specs', 21 | screenshotsFolder: 'tests/e2e/screenshots', 22 | videosFolder: 'tests/e2e/videos', 23 | supportFile: 'tests/e2e/support/index.js' 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/tests/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/api/introduction/api.html 2 | 3 | describe('My First Test', () => { 4 | it('Visits the app root url', () => { 5 | cy.visit('/') 6 | cy.contains('h1', 'Welcome to Your Vue.js + TypeScript App') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/tests/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/tests/unit/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { shallowMount } from '@vue/test-utils' 3 | import HelloWorld from '@/components/HelloWorld.vue' 4 | 5 | describe('HelloWorld.vue', () => { 6 | it('renders props.msg when passed', () => { 7 | const msg = 'new message' 8 | const wrapper = shallowMount(HelloWorld, { 9 | propsData: { msg } 10 | }) 11 | expect(wrapper.text()).to.include(msg) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env", 16 | "mocha", 17 | "chai" 18 | ], 19 | "paths": { 20 | "@/*": [ 21 | "src/*" 22 | ] 23 | }, 24 | "lib": [ 25 | "esnext", 26 | "dom", 27 | "dom.iterable", 28 | "scripthost" 29 | ] 30 | }, 31 | "include": [ 32 | "src/**/*.ts", 33 | "src/**/*.tsx", 34 | "src/**/*.vue", 35 | "tests/**/*.ts", 36 | "tests/**/*.tsx" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /ui-redis-front-end/redis-front/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devServer: { 3 | proxy: { 4 | '^/api/comments': { 5 | target: 'http://localhost:8086/api/1.0/comments', 6 | ws: true, 7 | changeOrigin: true, 8 | pathRewrite: {"^/api/comments" : "/"} 9 | }, 10 | '^/api/fulltext': { 11 | target: 'http://localhost:8085/api/1.0/search-service', 12 | ws: true, 13 | changeOrigin: true, 14 | pathRewrite: {"^/api/fulltext" : "/"} 15 | }, 16 | '^/api/legacy': { 17 | target: 'http://localhost:8081/api/1.0/sql-rest-api', 18 | ws: true, 19 | changeOrigin: true, 20 | pathRewrite: {"^/api/legacy" : "/"} 21 | }, 22 | '^/api/caching': { 23 | target: 'http://localhost:8084/api/1.0/caching', 24 | ws: true, 25 | changeOrigin: true, 26 | pathRewrite: {"^/api/caching" : "/"} 27 | }, 28 | '^/api/db-to-streams-service': { 29 | target: 'http://localhost:8082/api/1.0/db-to-streams-service', 30 | ws: true, 31 | changeOrigin: true, 32 | pathRewrite: {"^/api/db-to-streams-service" : "/"} 33 | }, 34 | '^/api/graph': { 35 | target: 'http://localhost:8083//api/1.0/graph-service', 36 | ws: true, 37 | changeOrigin: true, 38 | pathRewrite: {"^/api/graph" : "/"} 39 | } 40 | } 41 | } 42 | } --------------------------------------------------------------------------------