diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..9934f7a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,48 @@ +name: Build and Publish Docker Image + +on: + push: + branches: + - feature/NU-1740 + +jobs: + build: + name: Build and Push Docker Image + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Read version from file + id: get_version + run: | + VERSION=$(grep LIBRARY_DOCKER_IMAGE_VERSION version | cut -d'=' -f2) + echo "VERSION=${VERSION}" >> $GITHUB_ENV + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + password: ${{ secrets.DOCKERHUB_USER }} + username: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker images + uses: docker/buildx-action@v2 + with: + args: | + --platform linux/amd64,linux/arm64 + --push + -t myusername/myimage:latest + -t myusername/myimage:${{ env.VERSION }} + . \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7121880 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.swp +.idea +*.iml +target +.metals +.vscode +.DS_Store +common/scripts/jq \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..af5e0f1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +FROM holomekc/wiremock-gui:3.8.1 AS wiremock + +RUN apt-get update && \ + apt-get install -y wget && \ + wget -P /var/wiremock/extensions https://repo1.maven.org/maven2/org/wiremock/extensions/wiremock-faker-extension-standalone/0.2.0/wiremock-faker-extension-standalone-0.2.0.jar + +FROM phusion/baseimage:noble-1.0.0 + +# Use baseimage-docker's init system. +CMD ["/sbin/my_init"] + +WORKDIR /app + +COPY scenario-examples-bootstrapper/setup/ /app/setup/ +COPY scenario-examples-bootstrapper/mocks/ /app/mocks/ +COPY scenario-examples-bootstrapper/data/ /app/data/ +COPY scenario-examples-bootstrapper/utils/ /app/utils/ +COPY scenario-examples-bootstrapper/run-mocks-setup-data.sh /app/run-mocks-setup-data.sh + +COPY scenario-examples-bootstrapper/services/postgres.sh /etc/service/db/run +COPY scenario-examples-bootstrapper/services/wiremock.sh /etc/service/http-service/run +COPY scenario-examples-bootstrapper/services/setup.sh /etc/service/setup/run + +# WIREMOCK & POSTGRES +COPY --from=wiremock /var/wiremock /var/wiremock +COPY --from=wiremock /home/wiremock /home/wiremock + +USER root + +RUN apt update && \ + apt install -y --no-install-recommends curl ca-certificates jq less && \ + install -d /usr/share/postgresql-common/pgdg && \ + curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc && \ + echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ + apt update -y && \ + apt -y install postgresql-16 && \ + apt -y install openjdk-11-jre-headless && \ + apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ + curl https://raw.githubusercontent.com/birdayz/kaf/master/godownloader.sh | BINDIR=/bin bash + +EXPOSE 8080 +EXPOSE 5432 + +COPY scenario-examples-library/ /scenario-examples + +HEALTHCHECK --interval=10s --timeout=1s --retries=12 --start-period=30s \ + CMD (/app/setup/is-setup-done.sh && /app/mocks/db/is-postgres-ready.sh && /app/mocks/http-service/is-wiremock-ready.sh) || exit 1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba9e762 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# MOCKS + +## Resources: +https://github.com/wiremock/wiremock-faker-extension/blob/main/docs/reference.md +https://docs.wiremock.io/response-templating/basics/ +https://docs.wiremock.io/response-templating/dates-and-times/ + +# todo: +- delete +- write about disabling examples (eg. LOAN_REQUEST_DISABLED: true) +- write about required envs: NU_REQUEST_RESPONSE_OPEN_API_SERVICE_ADDRESS, SCHEMA_REGISTRY_ADDRESS, KAFKA_ADDRESS, NU_DESIGNER_ADDRESS \ No newline at end of file diff --git a/build-and-push-docker-image.sh b/build-and-push-docker-image.sh new file mode 100755 index 0000000..47b9e75 --- /dev/null +++ b/build-and-push-docker-image.sh @@ -0,0 +1,9 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ./version + +./build-docker-image.sh + +docker push "touk/nussknacker-example-scenarios-library${LIBRARY_DOCKER_IMAGE_VERSION}" \ No newline at end of file diff --git a/build-docker-image.sh b/build-docker-image.sh new file mode 100755 index 0000000..bb2bc97 --- /dev/null +++ b/build-docker-image.sh @@ -0,0 +1,7 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ./version + +docker build -t "touk/nussknacker-example-scenarios-library${LIBRARY_DOCKER_IMAGE_VERSION}" -f Dockerfile . \ No newline at end of file diff --git a/scenario-examples-bootstrapper/data/http/continuously-send-http-generated-requests.sh b/scenario-examples-bootstrapper/data/http/continuously-send-http-generated-requests.sh new file mode 100755 index 0000000..6f40cba --- /dev/null +++ b/scenario-examples-bootstrapper/data/http/continuously-send-http-generated-requests.sh @@ -0,0 +1,53 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../../utils/lib.sh + +if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) scenario example folder path${RESET}\n" + exit 1 +fi + +function runRequestSending() { + if [ "$#" -ne 2 ]; then + echo -e "${RED}ERROR: Two parameters required: 1) OpenAPI service slug, 2) request generator script${RESET}\n" + exit 11 + fi + + set -e + + local OPENAPI_SERVICE_SLUG=$1 + local REQUEST_GENERATOR_SCRIPT=$2 + + echo -n "Starting to send to '$OPENAPI_SERVICE_SLUG' OpenAPI service, requests generated by '$REQUEST_GENERATOR_SCRIPT' generator script... " + + mkdir -p /var/log/continuously-send-http-requests + nohup ../../utils/http/continuously-send-http-requests.sh "$OPENAPI_SERVICE_SLUG" "$REQUEST_GENERATOR_SCRIPT" > /var/log/continuously-send-http-requests/output.log 2>&1 & + + echo "OK" +} + +SCENARIO_EXAMPLE_DIR_PATH=${1%/} + +echo "Starting to send generated requests to Nu OpenAPI services..." + +shopt -s nullglob + +for ITEM in "$SCENARIO_EXAMPLE_DIR_PATH/data/http/generated"/*; do + if [ ! -f "$ITEM" ]; then + continue + fi + + if [[ ! "$ITEM" == *.sh ]]; then + echo -e "${RED}ERROR: Unrecognized file $ITEM. Required file with extension '.sh' and content with bash script${RESET}\n" + exit 3 + fi + + OPENAPI_SERVICE_SLUG=$(basename "$ITEM" ".sh") + + runRequestSending "$OPENAPI_SERVICE_SLUG" "$ITEM" + +done + +echo -e "Generators are running!\n" diff --git a/scenario-examples-bootstrapper/data/http/send-http-static-requests.sh b/scenario-examples-bootstrapper/data/http/send-http-static-requests.sh new file mode 100755 index 0000000..7c27ac8 --- /dev/null +++ b/scenario-examples-bootstrapper/data/http/send-http-static-requests.sh @@ -0,0 +1,56 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../../utils/lib.sh + +if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) scenario example folder path${RESET}\n" + exit 1 +fi + +function sendRequest() { + if [ "$#" -ne 2 ]; then + echo -e "${RED}ERROR: Two parameters required: 1) Request-Response OpenAPI service slug, 2) request body${RESET}\n" + exit 11 + fi + + set -e + + local OPENAPI_SERVICE_SLUG=$1 + local REQUEST_BODY=$2 + + echo -n "Sending request '$REQUEST_BODY' to Request-Response '$OPENAPI_SERVICE_SLUG' OpenAPI service... " + ../../utils/http/send-request-to-nu-openapi-service.sh "$OPENAPI_SERVICE_SLUG" "$REQUEST_BODY" + echo "OK" +} + +SCENARIO_EXAMPLE_DIR_PATH=${1%/} + +echo "Starting to send preconfigured Request-Response OpenAPI service requests..." + +shopt -s nullglob + +for ITEM in "$SCENARIO_EXAMPLE_DIR_PATH/data/http/static"/*; do + if [ ! -f "$ITEM" ]; then + continue + fi + + if [[ ! "$ITEM" == *.txt ]]; then + echo -e "${RED}ERROR: Unrecognized file $ITEM. Required file with extension '.txt' and content with JSON messages${RESET}\n" + exit 3 + fi + + OPENAPI_SERVICE_SLUG=$(basename "$ITEM" ".txt") + + while IFS= read -r REQUEST_BODY; do + if [[ $REQUEST_BODY == "#"* ]]; then + continue + fi + + sendRequest "$OPENAPI_SERVICE_SLUG" "$REQUEST_BODY" + + done < "$ITEM" +done + +echo -e "Requests sent!\n" diff --git a/scenario-examples-bootstrapper/data/kafka/continuously-send-kafka-generated-messages.sh b/scenario-examples-bootstrapper/data/kafka/continuously-send-kafka-generated-messages.sh new file mode 100755 index 0000000..176c384 --- /dev/null +++ b/scenario-examples-bootstrapper/data/kafka/continuously-send-kafka-generated-messages.sh @@ -0,0 +1,53 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../../utils/lib.sh + +if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) scenario example folder path${RESET}\n" + exit 1 +fi + +function runMessageSending() { + if [ "$#" -ne 2 ]; then + echo -e "${RED}ERROR: Two parameters required: 1) topic name, 2) message generator script${RESET}\n" + exit 11 + fi + + set -e + + local TOPIC_NAME=$1 + local MSG_GENERATOR_SCRIPT=$2 + + echo -n "Starting to send to '$TOPIC_NAME' messages generated by '$MSG_GENERATOR_SCRIPT' generator script... " + + mkdir -p /var/log/continuously-send-to-topic + nohup ../../utils/kafka/continuously-send-to-topic.sh "$TOPIC_NAME" "$MSG_GENERATOR_SCRIPT" > /var/log/continuously-send-to-topic/output.log 2>&1 & + + echo "OK" +} + +SCENARIO_EXAMPLE_DIR_PATH=${1%/} + +echo "Starting to send generated messages..." + +shopt -s nullglob + +for ITEM in "$SCENARIO_EXAMPLE_DIR_PATH/data/kafka/generated"/*; do + if [ ! -f "$ITEM" ]; then + continue + fi + + if [[ ! "$ITEM" == *.sh ]]; then + echo -e "${RED}ERROR: Unrecognized file $ITEM. Required file with extension '.sh' and content with bash script${RESET}\n" + exit 3 + fi + + TOPIC_NAME=$(basename "$ITEM" ".sh" | sed 's/.*/\u&/') + + runMessageSending "$TOPIC_NAME" "$ITEM" + +done + +echo -e "Generators are running!\n" diff --git a/scenario-examples-bootstrapper/data/kafka/send-kafka-static-messages.sh b/scenario-examples-bootstrapper/data/kafka/send-kafka-static-messages.sh new file mode 100755 index 0000000..0e5e245 --- /dev/null +++ b/scenario-examples-bootstrapper/data/kafka/send-kafka-static-messages.sh @@ -0,0 +1,56 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../../utils/lib.sh + +if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) scenario example folder path${RESET}\n" + exit 1 +fi + +function sendMessage() { + if [ "$#" -ne 2 ]; then + echo -e "${RED}ERROR: Two parameters required: 1) topic name, 2) message${RESET}\n" + exit 11 + fi + + set -e + + local TOPIC_NAME=$1 + local MSG=$2 + + echo -n "Sending message $MSG to '$TOPIC_NAME'" + ../../utils/kafka/send-to-topic.sh "$TOPIC_NAME" "$MSG" + echo "OK" +} + +SCENARIO_EXAMPLE_DIR_PATH=${1%/} + +echo "Starting to send preconfigured messages..." + +shopt -s nullglob + +for ITEM in "$SCENARIO_EXAMPLE_DIR_PATH/data/kafka/static"/*; do + if [ ! -f "$ITEM" ]; then + continue + fi + + if [[ ! "$ITEM" == *.txt ]]; then + echo -e "${RED}ERROR: Unrecognized file $ITEM. Required file with extension '.txt' and content with JSON messages${RESET}\n" + exit 3 + fi + + TOPIC_NAME=$(basename "$ITEM" ".sh" | sed 's/.*/\u&/') + + while IFS= read -r MSG; do + if [[ $MSG == "#"* ]]; then + continue + fi + + sendMessage "$TOPIC_NAME" "$MSG" + + done < "$ITEM" +done + +echo -e "Messages sent!\n" diff --git a/scenario-examples-bootstrapper/data/keep-sending.sh b/scenario-examples-bootstrapper/data/keep-sending.sh new file mode 100755 index 0000000..22c88ff --- /dev/null +++ b/scenario-examples-bootstrapper/data/keep-sending.sh @@ -0,0 +1,26 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../utils/lib.sh + +echo -e "${MAGENTA}-------- DATA GENERATION ACTIVATION STAGE is starting... ------${RESET}\n" + +shopt -s nullglob + +for FOLDER in /scenario-examples/*; do + if isScenarioEnabled "$FOLDER"; then + echo -e "Starting to send static and generated data for scenario from ${GREEN}$FOLDER${RESET} directory...\n\n" + + ./http/send-http-static-requests.sh "$FOLDER" + ./kafka/send-kafka-static-messages.sh "$FOLDER" + ./http/continuously-send-http-generated-requests.sh "$FOLDER" + ./kafka/continuously-send-kafka-generated-messages.sh "$FOLDER" + + echo -e "Static data sent and generators from ${GREEN}$FOLDER${RESET} directory are runnning!\n\n" + else + echo -e "Skipping sending static and generated data for scenario from ${GREEN}$FOLDER${RESET} directory.\n" + fi +done + +echo -e "${MAGENTA}-------- DATA GENERATION ACTIVATION STAGE is finished! --------${RESET}\n\n" \ No newline at end of file diff --git a/scenario-examples-bootstrapper/mocks/configure.sh b/scenario-examples-bootstrapper/mocks/configure.sh new file mode 100755 index 0000000..316a2f8 --- /dev/null +++ b/scenario-examples-bootstrapper/mocks/configure.sh @@ -0,0 +1,24 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../utils/lib.sh + +shopt -s nullglob + +echo -e "${MAGENTA}-------- MOCK CONFIGURATION STAGE is starting... ----${RESET}\n" + +for FOLDER in /scenario-examples/*; do + if isScenarioEnabled "$SCENARIO_DIR"; then + echo -e "Starting to configure mocks for scenarios from ${GREEN}$FOLDER${RESET} directory...\n\n" + + ./db/execute-ddls.sh "$FOLDER" + ./http-service/configure-mock-http-services.sh "$FOLDER" + + echo -e "Mocks for scenarios from ${GREEN}$FOLDER${RESET} directory configured!\n\n" + else + echo "Skipping configuring mocks for scenario from ${GREEN}$FOLDER${RESET} directory." + fi +done + +echo -e "${MAGENTA}-------- MOCK CONFIGURATION STAGE is finished! ------${RESET}\n\n" \ No newline at end of file diff --git a/scenario-examples-bootstrapper/mocks/db/execute-ddls.sh b/scenario-examples-bootstrapper/mocks/db/execute-ddls.sh new file mode 100755 index 0000000..eba094c --- /dev/null +++ b/scenario-examples-bootstrapper/mocks/db/execute-ddls.sh @@ -0,0 +1,49 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source postgres-operations.sh +source ../../utils/lib.sh + +if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) scenario example folder path${RESET}\n" + exit 1 +fi + +function execute_ddl_script() { + if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) DDL file path${RESET}\n" + exit 11 + fi + + set -e + + local DDL_FILE_NAME=$1 + + local SCHEMA_NAME + local DDL_CONTENT + + SCHEMA_NAME=$(basename "$(strip_extension "$DDL_FILE_NAME")") + echo -n "Creating schema: $SCHEMA_NAME... " + create_schema "$PG_USER" "$SCHEMA_NAME" > /dev/null + echo "OK" + + DDL_CONTENT=$(wrap_sql_with_current_schema "$SCHEMA_NAME" "$(cat "$DDL_FILE_NAME")") + echo -n "Executing DDL '$DDL_FILE_NAME'... " + echo "$DDL_CONTENT" | execute_sql "" "$PG_USER" "$PG_PASS" > /dev/null + echo "OK" +} + +SCENARIO_EXAMPLE_DIR_PATH=${1%/} + +echo "Starting to import Postgres DDL scripts..." + +shopt -s nullglob + +for ITEM in "$SCENARIO_EXAMPLE_DIR_PATH/mocks/db"/*; do + if [ -f "$ITEM" ]; then + execute_ddl_script "$ITEM" + fi +done + +echo -e "Postgres DDL scripts imported!\n" diff --git a/scenario-examples-bootstrapper/mocks/db/is-postgres-ready.sh b/scenario-examples-bootstrapper/mocks/db/is-postgres-ready.sh new file mode 100755 index 0000000..903d460 --- /dev/null +++ b/scenario-examples-bootstrapper/mocks/db/is-postgres-ready.sh @@ -0,0 +1,3 @@ +#!/bin/bash -e + +pg_isready -d mocks -U mocks > /dev/null diff --git a/scenario-examples-bootstrapper/mocks/db/postgres-init.sh b/scenario-examples-bootstrapper/mocks/db/postgres-init.sh new file mode 100755 index 0000000..d9a625c --- /dev/null +++ b/scenario-examples-bootstrapper/mocks/db/postgres-init.sh @@ -0,0 +1,27 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source postgres-operations.sh +source ../../utils/lib.sh + +init_db() { + init_bg_log_file + init_data_dir + init_custom_conf_dir + configure_pg_config + configure_authentication +} + +configure_users() { + create_user + create_custom_database + grant_privileges + alter_pg_user_pass +} + +init_db +postgres_start_bg +wait_until_started +configure_users +postgres_stop \ No newline at end of file diff --git a/scenario-examples-bootstrapper/mocks/db/postgres-operations.sh b/scenario-examples-bootstrapper/mocks/db/postgres-operations.sh new file mode 100755 index 0000000..f78c5b5 --- /dev/null +++ b/scenario-examples-bootstrapper/mocks/db/postgres-operations.sh @@ -0,0 +1,141 @@ +#!/bin/bash -e + +PG_DB_NAME="mocks" +PG_USER="mocks" +PG_PASS="mocks_pass" + +PG_BIN_DIR="/usr/lib/postgresql/16/bin" +PG_BASE_DIR="/home/postgres" +PG_DATA_DIR="$PG_BASE_DIR/data" +PG_CUSTOM_CONF_DIR="$PG_BASE_DIR/conf" +PG_CONF_FILE="$PG_CUSTOM_CONF_DIR/postgresql.conf" +PG_HBA_FILE="$PG_CUSTOM_CONF_DIR/pg_hba.conf" + +source /app/utils/lib.sh + +init_data_dir() { + if [ ! -e "$PG_DATA_DIR" ]; then + mkdir -p "$PG_DATA_DIR" + chown postgres "$PG_DATA_DIR" + /sbin/setuser postgres "$PG_BIN_DIR"/initdb -D "$PG_DATA_DIR" + fi +} + +init_custom_conf_dir() { + if [ ! -e "$PG_CUSTOM_CONF_DIR" ]; then + mkdir -p "$PG_CUSTOM_CONF_DIR" + chown postgres "$PG_CUSTOM_CONF_DIR" + fi +} + +configure_authentication() { + if [ ! -f "$PG_HBA_FILE" ]; then + cp "$PG_DATA_DIR/pg_hba.conf" "$PG_HBA_FILE" + chown postgres "$PG_HBA_FILE" + echo "#" >> "$PG_HBA_FILE" + echo "host all all all md5" >> "$PG_HBA_FILE" + fi +} + +configure_pg_config() { + if [ ! -f "$PG_CONF_FILE" ]; then + cp "$PG_DATA_DIR/postgresql.conf" "$PG_CONF_FILE" + chown postgres "$PG_CONF_FILE" + echo "#" >> "$PG_CONF_FILE" + echo "listen_addresses = '*'" >> "$PG_CONF_FILE" + fi +} + +init_bg_log_file() { + local log_file + log_file="/var/log/postgres_bg.log" + if [ ! -f "$log_file" ]; then + touch "$log_file" + chown postgres "$log_file" + fi +} + +wait_until_started() { + local max_startup_timeout_in_s=${1:-10} + while ! pg_isready >/dev/null 2>&1; do + sleep 1 + max_startup_timeout_in_s=$((max_startup_timeout_in_s - 1)) + if ((max_startup_timeout_in_s <= 0)); then + echo -e "${RED}ERROR: Postgres is not started${RESET}\n" + exit 1 + fi + done + echo "Postgres started" +} + +create_custom_database() { + local db_name="${1:-$PG_DB_NAME}" + DB_EXISTS=$(echo "SELECT 1 FROM pg_database WHERE datname='$db_name'" | execute_sql "" "postgres" "" "-tA") + if [ "$DB_EXISTS" != "1" ]; then + echo "CREATE DATABASE \"$db_name\"" | execute_sql "" "postgres" "" + else + echo "DB already exists - creation skipped" + fi +} + +create_user() { + ROLE_EXISTS=$(echo "SELECT 1 FROM pg_roles WHERE rolname='$PG_USER'" | execute_sql "" "postgres" "" "-tA") + if [ "$ROLE_EXISTS" != "1" ]; then + echo "CREATE ROLE \"${PG_USER}\" WITH LOGIN PASSWORD '${PG_PASS}';" | execute_sql "" "postgres" "" + else + echo "ROLE already exists - creation skipped" + fi +} + +grant_privileges() { + local user="${1:-$PG_USER}" + local db_name="${2:-$PG_DB_NAME}" + execute_sql "" "postgres" "" < /dev/null \ No newline at end of file diff --git a/scenario-examples-bootstrapper/mocks/http-service/run-wiremock.sh b/scenario-examples-bootstrapper/mocks/http-service/run-wiremock.sh new file mode 100755 index 0000000..e74e961 --- /dev/null +++ b/scenario-examples-bootstrapper/mocks/http-service/run-wiremock.sh @@ -0,0 +1,13 @@ +#!/bin/sh -e + +echo "RUNNING Wiremock service..." + +java $JAVA_OPTS -cp /var/wiremock/lib/*:/var/wiremock/extensions/* wiremock.Run \ + --port=8080 \ + --root-dir=/home/wiremock/mocks \ + --max-request-journal=1000 \ + --global-response-templating \ + --extensions=org.wiremock.RandomExtension \ + --async-response-enable=true \ + --async-response-threads=30 \ + --disable-banner \ No newline at end of file diff --git a/scenario-examples-bootstrapper/run-mocks-setup-data.sh b/scenario-examples-bootstrapper/run-mocks-setup-data.sh new file mode 100755 index 0000000..797763a --- /dev/null +++ b/scenario-examples-bootstrapper/run-mocks-setup-data.sh @@ -0,0 +1,26 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source /app/utils/lib.sh + +rm -rf /app/healthy + +if /app/mocks/db/is-postgres-ready.sh && /app/mocks/http-service/is-wiremock-ready.sh; then + echo -e "\n${GREEN}------ Nu scenarios library is being prepared... ---------${RESET}\n\n" + + /app/mocks/configure.sh + /app/setup/run-setup.sh + /app/data/keep-sending.sh + + echo -e "\n${GREEN}------ Nu scenarios library sucessfully bootstrapped! ----${RESET}\n\n" + + touch /app/healthy + + # loop forever (you can use manually called utils scripts now) + tail -f /dev/null +else + echo -e "\nWaiting for Postgres and Wiremock to be up and ready...\n" + sleep 5 + exit 1 +fi diff --git a/scenario-examples-bootstrapper/services/postgres.sh b/scenario-examples-bootstrapper/services/postgres.sh new file mode 100755 index 0000000..74bce8c --- /dev/null +++ b/scenario-examples-bootstrapper/services/postgres.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec /app/mocks/db/run-postgres.sh \ No newline at end of file diff --git a/scenario-examples-bootstrapper/services/setup.sh b/scenario-examples-bootstrapper/services/setup.sh new file mode 100755 index 0000000..571e0b3 --- /dev/null +++ b/scenario-examples-bootstrapper/services/setup.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec /app/run-mocks-setup-data.sh \ No newline at end of file diff --git a/scenario-examples-bootstrapper/services/wiremock.sh b/scenario-examples-bootstrapper/services/wiremock.sh new file mode 100755 index 0000000..d7089ba --- /dev/null +++ b/scenario-examples-bootstrapper/services/wiremock.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec /app/mocks/http-service/run-wiremock.sh \ No newline at end of file diff --git a/scenario-examples-bootstrapper/setup/is-setup-done.sh b/scenario-examples-bootstrapper/setup/is-setup-done.sh new file mode 100755 index 0000000..b6cb932 --- /dev/null +++ b/scenario-examples-bootstrapper/setup/is-setup-done.sh @@ -0,0 +1,3 @@ +#!/bin/bash -e + +test -f "/app/healthy" diff --git a/scenario-examples-bootstrapper/setup/kafka/setup-topics.sh b/scenario-examples-bootstrapper/setup/kafka/setup-topics.sh new file mode 100755 index 0000000..b2ca640 --- /dev/null +++ b/scenario-examples-bootstrapper/setup/kafka/setup-topics.sh @@ -0,0 +1,53 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../../utils/lib.sh + +if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) scenario example folder path${RESET}\n" + exit 1 +fi + +function createTopic() { + if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) topic name${RESET}\n" + exit 11 + fi + + set -e + local TOPIC_NAME=$1 + + echo -n "Creating topic '$TOPIC_NAME'... " + ../../utils/kafka/create-topic-idempotently.sh "$TOPIC_NAME" + echo "OK" +} + +SCENARIO_EXAMPLE_DIR_PATH=${1%/} + +echo "Starting to create preconfigured topics..." + +shopt -s nullglob + +for ITEM in "$SCENARIO_EXAMPLE_DIR_PATH/setup/kafka"/*; do + if [ ! -f "$ITEM" ]; then + continue + fi + + if [[ ! "$ITEM" == *.txt ]]; then + echo -e "${RED}ERROR: Unrecognized file $ITEM. Required file with extension '.txt' and content with topic names${RESET}\n" + exit 2 + fi + + while IFS= read -r TOPIC_NAME; do + + if [[ $TOPIC_NAME == "#"* ]]; then + continue + fi + + createTopic "$TOPIC_NAME" + + done < "$ITEM" +done + +echo -e "Topics created!\n" diff --git a/scenario-examples-bootstrapper/setup/nu/customize-nu-configuration.sh b/scenario-examples-bootstrapper/setup/nu/customize-nu-configuration.sh new file mode 100755 index 0000000..eda15bf --- /dev/null +++ b/scenario-examples-bootstrapper/setup/nu/customize-nu-configuration.sh @@ -0,0 +1,61 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../../utils/lib.sh + +if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) scenario example folder path${RESET}\n" + exit 1 +fi + +SCENARIO_EXAMPLE_DIR_PATH=${1%/} +CONFS_DIR=/opt/nussknacker/conf +APP_CUSTOMIZATION_FILE_PATH="$CONFS_DIR/application-customizations.conf" + +function customizeNuConfiguration() { + if [ "$#" -ne 2 ]; then + echo -e "${RED}ERROR: Two parameters required: 1) configuration file path 2) example scenario id${RESET}\n" + exit 11 + fi + + set -e + + local EXAMPLE_SCENARIO_RELATED_CONFIGURAION_FILE_PATH=$1 + local EXAMPLE_SCENARIO_ID=$2 + local EXAMPLE_SCENARIO_RELATED_CONFIGURAION_FILE_NAME="${EXAMPLE_SCENARIO_ID}-$(basename "$EXAMPLE_SCENARIO_RELATED_CONFIGURAION_FILE_PATH")" + + echo -n "Including $EXAMPLE_SCENARIO_RELATED_CONFIGURAION_FILE_PATH configuration... " + + cp -f "$EXAMPLE_SCENARIO_RELATED_CONFIGURAION_FILE_PATH" "$CONFS_DIR/$EXAMPLE_SCENARIO_RELATED_CONFIGURAION_FILE_NAME" + local INCLUDE_CONF_LINE="include \"$EXAMPLE_SCENARIO_RELATED_CONFIGURAION_FILE_NAME\"" + + if ! grep -qxF "$INCLUDE_CONF_LINE" "$APP_CUSTOMIZATION_FILE_PATH"; then + echo "$INCLUDE_CONF_LINE" >> "$APP_CUSTOMIZATION_FILE_PATH" + fi + echo "OK" +} + +echo "Starting to customize Nu configuration..." + +touch "$APP_CUSTOMIZATION_FILE_PATH" + +shopt -s nullglob + +for ITEM in "$SCENARIO_EXAMPLE_DIR_PATH/setup/nu-designer"/*; do + if [ ! -f "$ITEM" ]; then + continue + fi + + if [[ ! "$ITEM" == *.conf ]]; then + echo -e "${RED}ERROR: Unrecognized file $ITEM. Required file with extension '.conf' and content with HOCON Nu configuration${RESET}\n" + exit 2 + fi + + SCENARIO_EXAMPLE_ID=$(basename "$SCENARIO_EXAMPLE_DIR_PATH") + customizeNuConfiguration "$ITEM" "$SCENARIO_EXAMPLE_ID" +done + +../../utils/nu/reload-configuration.sh + +echo -e "Configuration customized!\n" \ No newline at end of file diff --git a/scenario-examples-bootstrapper/setup/nu/import-and-deploy-example-scenarios.sh b/scenario-examples-bootstrapper/setup/nu/import-and-deploy-example-scenarios.sh new file mode 100755 index 0000000..a189d63 --- /dev/null +++ b/scenario-examples-bootstrapper/setup/nu/import-and-deploy-example-scenarios.sh @@ -0,0 +1,48 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../../utils/lib.sh + +if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) scenario example folder path${RESET}\n" + exit 1 +fi + +SCENARIO_EXAMPLE_DIR_PATH=${1%/} + +function importAndDeployScenario() { + if [ "$#" -ne 2 ]; then + echo -e "${RED}Error: Two parameters required: 1) scenario name, 2) example scenario file path${RESET}\n" + exit 11 + fi + + set -e + + local EXAMPLE_SCENARIO_NAME=$1 + local EXAMPLE_SCENARIO_FILE=$2 + + ../../utils/nu/load-scenario-from-json-file.sh "$EXAMPLE_SCENARIO_NAME" "$EXAMPLE_SCENARIO_FILE" + ../../utils/nu/deploy-scenario-and-wait-for-running-state.sh "$EXAMPLE_SCENARIO_NAME" +} + +echo "Starting to import and deploy example scenarios..." + +shopt -s nullglob + +for ITEM in "$SCENARIO_EXAMPLE_DIR_PATH"/*; do + if [ ! -f "$ITEM" ]; then + continue + fi + + if [[ ! "$ITEM" == *.json ]]; then + echo -e "${RED}ERROR: Unrecognized file $ITEM. Required file with extension '.json' and content with Nu scenario JSON${RESET}\n" + exit 2 + fi + + EXAMPLE_SCENARIO_NAME="$(basename "$ITEM" ".json")" + + importAndDeployScenario "$EXAMPLE_SCENARIO_NAME" "$ITEM" +done + +echo -e "Scenarios imported and deployed!\n" diff --git a/scenario-examples-bootstrapper/setup/run-setup.sh b/scenario-examples-bootstrapper/setup/run-setup.sh new file mode 100755 index 0000000..c8b1a78 --- /dev/null +++ b/scenario-examples-bootstrapper/setup/run-setup.sh @@ -0,0 +1,26 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../utils/lib.sh + +shopt -s nullglob + +echo -e "${MAGENTA}-------- SETUP STAGE is starting... -------${RESET}\n" + +for FOLDER in /scenario-examples/*; do + if isScenarioEnabled "$FOLDER"; then + echo -e "Starting to configure and run example scenarios from ${GREEN}$FOLDER${RESET} directory...\n\n" + + ./schema-registry/setup-schemas.sh "$FOLDER" + ./kafka/setup-topics.sh "$FOLDER" + ./nu/customize-nu-configuration.sh "$FOLDER" + ./nu/import-and-deploy-example-scenarios.sh "$FOLDER" + + echo -e "Scenarios from ${GREEN}$FOLDER${RESET} directory configured and running!\n\n" + else + echo "Skipping configuring and running example scenario from ${GREEN}$FOLDER${RESET} directory." + fi +done + +echo -e "${MAGENTA}-------- SETUP STAGE is finished! ---------${RESET}\n\n" diff --git a/scenario-examples-bootstrapper/setup/schema-registry/setup-schemas.sh b/scenario-examples-bootstrapper/setup/schema-registry/setup-schemas.sh new file mode 100755 index 0000000..8235171 --- /dev/null +++ b/scenario-examples-bootstrapper/setup/schema-registry/setup-schemas.sh @@ -0,0 +1,48 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../../utils/lib.sh + +if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) scenario example folder path${RESET}\n" + exit 1 +fi + +function createJsonSchema() { + if [ "$#" -ne 2 ]; then + echo -e "${RED}ERROR: Two parameters required: 1) schema name, 2) schema file path${RESET}\n" + exit 11 + fi + + set -e + + local SCHEMA_NAME=$1 + local SCHEMA_FILE_PATH=$2 + + echo -n "Creating schema '$SCHEMA_NAME'... " + ../../utils/schema-registry/add-json-schema-idempotently.sh "$SCHEMA_NAME" "$SCHEMA_FILE_PATH" + echo "OK" +} + +SCENARIO_EXAMPLE_DIR_PATH=${1%/} + +echo "Starting to add preconfigured schemas..." + +shopt -s nullglob + +for ITEM in "$SCENARIO_EXAMPLE_DIR_PATH/setup/schema-registry"/*; do + if [ ! -f "$ITEM" ]; then + continue + fi + + if [[ ! "$ITEM" == *.schema.json ]]; then + echo -e "${RED}ERROR: Unrecognized file '$ITEM'. Required file with extension '.schema.json' and content with JSON schema${RESET}\n" + exit 2 + fi + + SCHEMA_NAME="$(basename "$ITEM" ".schema.json")-value" + createJsonSchema "$SCHEMA_NAME" "$ITEM" +done + +echo -e "Schemas added!\n" diff --git a/scenario-examples-bootstrapper/utils/http/continuously-send-http-requests.sh b/scenario-examples-bootstrapper/utils/http/continuously-send-http-requests.sh new file mode 100755 index 0000000..ce21a49 --- /dev/null +++ b/scenario-examples-bootstrapper/utils/http/continuously-send-http-requests.sh @@ -0,0 +1,20 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../lib.sh + +if [ "$#" -ne 2 ]; then + echo -e "${RED}ERROR: Two parameters required: 1) OpenAPI service slug, 2) request generator script path${RESET}\n" + exit 1 +fi + +OPENAPI_SERVICE_SLUG=$1 +REQUEST_GENERATOR_SCRIPT=$2 + +verifyBashScript "$REQUEST_GENERATOR_SCRIPT" + +while true; do + sleep 0.1 + ./send-request-to-nu-openapi-service.sh "$OPENAPI_SERVICE_SLUG" "$($REQUEST_GENERATOR_SCRIPT)" > /dev/null || true +done diff --git a/scenario-examples-bootstrapper/utils/http/send-request-to-nu-openapi-service.sh b/scenario-examples-bootstrapper/utils/http/send-request-to-nu-openapi-service.sh new file mode 100755 index 0000000..08c441d --- /dev/null +++ b/scenario-examples-bootstrapper/utils/http/send-request-to-nu-openapi-service.sh @@ -0,0 +1,33 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../lib.sh + +if [ "$#" -ne 2 ]; then + echo -e "${RED}ERROR: Two parameters required: 1) Nu OpenAPI service slug, 2) request payload${RESET}\n" + exit 1 +fi + +if ! [ -v NU_REQUEST_RESPONSE_OPEN_API_SERVICE_ADDRESS ] || [ -z "$NU_REQUEST_RESPONSE_OPEN_API_SERVICE_ADDRESS" ]; then + echo -e "${RED}ERROR: required variable NU_REQUEST_RESPONSE_OPEN_API_SERVICE_ADDRESS not set or empty${RESET}\n" + exit 2 +fi + +OPENAPI_SERVICE_SLUG=$1 +REQUEST_BODY=$2 + +RESPONSE=$(curl -s -L -w "\n%{http_code}" \ + -X POST "http://$NU_REQUEST_RESPONSE_OPEN_API_SERVICE_ADDRESS/scenario/$OPENAPI_SERVICE_SLUG" \ + -H "Content-Type: application/json" -d "$REQUEST_BODY" +) + +HTTP_STATUS=$(echo "$RESPONSE" | tail -n 1) +RESPONSE_BODY=$(echo "$RESPONSE" | sed \$d) + +if [[ "$HTTP_STATUS" != 200 ]] ; then + echo -e "${RED}ERROR: '$OPENAPI_SERVICE_SLUG' OpenAPI service unexpected response.\nHTTP status: $HTTP_STATUS, response body: $RESPONSE_BODY${RESET}\n" + exit 3 +fi + +echo "$RESPONSE_BODY" \ No newline at end of file diff --git a/scenario-examples-bootstrapper/utils/kafka/continuously-send-to-topic.sh b/scenario-examples-bootstrapper/utils/kafka/continuously-send-to-topic.sh new file mode 100755 index 0000000..867183e --- /dev/null +++ b/scenario-examples-bootstrapper/utils/kafka/continuously-send-to-topic.sh @@ -0,0 +1,20 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../lib.sh + +if [ "$#" -ne 2 ]; then + echo -e "${RED}ERROR: Two parameters required: 1) topic name, 2) generator script path${RESET}\n" + exit 1 +fi + +TOPIC=$1 +GENERATOR_SCRIPT=$2 + +verifyBashScript "$GENERATOR_SCRIPT" + +while true; do + sleep 0.1 + ./send-to-topic.sh "$TOPIC" "$($GENERATOR_SCRIPT)" || true +done diff --git a/scenario-examples-bootstrapper/utils/kafka/create-topic-idempotently.sh b/scenario-examples-bootstrapper/utils/kafka/create-topic-idempotently.sh new file mode 100755 index 0000000..7fc2ca9 --- /dev/null +++ b/scenario-examples-bootstrapper/utils/kafka/create-topic-idempotently.sh @@ -0,0 +1,21 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../lib.sh + +if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) topic name${RESET}\n" + exit 1 +fi + +if ! [ -v KAFKA_ADDRESS ] || [ -z "$KAFKA_ADDRESS" ]; then + echo -e "${RED}ERROR: required variable KAFKA_ADDRESS not set or empty${RESET}\n" + exit 2 +fi + +TOPIC_NAME=$1 + +if ! kaf --brokers="$KAFKA_ADDRESS" topics ls | awk '{print $1}' | grep "^$TOPIC_NAME$" > /dev/null 2>&1; then + kaf --brokers="$KAFKA_ADDRESS" topic create "$TOPIC_NAME" > /dev/null +fi \ No newline at end of file diff --git a/scenario-examples-bootstrapper/utils/kafka/purge-topic.sh b/scenario-examples-bootstrapper/utils/kafka/purge-topic.sh new file mode 100755 index 0000000..d657a11 --- /dev/null +++ b/scenario-examples-bootstrapper/utils/kafka/purge-topic.sh @@ -0,0 +1,20 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../lib.sh + +if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) topic name${RESET}\n" + exit 1 +fi + +if ! [ -v KAFKA_ADDRESS ] || [ -z "$KAFKA_ADDRESS" ]; then + echo -e "${RED}ERROR: required variable KAFKA_ADDRESS not set or empty${RESET}\n" + exit 2 +fi + +TOPIC_NAME=$1 + +kaf --brokers="$KAFKA_ADDRESS" topic delete "$TOPIC_NAME" > /dev/null +./create-topic-idempotently.sh diff --git a/scenario-examples-bootstrapper/utils/kafka/send-to-topic.sh b/scenario-examples-bootstrapper/utils/kafka/send-to-topic.sh new file mode 100755 index 0000000..2172409 --- /dev/null +++ b/scenario-examples-bootstrapper/utils/kafka/send-to-topic.sh @@ -0,0 +1,25 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../lib.sh + +if [ "$#" -ne 2 ]; then + echo -e "${RED}ERROR: Two parameters required: 1) topic name, 2) message${RESET}\n" + exit 1 +fi + +if ! [ -v KAFKA_ADDRESS ] || [ -z "$KAFKA_ADDRESS" ]; then + echo -e "${RED}ERROR: required variable KAFKA_ADDRESS not set or empty${RESET}\n" + exit 2 +fi + +TOPIC_NAME=$1 +MESSAGE=$2 + +if kaf --brokers="$KAFKA_ADDRESS" topics ls | awk '{print $1}' | grep "^$TOPIC_NAME$" > /dev/null 2>&1; then + echo "$MESSAGE" | kaf --brokers="$KAFKA_ADDRESS" produce "$TOPIC_NAME" > /dev/null +else + echo -e "${RED}ERROR: Topic name '$TOPIC_NAME' not found${RESET}\n" + exit 3 +fi \ No newline at end of file diff --git a/scenario-examples-bootstrapper/utils/lib.sh b/scenario-examples-bootstrapper/utils/lib.sh new file mode 100755 index 0000000..bef60e9 --- /dev/null +++ b/scenario-examples-bootstrapper/utils/lib.sh @@ -0,0 +1,87 @@ +#!/bin/bash -e + +RED='\033[31m' +GREEN='\033[32m' +MAGENTA='\033[35m' +RESET='\033[0m' + +function verifyBashScript() { + local FILE=$1 + + if [[ -f "$FILE" ]]; then + if [[ $(head -n 1 "$FILE") =~ ^#!/bin/bash ]]; then + return 0 + else + echo "File '$FILE' exists but is not a Bash script." + return 1 + fi + else + echo "File '$FILE' does not exist." + return 2 + fi +} + +function random_Ndigit_number() { + if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) number of digits${RESET}\n" + return 1 + fi + + local LENGTH=$1 + local RESULT="" + + local FIRST_DIGIT=$((RANDOM % 9 + 1)) + RESULT+="$FIRST_DIGIT" + + while [ ${#RESULT} -lt $LENGTH ]; do + local REMAINING=$((LENGTH - ${#RESULT})) + local PART=$(printf "%05d" $((RANDOM % 100000))) + RESULT+=${PART:0:$REMAINING} + done + echo "$RESULT" +} + +function random_4digit_number() { + random_Ndigit_number 4 +} + +function random_3digit_number() { + random_Ndigit_number 3 +} + +function random_1digit_number() { + random_Ndigit_number 1 +} + +function now() { + echo "$(date +%s)$(random_3digit_number)" +} + +function pick_randomly() { + local OPTIONS=("$@") + local COUNT=${#OPTIONS[@]} + local RANDOM_INDEX=$((RANDOM % COUNT)) + echo "${OPTIONS[$RANDOM_INDEX]}" +} + +function strip_extension() { + local file="$1" + echo "${file%.*}" +} + +function isScenarioEnabled() { + if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) scenario folder path${RESET}\n" + return 1 + fi + + SCENARIO_DIR=$1 + SCENARIO_NAME=$(basename "$SCENARIO_DIR") + + IS_DISABLED=$(echo "${SCENARIO_NAME}_DISABLED" | tr '-' '_' | awk '{print toupper($0)}') + if [[ "${!IS_DISABLED,,}" == "true" ]]; then + return 2 + fi + + return 0 +} \ No newline at end of file diff --git a/scenario-examples-bootstrapper/utils/nu/deploy-scenario-and-wait-for-running-state.sh b/scenario-examples-bootstrapper/utils/nu/deploy-scenario-and-wait-for-running-state.sh new file mode 100755 index 0000000..7d4740a --- /dev/null +++ b/scenario-examples-bootstrapper/utils/nu/deploy-scenario-and-wait-for-running-state.sh @@ -0,0 +1,103 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../lib.sh + +if [ "$#" -lt 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) scenario name${RESET}\n" + exit 1 +fi + +if ! [ -v NU_DESIGNER_ADDRESS ] || [ -z "$NU_DESIGNER_ADDRESS" ]; then + echo -e "${RED}ERROR: required variable NU_DESIGNER_ADDRESS not set or empty${RESET}\n" + exit 2 +fi + +SCENARIO_NAME=$1 +TIMEOUT_SECONDS=${2:-60} +WAIT_INTERVAL=5 + +function deployScenario() { + if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) scenario name${RESET}\n" + exit 11 + fi + + set -e + + local SCENARIO_NAME=$1 + + local RESPONSE + RESPONSE=$(curl -s -L -w "\n%{http_code}" -u admin:admin \ + -X POST "http://${NU_DESIGNER_ADDRESS}/api/processManagement/deploy/$SCENARIO_NAME" + ) + + local HTTP_STATUS + HTTP_STATUS=$(echo "$RESPONSE" | tail -n 1) + + if [ "$HTTP_STATUS" != "200" ]; then + local RESPONSE_BODY + RESPONSE_BODY=$(echo "$RESPONSE" | sed \$d) + echo -e "${RED}ERROR: Cannot run scenario $SCENARIO_NAME deployment.\nHTTP status: $HTTP_STATUS, response body: $RESPONSE_BODY${RESET}\n" + exit 12 + fi + + echo "Scenario $SCENARIO_NAME deployment started..." +} + +function checkDeploymentStatus() { + if [ "$#" -ne 1 ]; then + echo -e "${RED}ERROR: One parameter required: 1) scenario name${RESET}\n" + exit 21 + fi + + set -e + + local SCENARIO_NAME=$1 + + local RESPONSE + RESPONSE=$(curl -s -L -w "\n%{http_code}" -u admin:admin \ + -X GET "http://${NU_DESIGNER_ADDRESS}/api/processes/$SCENARIO_NAME/status" + ) + + local HTTP_STATUS + HTTP_STATUS=$(echo "$RESPONSE" | tail -n 1) + local RESPONSE_BODY + RESPONSE_BODY=$(echo "$RESPONSE" | sed \$d) + + if [ "$HTTP_STATUS" != "200" ]; then + echo -e "${RED}ERROR: Cannot check scenario $SCENARIO_NAME deployment status.\nHTTP status: $HTTP_STATUS, response body: $RESPONSE_BODY${RESET}\n" + exit 22 + fi + + local SCENARIO_STATUS + SCENARIO_STATUS=$(echo "$RESPONSE_BODY" | jq -r '.status.name') + echo "$SCENARIO_STATUS" +} + +echo "Deploying scenario $SCENARIO_NAME..." + +START_TIME=$(date +%s) +END_TIME=$((START_TIME + TIMEOUT_SECONDS)) + +deployScenario "$SCENARIO_NAME" + +while true; do + DEPLOYMENT_STATUS=$(checkDeploymentStatus "$SCENARIO_NAME") + + if [ "$DEPLOYMENT_STATUS" == "RUNNING" ]; then + break + fi + + CURRENT_TIME=$(date +%s) + if [ $CURRENT_TIME -gt $END_TIME ]; then + echo -e "${RED}ERROR: Timeout for waiting for the RUNNING state of $SCENARIO_NAME deployment reached!${RESET}\n" + exit 3 + fi + + echo "$SCENARIO_NAME deployment state is $DEPLOYMENT_STATUS. Checking again in $WAIT_INTERVAL seconds..." + sleep $WAIT_INTERVAL +done + +echo "Scenario $SCENARIO_NAME is RUNNING!" diff --git a/scenario-examples-bootstrapper/utils/nu/load-scenario-from-json-file.sh b/scenario-examples-bootstrapper/utils/nu/load-scenario-from-json-file.sh new file mode 100755 index 0000000..c2b7a36 --- /dev/null +++ b/scenario-examples-bootstrapper/utils/nu/load-scenario-from-json-file.sh @@ -0,0 +1,168 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../lib.sh + +if [ "$#" -lt 2 ]; then + echo -e "${RED}ERROR: Two parameters required: 1) scenario name, 2) scenario file path${RESET}\n" + exit 1 +fi + +if ! [ -v NU_DESIGNER_ADDRESS ] || [ -z "$NU_DESIGNER_ADDRESS" ]; then + echo -e "${RED}ERROR: required variable NU_DESIGNER_ADDRESS not set or empty${RESET}\n" + exit 2 +fi + +SCENARIO_NAME=$1 +SCENARIO_FILE_PATH=$2 +CATEGORY=${3:-"Default"} + +if [ ! -f "$SCENARIO_FILE_PATH" ]; then + echo -e "${RED}ERROR: Cannot find file $SCENARIO_FILE_PATH with scenario${RESET}\n" + exit 3 +fi + +function createEmptyScenario() { + if [ "$#" -ne 4 ]; then + echo -e "${RED}ERROR: Four parameters required: 1) scenario name, 2) processing mode, 3) category, 4) engine${RESET}\n" + exit 11 + fi + + set -e + + local SCENARIO_NAME=$1 + local PROCESSING_MODE=$2 + local CATEGORY=$3 + local ENGINE=$4 + + local REQUEST_BODY="{ + \"name\": \"$SCENARIO_NAME\", + \"processingMode\": \"$PROCESSING_MODE\", + \"category\": \"$CATEGORY\", + \"engineSetupName\": \"$ENGINE\", + \"isFragment\": false + }" + + local RESPONSE + RESPONSE=$(curl -s -L -w "\n%{http_code}" -u admin:admin \ + -X POST "http://${NU_DESIGNER_ADDRESS}/api/processes" \ + -H "Content-Type: application/json" -d "$REQUEST_BODY" + ) + + local HTTP_STATUS + HTTP_STATUS=$(echo "$RESPONSE" | tail -n 1) + + if [ "$HTTP_STATUS" == "400" ]; then + local RESPONSE_BODY + RESPONSE_BODY=$(echo "$RESPONSE" | sed \$d) + + if [[ "$RESPONSE_BODY" == *"already exists"* ]]; then + echo "Scenario already exists." + exit 0 + else + echo -e "${RED}ERROR: Cannot create empty scenario $SCENARIO_NAME.\nHTTP status: $HTTP_STATUS, response body: $RESPONSE_BODY${RESET}\n" + exit 12 + fi + elif [ "$HTTP_STATUS" != "201" ]; then + local RESPONSE_BODY + RESPONSE_BODY=$(echo "$RESPONSE" | sed \$d) + echo -e "${RED}ERROR: Cannot create empty scenario $SCENARIO_NAME.\nHTTP status: $HTTP_STATUS, response body: $RESPONSE_BODY${RESET}\n" + exit 13 + fi + + echo "Empty scenario $SCENARIO_NAME created successfully." +} + +function importScenarioFromFile() { + if [ "$#" -ne 2 ]; then + echo -e "${RED}ERROR: Two parameters required: 1) scenario name, 2) scenario file path${RESET}\n" + exit 21 + fi + + set -e + + local SCENARIO_NAME=$1 + local SCENARIO_FILE=$2 + + local RESPONSE + RESPONSE=$(curl -s -L -w "\n%{http_code}" -u admin:admin \ + -X POST "http://${NU_DESIGNER_ADDRESS}/api/processes/import/$SCENARIO_NAME" \ + -F "process=@$SCENARIO_FILE" + ) + + # Check response body and status code + local HTTP_STATUS + HTTP_STATUS=$(echo "$RESPONSE" | tail -n 1) + + local RESPONSE_BODY + RESPONSE_BODY=$(echo "$RESPONSE" | sed \$d) + + if [ "$HTTP_STATUS" == "200" ]; then + local SCENARIO_GRAPH + SCENARIO_GRAPH=$(echo "$RESPONSE_BODY" | jq '.scenarioGraph') + echo "$SCENARIO_GRAPH" + else + echo -e "${RED}ERROR: Cannot import scenario $SCENARIO_NAME.\nHTTP status: $HTTP_STATUS, response body: $RESPONSE_BODY${RESET}\n" + exit 22 + fi +} + +function saveScenario() { + if [ "$#" -ne 2 ]; then + echo -e "${RED}ERROR: Two parameters required: 1) scenario name, 2) scenario graph JSON representation${RESET}\n" + exit 31 + fi + + set -e + + local SCENARIO_NAME=$1 + local SCENARIO_GRAPH_JSON=$2 + + local REQUEST_BODY="{ + \"scenarioGraph\": $SCENARIO_GRAPH_JSON, + \"comment\": \"\" + }" + + local RESPONSE + RESPONSE=$(curl -s -L -w "\n%{http_code}" -u admin:admin \ + -X PUT "http://${NU_DESIGNER_ADDRESS}/api/processes/$SCENARIO_NAME" \ + -H "Content-Type: application/json" -d "$REQUEST_BODY" + ) + + local HTTP_STATUS + HTTP_STATUS=$(echo "$RESPONSE" | tail -n 1) + + if [ "$HTTP_STATUS" != "200" ]; then + local RESPONSE_BODY + RESPONSE_BODY=$(echo "$RESPONSE" | sed \$d) + echo -e "${RED}ERROR: Cannot save scenario $SCENARIO_NAME.\nHTTP status: $HTTP_STATUS, response body: $RESPONSE_BODY${RESET}\n" + exit 32 + fi + + echo "Scenario $SCENARIO_NAME saved successfully." +} + +META_DATA_TYPE=$(jq -r .metaData.additionalFields.metaDataType < "$SCENARIO_FILE_PATH") +case "$META_DATA_TYPE" in + "StreamMetaData") + ENGINE="Flink" + PROCESSING_MODE="Unbounded-Stream" + ;; + "LiteStreamMetaData") + ENGINE="Lite Embedded" + PROCESSING_MODE="Unbounded-Stream" + ;; + "RequestResponseMetaData") + ENGINE="Lite Embedded" + PROCESSING_MODE="Request-Response" + ;; + *) + echo -e "${RED}ERROR: Cannot import scenario with metadata type: $META_DATA_TYPE${RESET}\n" + exit 4 + ;; +esac + +createEmptyScenario "$SCENARIO_NAME" "$PROCESSING_MODE" "$CATEGORY" "$ENGINE" +SCENARIO_GRAPH=$(importScenarioFromFile "$SCENARIO_NAME" "$SCENARIO_FILE_PATH") +saveScenario "$SCENARIO_NAME" "$SCENARIO_GRAPH" diff --git a/scenario-examples-bootstrapper/utils/nu/load-scenario-from-json.sh b/scenario-examples-bootstrapper/utils/nu/load-scenario-from-json.sh new file mode 100755 index 0000000..a29b2dc --- /dev/null +++ b/scenario-examples-bootstrapper/utils/nu/load-scenario-from-json.sh @@ -0,0 +1,19 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../lib.sh + +if [ "$#" -lt 2 ]; then + echo -e "${RED}ERROR: Two parameters required: 1) scenario name, 2) scenario JSON${RESET}\n" + exit 1 +fi + +SCENARIO_NAME=$1 +SCENARIO_JSON=$2 +SCENARIO_JSON_FILE="/tmp/scenario-$SCENARIO_NAME.json" + +echo "$SCENARIO_JSON" > "$SCENARIO_JSON_FILE" +trap 'rm "$SCENARIO_JSON_FILE"' EXIT + +./load-scenario-from-json-file.sh "$SCENARIO_NAME" "$SCENARIO_JSON_FILE" diff --git a/scenario-examples-bootstrapper/utils/nu/reload-configuration.sh b/scenario-examples-bootstrapper/utils/nu/reload-configuration.sh new file mode 100755 index 0000000..908a6e1 --- /dev/null +++ b/scenario-examples-bootstrapper/utils/nu/reload-configuration.sh @@ -0,0 +1,33 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../lib.sh + +if ! [ -v NU_DESIGNER_ADDRESS ] || [ -z "$NU_DESIGNER_ADDRESS" ]; then + echo -e "${RED}ERROR: required variable NU_DESIGNER_ADDRESS not set or empty${RESET}\n" + exit 1 +fi + +function reloadConfiguration() { + set -e + + local RESPONSE + RESPONSE=$(curl -s -L -w "\n%{http_code}" -u admin:admin \ + -X POST "http://${NU_DESIGNER_ADDRESS}/api/app/processingtype/reload" + ) + + local HTTP_STATUS + HTTP_STATUS=$(echo "$RESPONSE" | tail -n 1) + local RESPONSE_BODY + RESPONSE_BODY=$(echo "$RESPONSE" | sed \$d) + + if [ "$HTTP_STATUS" != "204" ]; then + echo -e "${RED}ERROR: Cannot reload Nu configuration.\nHTTP status: $HTTP_STATUS, response body: $RESPONSE_BODY${RESET}\n" + exit 22 + fi +} + +echo -n "Reloading Nu configuration... " +reloadConfiguration +echo "OK" diff --git a/scenario-examples-bootstrapper/utils/schema-registry/add-json-schema-idempotently.sh b/scenario-examples-bootstrapper/utils/schema-registry/add-json-schema-idempotently.sh new file mode 100755 index 0000000..b3bbf22 --- /dev/null +++ b/scenario-examples-bootstrapper/utils/schema-registry/add-json-schema-idempotently.sh @@ -0,0 +1,40 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../lib.sh + +if [ "$#" -ne 2 ]; then + echo -e "${RED}ERROR: Two parameters required: 1) schema name, 2) schema file path${RESET}\n" + exit 1 +fi + +if ! [ -v SCHEMA_REGISTRY_ADDRESS ] || [ -z "$SCHEMA_REGISTRY_ADDRESS" ]; then + echo -e "${RED}ERROR: required variable SCHEMA_REGISTRY_ADDRESS not set or empty${RESET}\n" + exit 2 +fi + +SCHEMA_NAME=$1 +SCHEMA_FILE=$2 + +ESCAPED_JSON_SCHEMA=$(awk 'BEGIN{ORS="\\n"} {gsub(/"/, "\\\"")} 1' < "$SCHEMA_FILE") + +REQUEST_BODY="{ + \"schema\": \"$ESCAPED_JSON_SCHEMA\", + \"schemaType\": \"JSON\", + \"references\": [] +}" + +RESPONSE=$(curl -s -L -w "\n%{http_code}" -u admin:admin \ + -X POST "http://${SCHEMA_REGISTRY_ADDRESS}/subjects/${SCHEMA_NAME}/versions" \ + -H "Content-Type: application/vnd.schemaregistry.v1+json" -d "$REQUEST_BODY" +) + +HTTP_STATUS=$(echo "$RESPONSE" | tail -n 1) + +if [[ "$HTTP_STATUS" != 200 ]] ; then + RESPONSE_BODY + RESPONSE_BODY=$(echo "$RESPONSE" | sed \$d) + echo -e "${RED}ERROR: Cannot create schema $SCHEMA_NAME.\nHTTP status: $HTTP_STATUS, response body: $RESPONSE_BODY${RESET}\n" + exit 3 +fi \ No newline at end of file diff --git a/scenario-examples-library/detect-large-transactions/DetectLargeTransactions.json b/scenario-examples-library/detect-large-transactions/DetectLargeTransactions.json new file mode 100644 index 0000000..5523b1a --- /dev/null +++ b/scenario-examples-library/detect-large-transactions/DetectLargeTransactions.json @@ -0,0 +1,125 @@ +{ + "metaData": { + "id": "DetectLargeTransactions", + "additionalFields": { + "description": null, + "properties": { + "parallelism": "1", + "spillStateToDisk": "true" + }, + "metaDataType": "StreamMetaData" + } + }, + "nodes": [ + { + "id": "transactions", + "ref": { + "typ": "kafka", + "parameters": [ + { + "name": "Topic", + "expression": { + "language": "spel", + "expression": "'Transactions'" + } + }, + { + "name": "Schema version", + "expression": { + "language": "spel", + "expression": "'latest'" + } + } + ] + }, + "additionalFields": { + "description": null, + "layoutData": { + "x": 0, + "y": 0 + } + }, + "type": "Source" + }, + { + "nextFalse": [ + ], + "id": "only large ones", + "expression": { + "language": "spel", + "expression": "#input.amount > 20" + }, + "isDisabled": null, + "additionalFields": { + "description": null, + "layoutData": { + "x": 0, + "y": 180 + } + }, + "type": "Filter" + }, + { + "id": "send for audit", + "ref": { + "typ": "kafka", + "parameters": [ + { + "name": "Topic", + "expression": { + "language": "spel", + "expression": "'ProcessedTransactions'" + } + }, + { + "name": "Schema version", + "expression": { + "language": "spel", + "expression": "'latest'" + } + }, + { + "name": "Key", + "expression": { + "language": "spel", + "expression": "" + } + }, + { + "name": "Raw editor", + "expression": { + "language": "spel", + "expression": "true" + } + }, + { + "name": "Value validation mode", + "expression": { + "language": "spel", + "expression": "'strict'" + } + }, + { + "name": "Value", + "expression": { + "language": "spel", + "expression": "#input" + } + } + ] + }, + "endResult": null, + "isDisabled": null, + "additionalFields": { + "description": null, + "layoutData": { + "x": 0, + "y": 360 + } + }, + "type": "Sink" + } + ], + "additionalBranches": [ + ] +} diff --git a/scenario-examples-library/detect-large-transactions/data/kafka/generated/transactions.sh b/scenario-examples-library/detect-large-transactions/data/kafka/generated/transactions.sh new file mode 100755 index 0000000..f8004ec --- /dev/null +++ b/scenario-examples-library/detect-large-transactions/data/kafka/generated/transactions.sh @@ -0,0 +1,11 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source /app/utils/lib.sh + +ID=$((1 + $(random_4digit_number) % 5)) +AMOUNT=$((1 + $(random_4digit_number) % 30)) +TIME=$(($(now) - $(random_4digit_number) % 20)) + +echo "{ \"clientId\": \"Client$ID\", \"amount\": $AMOUNT, \"eventDate\": $TIME}" \ No newline at end of file diff --git a/scenario-examples-library/detect-large-transactions/data/kafka/static/transactions.txt b/scenario-examples-library/detect-large-transactions/data/kafka/static/transactions.txt new file mode 100644 index 0000000..65a4152 --- /dev/null +++ b/scenario-examples-library/detect-large-transactions/data/kafka/static/transactions.txt @@ -0,0 +1,3 @@ +# Example messages below (message per line) +#{ "clientId": "Client1", "amount": 100, "eventDate": 1720166429}" +#{ "clientId": "Client2", "amount": 1000, "eventDate": 1720166429}" diff --git a/scenario-examples-library/detect-large-transactions/setup/kafka/topics.txt b/scenario-examples-library/detect-large-transactions/setup/kafka/topics.txt new file mode 100644 index 0000000..8efc1cf --- /dev/null +++ b/scenario-examples-library/detect-large-transactions/setup/kafka/topics.txt @@ -0,0 +1,2 @@ +ProcessedTransactions +Transactions diff --git a/scenario-examples-library/detect-large-transactions/setup/schema-registry/ProcessedTransactions.schema.json b/scenario-examples-library/detect-large-transactions/setup/schema-registry/ProcessedTransactions.schema.json new file mode 100644 index 0000000..ca3c0d0 --- /dev/null +++ b/scenario-examples-library/detect-large-transactions/setup/schema-registry/ProcessedTransactions.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "properties": { + "clientId": { "type": "string" }, + "amount": { "type": "integer" }, + "isLast": { "type": "boolean", "default": false }, + "eventDate": { "type": "integer" } + }, + "required": ["clientId", "amount"], + "additionalProperties": false +} diff --git a/scenario-examples-library/detect-large-transactions/setup/schema-registry/Transactions.schema.json b/scenario-examples-library/detect-large-transactions/setup/schema-registry/Transactions.schema.json new file mode 100644 index 0000000..ca3c0d0 --- /dev/null +++ b/scenario-examples-library/detect-large-transactions/setup/schema-registry/Transactions.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "properties": { + "clientId": { "type": "string" }, + "amount": { "type": "integer" }, + "isLast": { "type": "boolean", "default": false }, + "eventDate": { "type": "integer" } + }, + "required": ["clientId", "amount"], + "additionalProperties": false +} diff --git a/scenario-examples-library/loan-request/LoanRequest.json b/scenario-examples-library/loan-request/LoanRequest.json new file mode 100644 index 0000000..a18e407 --- /dev/null +++ b/scenario-examples-library/loan-request/LoanRequest.json @@ -0,0 +1,239 @@ +{ + "metaData": { + "id": "LoanRequest", + "additionalFields": { + "description": null, + "properties": { + "inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"customerId\": {\n \"type\": \"string\"\n },\n \"location\": {\n \"type\": \"object\",\n \"properties\": {\n \"city\": {\n \"type\": \"string\"\n },\n \"street\": {\n \"type\": \"string\"\n }\n }\n },\n \"requestType\": {\n \"type\": \"string\"\n },\n \"requestedAmount\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\"customerId\", \"location\", \"requestType\", \"requestedAmount\"],\n \"additionalProperties\": false\n}", + "outputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"acceptedAmount\": {\n \"type\": \"number\",\n \"description\": \"Accepted amount\"\n },\n \"message\": {\n \"type\": \"string\",\n \"description\": \"Additional message\"\n }\n },\n \"required\": [\"acceptedAmount\", \"message\"],\n \"additionalProperties\": false\n}", + "slug": "loan" + }, + "metaDataType": "RequestResponseMetaData" + } + }, + "nodes": [ + { + "id": "request", + "ref": { + "typ": "request", + "parameters": [ + ] + }, + "additionalFields": { + "description": null, + "layoutData": { + "x": 360, + "y": 0 + } + }, + "type": "Source" + }, + { + "defaultNext": [ + ], + "nexts": [ + { + "expression": { + "language": "spel", + "expression": "#input.requestType == 'loan'" + }, + "nodes": [ + { + "id": "loan response", + "ref": { + "typ": "response", + "parameters": [ + { + "name": "acceptedAmount", + "expression": { + "language": "spel", + "expression": "50" + } + }, + { + "name": "message", + "expression": { + "language": "spel", + "expression": "'only small amount available'" + } + } + ] + }, + "endResult": null, + "isDisabled": null, + "additionalFields": { + "description": null, + "layoutData": { + "x": 0, + "y": 360 + } + }, + "type": "Sink" + } + ] + }, + { + "expression": { + "language": "spel", + "expression": "#input.requestType == 'mortgage'" + }, + "nodes": [ + { + "defaultNext": [ + ], + "nexts": [ + { + "expression": { + "language": "spel", + "expression": "#input.location.city == 'Warszawa'" + }, + "nodes": [ + { + "id": "Warsaw mortgage", + "ref": { + "typ": "response", + "parameters": [ + { + "name": "acceptedAmount", + "expression": { + "language": "spel", + "expression": "1000" + } + }, + { + "name": "message", + "expression": { + "language": "spel", + "expression": "'Large sum for Warszawa'" + } + } + ] + }, + "endResult": null, + "isDisabled": null, + "additionalFields": { + "description": null, + "layoutData": { + "x": 180, + "y": 540 + } + }, + "type": "Sink" + } + ] + }, + { + "expression": { + "language": "spel", + "expression": "true" + }, + "nodes": [ + { + "id": "Other city mortgage", + "ref": { + "typ": "response", + "parameters": [ + { + "name": "Raw editor", + "expression": { + "language": "spel", + "expression": "false" + } + }, + { + "name": "acceptedAmount", + "expression": { + "language": "spel", + "expression": "100" + } + }, + { + "name": "message", + "expression": { + "language": "spel", + "expression": "'Large sum for other city'" + } + } + ] + }, + "endResult": null, + "isDisabled": null, + "additionalFields": { + "description": null, + "layoutData": { + "x": 540, + "y": 540 + } + }, + "type": "Sink" + } + ] + } + ], + "id": "switch", + "additionalFields": { + "description": null, + "layoutData": { + "x": 360, + "y": 360 + } + }, + "type": "Switch" + } + ] + }, + { + "expression": { + "language": "spel", + "expression": "true" + }, + "nodes": [ + { + "id": "unknown", + "ref": { + "typ": "response", + "parameters": [ + { + "name": "acceptedAmount", + "expression": { + "language": "spel", + "expression": "0" + } + }, + { + "name": "message", + "expression": { + "language": "spel", + "expression": "'Unknown loan type'" + } + } + ] + }, + "endResult": null, + "isDisabled": null, + "additionalFields": { + "description": null, + "layoutData": { + "x": 720, + "y": 360 + } + }, + "type": "Sink" + } + ] + } + ], + "id": "loan type", + "additionalFields": { + "description": null, + "layoutData": { + "x": 360, + "y": 180 + } + }, + "type": "Switch" + } + ], + "additionalBranches": [ + ] + } \ No newline at end of file diff --git a/scenario-examples-library/loan-request/data/http/generated/loan.sh b/scenario-examples-library/loan-request/data/http/generated/loan.sh new file mode 100755 index 0000000..b46b7f7 --- /dev/null +++ b/scenario-examples-library/loan-request/data/http/generated/loan.sh @@ -0,0 +1,12 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source /app/utils/lib.sh + +ID="$(random_4digit_number)" +AMOUNT="$(random_4digit_number)" +REQUEST_TYPE="$(pick_randomly "loan" "mortgage" "insurance")" +CITY="$(pick_randomly "Warszawa" "Berlin" "Gdańsk" "Kraków", "Poznań", "Praga")" + +echo "{\"customerId\": \"$ID\", \"requestedAmount\": $AMOUNT, \"requestType\": \"$REQUEST_TYPE\", \"location\": { \"city\": \"$CITY\", \"street\": \"\" }}" \ No newline at end of file diff --git a/scenario-examples-library/loan-request/data/http/static/loan.txt b/scenario-examples-library/loan-request/data/http/static/loan.txt new file mode 100644 index 0000000..9b66751 --- /dev/null +++ b/scenario-examples-library/loan-request/data/http/static/loan.txt @@ -0,0 +1,7 @@ +# Example Request-Response OpenAPI service requests (request payload per line) +#{"customerId": "anon", "requestedAmount": 1555, "requestType": "mortgage", "location": { "city": "Warszawa", "street": "Marszałkowska" }} +#{"customerId": "anon", "requestedAmount": 86, "requestType": "loan", "location": { "city": "Lublin", "street": "Głęboka" }} +#{"customerId": "1", "requestedAmount": 1000, "requestType": "loan", "location": { "city": "Warszawa", "street": "Marszałkowska" }} +#{"customerId": "1", "requestedAmount": 500, "requestType": "savings", "location": { "city": "London", "street": "Kensington" }} +#{"customerId": "4", "requestedAmount": 2000, "requestType": "mortgage", "location": { "city": "Lublin", "street": "Lipowa" }} +#{"customerId": "3", "requestedAmount": 2000, "requestType": "loan", "location": { "city": "Lublin", "street": "Głęboka" }} diff --git a/scenario-examples-library/offer-customer-proposal-based-on-activity-event/OfferCustomerProposalBasedOnActivityEvent.json b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/OfferCustomerProposalBasedOnActivityEvent.json new file mode 100644 index 0000000..9bf82cd --- /dev/null +++ b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/OfferCustomerProposalBasedOnActivityEvent.json @@ -0,0 +1,446 @@ +{ + "metaData" : { + "id" : "OfferCustomerProposalBasedOnActivityEvent", + "additionalFields" : { + "description" : null, + "properties" : { + "parallelism" : "1", + "spillStateToDisk" : "true", + "useAsyncInterpretation" : "", + "checkpointIntervalInSeconds" : "" + }, + "metaDataType" : "StreamMetaData" + } + }, + "nodes" : [ + { + "id" : "Customers-related events", + "ref" : { + "typ" : "kafka", + "parameters" : [ + { + "name" : "Topic", + "expression" : { + "language" : "spel", + "expression" : "'CustomerEvents'" + } + }, + { + "name" : "Schema version", + "expression" : { + "language" : "spel", + "expression" : "'latest'" + } + } + ] + }, + "additionalFields" : { + "description" : null, + "layoutData" : { + "x" : 180, + "y" : 0 + } + }, + "type" : "Source" + }, + { + "nextFalse" : [ + ], + "id" : "May client be interested in the new offer?", + "expression" : { + "language" : "spel", + "expression" : "#input.eventType.toString == \"ClientBrowseOffers\" || \n#input.eventType.toString == \"ClientSentTerminationLetter\"" + }, + "isDisabled" : null, + "additionalFields" : { + "description" : "We are interested only in two types of events: \"ClientSentTerminationLetter\", \"ClientBrowseOffers\"", + "layoutData" : { + "x" : 180, + "y" : 180 + } + }, + "type" : "Filter" + }, + { + "id" : "Get Customer Profile from API", + "service" : { + "id" : "getCustomerProfile", + "parameters" : [ + { + "name" : "customerId", + "expression" : { + "language" : "spel", + "expression" : "#input.customerId" + } + } + ] + }, + "output" : "profile", + "additionalFields" : { + "description" : "HTTP service is called to get customer profile using the \"customerId\" value from the event", + "layoutData" : { + "x" : 180, + "y" : 360 + } + }, + "type" : "Enricher" + }, + { + "nextFalse" : [ + ], + "id" : "Is adult customer?", + "expression" : { + "language" : "spel", + "expression" : "#profile.customerAge > 18" + }, + "isDisabled" : null, + "additionalFields" : { + "description" : "We are not interested in non-adult customers", + "layoutData" : { + "x" : 180, + "y" : 540 + } + }, + "type" : "Filter" + }, + { + "id" : "Get Offers for a given Customer Type from API", + "service" : { + "id" : "getOffersForCustomerType", + "parameters" : [ + { + "name" : "customerType", + "expression" : { + "language" : "spel", + "expression" : "#profile.customerType" + } + } + ] + }, + "output" : "offers", + "additionalFields" : { + "description" : "HTTP service is called to get offers for a passed customer type (taken from the response from the previous HTTP service call)", + "layoutData" : { + "x" : 180, + "y" : 720 + } + }, + "type" : "Enricher" + }, + { + "nextFalse" : [ + ], + "id" : "At least one offer exists?", + "expression" : { + "language" : "spel", + "expression" : "#offers.size > 0" + }, + "isDisabled" : null, + "additionalFields" : { + "description" : "We can continue if at least one offer is returned", + "layoutData" : { + "x" : 180, + "y" : 900 + } + }, + "type" : "Filter" + }, + { + "id" : "Pick the best offer", + "varName" : "offer", + "value" : { + "language" : "spel", + "expression" : "#offers.?[price == #COLLECTION.min(#offers.![price])][0]" + }, + "additionalFields" : { + "description" : "The offer with the best price is taken", + "layoutData" : { + "x" : 180, + "y" : 1080 + } + }, + "type" : "Variable" + }, + { + "nextFalse" : [ + { + "id" : "Random 4% discount", + "varName" : "specialDiscountPercentage", + "value" : { + "language" : "spel", + "expression" : "#RANDOM.nextInt(0,1000) == 0 ? 4 : 0" + }, + "additionalFields" : { + "description" : "1/1000 events will receive a 4% discount", + "layoutData" : { + "x" : 0, + "y" : 1620 + } + }, + "type" : "Variable" + }, + { + "definition" : { + "id" : "Random 4% discount", + "joinId" : "Union" + }, + "type" : "BranchEndData" + } + ], + "id" : "Is premium customer?", + "expression" : { + "language" : "spel", + "expression" : "#profile.isPremiumCustomer" + }, + "isDisabled" : null, + "additionalFields" : { + "description" : "We will give premium customers different discounts than regular customers.", + "layoutData" : { + "x" : 180, + "y" : 1260 + } + }, + "type" : "Filter" + }, + { + "id" : "Special discount depending on day of week (decision table)", + "service" : { + "id" : "decision-table", + "parameters" : [ + { + "name" : "Decision Table", + "expression" : { + "language" : "tabularDataDefinition", + "expression" : "{\n \"rows\": [\n [\n \"MONDAY\",\n \"0\"\n ],\n [\n \"TUESDAY\",\n \"0\"\n ],\n [\n \"WEDNESDAY\",\n \"3\"\n ],\n [\n \"THURSDAY\",\n \"0\"\n ],\n [\n \"FRIDAY\",\n \"5\"\n ],\n [\n \"SATURDAY\",\n \"5\"\n ],\n [\n \"SUNDAY\",\n \"5\"\n ],\n [\n null,\n null\n ]\n ],\n \"columns\": [\n {\n \"name\": \"DayOfWeek\",\n \"type\": \"java.lang.String\"\n },\n {\n \"name\": \"DiscountPercentage\",\n \"type\": \"java.lang.Integer\"\n }\n ]\n}" + } + }, + { + "name" : "Match condition", + "expression" : { + "language" : "spel", + "expression" : "#ROW.DayOfWeek == #DATE.nowAtDefaultTimeZone.getDayOfWeek.toString.toUpperCase" + } + } + ] + }, + "output" : "specialDiscountPercentages", + "additionalFields" : { + "description" : "Customer receives 5% discount on weekends and 3% discount on Wednesdays", + "layoutData" : { + "x" : 360, + "y" : 1440 + } + }, + "type" : "Enricher" + }, + { + "id" : "Extract the special discount", + "varName" : "specialDiscountPercentage", + "value" : { + "language" : "spel", + "expression" : "#specialDiscountPercentages.isEmpty ? \n0 : #specialDiscountPercentages[0].DiscountPercentage" + }, + "additionalFields" : { + "description" : null, + "layoutData" : { + "x" : 360, + "y" : 1620 + } + }, + "type" : "Variable" + }, + { + "definition" : { + "id" : "Extract the special discount", + "joinId" : "Union" + }, + "type" : "BranchEndData" + } + ], + "additionalBranches" : [ + [ + { + "id" : "Union", + "outputVar" : "context", + "nodeType" : "union", + "parameters" : [ + ], + "branchParameters" : [ + { + "branchId" : "Random 4% discount", + "parameters" : [ + { + "name" : "Output expression", + "expression" : { + "language" : "spel", + "expression" : "{ specialDiscountPercentage: #specialDiscountPercentage, offer: #offer, profile: #profile }" + } + } + ] + }, + { + "branchId" : "Extract the special discount", + "parameters" : [ + { + "name" : "Output expression", + "expression" : { + "language" : "spel", + "expression" : "{ specialDiscountPercentage: #specialDiscountPercentage, offer: #offer, profile: #profile }" + } + } + ] + } + ], + "additionalFields" : { + "description" : "it's workaround to unify context after the split: \nhttps://nussknacker.io/documentation/docs/next/scenarios_authoring/DesignerTipsAndTricks/#passing-the-context-after-the-union-node", + "layoutData" : { + "x" : 180, + "y" : 1800 + } + }, + "type" : "Join" + }, + { + "id" : "Calculate final price of the offer", + "varName" : "finalPrice", + "value" : { + "language" : "spel", + "expression" : "#context.offer.price - #context.specialDiscountPercentage * #context.offer.price" + }, + "additionalFields" : { + "description" : "Final price of the offer is calculated here", + "layoutData" : { + "x" : 180, + "y" : 1980 + } + }, + "type" : "Variable" + }, + { + "id" : "Prepare full offer message", + "varName" : "offerMessage", + "value" : { + "language" : "spel", + "expression" : "'Hello ' + #context.profile.customerName + \"! \\n\" + #context.offer.name + \"\\n\\n\" + #context.offer.message + \"\\nValid to \" + #DATE_FORMAT.formatter('yyyy-MM-dd HH:mm').format(#context.offer.validity) + \". Don't miss it!\"" + }, + "additionalFields" : { + "description" : "Offer message is prepared here", + "layoutData" : { + "x" : 180, + "y" : 2160 + } + }, + "type" : "Variable" + }, + { + "id" : "Offer proposal to customer", + "ref" : { + "typ" : "kafka", + "parameters" : [ + { + "name" : "Topic", + "expression" : { + "language" : "spel", + "expression" : "'OfferProposalsBasedOnCustomerEvents'" + } + }, + { + "name" : "Schema version", + "expression" : { + "language" : "spel", + "expression" : "'latest'" + } + }, + { + "name" : "Key", + "expression" : { + "language" : "spel", + "expression" : "" + } + }, + { + "name" : "Raw editor", + "expression" : { + "language" : "spel", + "expression" : "false" + } + }, + { + "name" : "amount", + "expression" : { + "language" : "spel", + "expression" : "#context.offer.price" + } + }, + { + "name" : "offerName", + "expression" : { + "language" : "spel", + "expression" : "#context.offer.name" + } + }, + { + "name" : "clientName", + "expression" : { + "language" : "spel", + "expression" : "#context.profile.customerName" + } + }, + { + "name" : "profileId", + "expression" : { + "language" : "spel", + "expression" : "#context.profile.id" + } + }, + { + "name" : "offerDescription", + "expression" : { + "language" : "spel", + "expression" : "#context.offer.message" + } + }, + { + "name" : "dueDate", + "expression" : { + "language" : "spel", + "expression" : "#context.offer.validity" + } + }, + { + "name" : "customerId", + "expression" : { + "language" : "spel", + "expression" : "#context.profile.customerId" + } + }, + { + "name" : "clientMsisdn", + "expression" : { + "language" : "spel", + "expression" : "#context.profile.customerMsisdn" + } + }, + { + "name" : "preparedMessageReadyToSend", + "expression" : { + "language" : "spel", + "expression" : "#offerMessage" + } + } + ] + }, + "endResult" : null, + "isDisabled" : null, + "additionalFields" : { + "description" : "We put the offer in a Kafka topic. Some other system will take care of sending it.", + "layoutData" : { + "x" : 180, + "y" : 2340 + } + }, + "type" : "Sink" + } + ] + ] +} \ No newline at end of file diff --git a/scenario-examples-library/offer-customer-proposal-based-on-activity-event/data/kafka/generated/customerEvents.sh b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/data/kafka/generated/customerEvents.sh new file mode 100755 index 0000000..476afdd --- /dev/null +++ b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/data/kafka/generated/customerEvents.sh @@ -0,0 +1,10 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source /app/utils/lib.sh + +ID=$(random_Ndigit_number 10) +EVENT_TYPE="$(pick_randomly "ClientCloseToShowroom" "ClientBrowseOffers" "ClientEndedCallWithCustomerService" "ClientSentTerminationLetter" "Other")" + +echo "{ \"customerId\": \"$ID\", \"eventType\": \"$EVENT_TYPE\" }" \ No newline at end of file diff --git a/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/__files/customer-api/openapi/CustomerApi.yaml b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/__files/customer-api/openapi/CustomerApi.yaml new file mode 100644 index 0000000..2dd082f --- /dev/null +++ b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/__files/customer-api/openapi/CustomerApi.yaml @@ -0,0 +1,106 @@ +openapi: 3.0.0 +info: + title: Customer API + version: 1.0.0 + description: API for retrieving customer profiles and offers based on customer type. + +paths: + '/customer/{customerId}/profile': + get: + summary: Get customer profile by customer's ID + description: Retrieve detailed profile information for a customer using their unique customer ID. + operationId: getCustomerProfile + parameters: + - name: customerId + in: path + required: true + description: The unique identifier of the customer. + schema: + type: string + responses: + '200': + description: Customer profile found successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/CustomerProfile' + '404': + description: No customer profile found for the given ID. + + '/customer/{customerType}/offers': + get: + summary: Get offers by customer type + description: Retrieve offers available for a specific type of customer. + operationId: getOffersForCustomerType + parameters: + - name: customerType + in: path + required: true + description: The type/category of the customer. + schema: + type: string + responses: + '200': + description: Offers found for the specified customer type. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CustomerTypeOffer' + '404': + description: No offer found for the specified customer type. + +components: + schemas: + CustomerProfile: + title: CustomerProfile + type: object + description: Schema representing a customer's profile information. + properties: + id: + type: string + description: The unique identifier of the customer profile. + customerId: + type: string + description: The identifier of customer + customerType: + type: string + description: The type or category of the customer. + customerName: + type: string + description: The customer's name + customerMsisdn: + type: string + description: The customer's phone number + customerAge: + type: integer + description: The customer's age + customerSex: + type: string + description: The customer's sex + isPremiumCustomer: + type: boolean + description: Indicates if the customer is a premium one + + CustomerTypeOffer: + title: CustomerTypeOffer + type: object + description: Schema representing an offer available for a specific type of customer. + properties: + id: + type: string + description: The unique identifier of the offer. + name: + type: string + description: The name or title of the offer. + message: + type: string + description: A human-readable offer description + price: + type: integer + description: Price of the offer + validity: + type: string + format: date-time + description: The validity date of the offer. diff --git a/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/__files/customer-api/responses/CustomerProfile.json b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/__files/customer-api/responses/CustomerProfile.json new file mode 100644 index 0000000..4c43f80 --- /dev/null +++ b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/__files/customer-api/responses/CustomerProfile.json @@ -0,0 +1,10 @@ +{ + "id": "{{randomValue length=10 type='NUMERIC'}}", + "customerId": "{{request.path.[1]}}", + "customerType": "{{{pickRandom 'Freemium' 'Regular' 'VIP'}}}", + "customerName": "{{random 'Name.fullName'}}", + "customerMsisdn": "{{random 'PhoneNumber.phoneNumber'}}", + "customerAge": {{randomInt lower=10 upper=99}}, + "customerSex": "{{{pickRandom 'Male' 'Female' 'N/A'}}}", + "isPremiumCustomer": {{random 'Bool.bool'}} +} \ No newline at end of file diff --git a/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/__files/customer-api/responses/CustomerTypeOffers0.json b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/__files/customer-api/responses/CustomerTypeOffers0.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/__files/customer-api/responses/CustomerTypeOffers0.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/__files/customer-api/responses/CustomerTypeOffers1.json b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/__files/customer-api/responses/CustomerTypeOffers1.json new file mode 100644 index 0000000..704ceea --- /dev/null +++ b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/__files/customer-api/responses/CustomerTypeOffers1.json @@ -0,0 +1,9 @@ +[ + { + "id": "{{randomValue length=10 type='NUMERIC'}}", + "name": "{{random 'Commerce.productName'}} promotion", + "message": "{{random 'Lorem.sentence'}} {{random 'Lorem.sentence'}} {{random 'Lorem.sentence'}}", + "price": {{randomInt lower=10 upper=50}}, + "validity": "{{dateFormat (now offset='3 days') format='yyyy-MM-dd\'T\'HH:mm:ss\'Z\''}}" + } +] \ No newline at end of file diff --git a/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/__files/customer-api/responses/CustomerTypeOffers2.json b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/__files/customer-api/responses/CustomerTypeOffers2.json new file mode 100644 index 0000000..af6af79 --- /dev/null +++ b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/__files/customer-api/responses/CustomerTypeOffers2.json @@ -0,0 +1,16 @@ +[ + { + "id": "{{randomValue length=10 type='NUMERIC'}}", + "name": "{{random 'Commerce.productName'}} promotion", + "message": "{{random 'Lorem.sentence'}} {{random 'Lorem.sentence'}} {{random 'Lorem.sentence'}}", + "price": {{randomInt lower=10 upper=50}}, + "validity": "{{dateFormat (now offset='3 days') format='yyyy-MM-dd\'T\'HH:mm:ss\'Z\''}}" + }, + { + "id": "{{randomValue length=10 type='NUMERIC'}}", + "name": "{{random 'Commerce.productName'}} promotion", + "message": "{{random 'Lorem.sentence'}} {{random 'Lorem.sentence'}} {{random 'Lorem.sentence'}}", + "price": {{randomInt lower=10 upper=50}}, + "validity": "{{dateFormat (now offset='3 days') format='yyyy-MM-dd\'T\'HH:mm:ss\'Z\''}}" + } +] \ No newline at end of file diff --git a/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/mappings/customer-api/GetCustomerProfile.json b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/mappings/customer-api/GetCustomerProfile.json new file mode 100644 index 0000000..57153c0 --- /dev/null +++ b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/mappings/customer-api/GetCustomerProfile.json @@ -0,0 +1,13 @@ +{ + "request": { + "urlPattern": "/customer/(.+)/profile", + "method": "GET" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "customer-api/responses/CustomerProfile.json" + } +} \ No newline at end of file diff --git a/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/mappings/customer-api/GetOffersForCusomerType.json b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/mappings/customer-api/GetOffersForCusomerType.json new file mode 100644 index 0000000..d19260a --- /dev/null +++ b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/mocks/http-service/customer-api/mappings/customer-api/GetOffersForCusomerType.json @@ -0,0 +1,26 @@ +{ + "mappings": [ + { + "request": { + "urlPattern": "/customer/(.+)/offers", + "method": "GET" + }, + "response": { + "status": 404 + } + }, + { + "request": { + "urlPattern": "/customer/(?i)(Freemium|Regular|VIP)/offers", + "method": "GET" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "customer-api/responses/CustomerTypeOffers{{{pickRandom '0' '1' '2'}}}.json" + } + } + ] +} \ No newline at end of file diff --git a/scenario-examples-library/offer-customer-proposal-based-on-activity-event/setup/kafka/topics.txt b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/setup/kafka/topics.txt new file mode 100644 index 0000000..28e852c --- /dev/null +++ b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/setup/kafka/topics.txt @@ -0,0 +1,2 @@ +CustomerEvents +OfferProposalsBasedOnCustomerEvents diff --git a/scenario-examples-library/offer-customer-proposal-based-on-activity-event/setup/nu-designer/custom-configuration.conf b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/setup/nu-designer/custom-configuration.conf new file mode 100644 index 0000000..b26d8dc --- /dev/null +++ b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/setup/nu-designer/custom-configuration.conf @@ -0,0 +1,19 @@ +## To customize Nu Designer configuration see https://nussknacker.io/documentation/docs/configuration/Common/#configuration-file + +scenarioTypes { + "streaming" { + # customize Flink streaming scenario type + modelConfig { + components { + # OpenAPI enrichers + "customerProfileOffers" { + providerType: "openAPI" + url: "http://quickstart-setup:8080/__admin/files/customer-api/openapi/CustomerApi.yaml" + rootUrl: "http://quickstart-setup:8080/" + namePattern: "get.*" + allowedMethods: ["GET"] + } + } + } + } +} diff --git a/scenario-examples-library/offer-customer-proposal-based-on-activity-event/setup/schema-registry/CustomerEvents.schema.json b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/setup/schema-registry/CustomerEvents.schema.json new file mode 100644 index 0000000..933a3c2 --- /dev/null +++ b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/setup/schema-registry/CustomerEvents.schema.json @@ -0,0 +1,26 @@ +{ + "type": "object", + "title": "CustomerEvent", + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "customerId", + "eventType" + ], + "properties": { + "customerId": { + "description": "The customer unique identifier", + "type": "string" + }, + "eventType": { + "description": "Type of event", + "type": "string", + "enum": [ + "ClientCloseToShowroom", + "ClientBrowseOffers", + "ClientEndedCallWithCustomerService", + "ClientSentTerminationLetter", + "Other" + ] + } + } +} diff --git a/scenario-examples-library/offer-customer-proposal-based-on-activity-event/setup/schema-registry/OfferProposalsBasedOnCustomerEvents.schema.json b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/setup/schema-registry/OfferProposalsBasedOnCustomerEvents.schema.json new file mode 100644 index 0000000..ddcec28 --- /dev/null +++ b/scenario-examples-library/offer-customer-proposal-based-on-activity-event/setup/schema-registry/OfferProposalsBasedOnCustomerEvents.schema.json @@ -0,0 +1,49 @@ +{ + "type": "object", + "title": "OfferProposal", + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "offerName", + "offerDescription", + "clientName", + "amount", + "dueDate" + ], + "properties": { + "customerId": { + "type": "string" + }, + "profileId": { + "type": "string" + }, + "clientName": { + "type": "string", + "description": "The person's full name" + }, + "clientMsisdn": { + "type": "string", + "description": "Client's phone number" + }, + "offerName": { + "type": "string", + "description": "The assigned offer for the person" + }, + "offerDescription": { + "type": "string", + "description": "The assigned offer human readable description" + }, + "amount": { + "type": "integer", + "description": "The offer price" + }, + "dueDate": { + "type": "string", + "format": "date-time", + "description": "The expiration date for the offer" + }, + "preparedMessageReadyToSend": { + "type": "string", + "description": "The human-readable message. It should be prepared to be sent without further modifications." + } + } +} diff --git a/scenario-examples-library/rtm-client-near-pos/RTMClientNearPOS.json b/scenario-examples-library/rtm-client-near-pos/RTMClientNearPOS.json new file mode 100644 index 0000000..8ec020b --- /dev/null +++ b/scenario-examples-library/rtm-client-near-pos/RTMClientNearPOS.json @@ -0,0 +1,660 @@ +{ + "metaData": { + "id": "RTMClientNearPOS", + "additionalFields": { + "description": null, + "properties": { + "parallelism": "1", + "spillStateToDisk": "true", + "useAsyncInterpretation": "", + "checkpointIntervalInSeconds": "" + }, + "metaDataType": "StreamMetaData" + } + }, + "nodes": [ + { + "id": "stream of given clients geo location", + "ref": { + "typ": "kafka", + "parameters": [ + { + "name": "Topic", + "expression": { + "language": "spel", + "expression": "'GeoLocations'" + } + }, + { + "name": "Schema version", + "expression": { + "language": "spel", + "expression": "'latest'" + } + } + ] + }, + "additionalFields": { + "description": "geo locations stream", + "layoutData": { + "x": 360, + "y": 0 + } + }, + "type": "Source" + }, + { + "id": "enrich with contact history data", + "service": { + "id": "customers-data-query-enricher", + "parameters": [ + { + "name": "Result strategy", + "expression": { + "language": "spel", + "expression": "'Result set'" + } + }, + { + "name": "Query", + "expression": { + "language": "spelTemplate", + "expression": "select 'x' from contact_history where client_id = ? and event_time > NOW() - INTERVAL '5 minutes'" + } + }, + { + "name": "Cache TTL", + "expression": { + "language": "spel", + "expression": "T(java.time.Duration).parse('PT1M')" + } + }, + { + "name": "arg1", + "expression": { + "language": "spel", + "expression": "#input.clientId" + } + } + ] + }, + "output": "contact_history_entry", + "additionalFields": { + "description": "Wer are fetching given client contact history from the last X days.\nFor the purpose of example we are fetching entries from the last 5 minutes instead of days or longer period.", + "layoutData": { + "x": 360, + "y": 180 + } + }, + "type": "Enricher" + }, + { + "nextFalse": [ + ], + "id": "client is not contacted in last X Days", + "expression": { + "language": "spel", + "expression": "#contact_history_entry.empty" + }, + "isDisabled": null, + "additionalFields": { + "description": "The main goal is not to send too many notifications to the given client.", + "layoutData": { + "x": 360, + "y": 360 + } + }, + "type": "Filter" + }, + { + "id": "enrich with data from list of blocked clients", + "service": { + "id": "customers-data-lookup-enricher", + "parameters": [ + { + "name": "Table", + "expression": { + "language": "spel", + "expression": "'blocked_list'" + } + }, + { + "name": "Cache TTL", + "expression": { + "language": "spel", + "expression": "T(java.time.Duration).parse('PT1M')" + } + }, + { + "name": "Key column", + "expression": { + "language": "spel", + "expression": "'client_id'" + } + }, + { + "name": "Key value", + "expression": { + "language": "spel", + "expression": "#input.clientId" + } + } + ] + }, + "output": "is_client_on_blocked_list", + "additionalFields": { + "description": null, + "layoutData": { + "x": 360, + "y": 540 + } + }, + "type": "Enricher" + }, + { + "nextFalse": [ + ], + "id": "client is not blocked", + "expression": { + "language": "spel", + "expression": "#is_client_on_blocked_list == null" + }, + "isDisabled": null, + "additionalFields": { + "description": null, + "layoutData": { + "x": 360, + "y": 720 + } + }, + "type": "Filter" + }, + { + "id": "enrich with client data", + "service": { + "id": "customers-data-lookup-enricher", + "parameters": [ + { + "name": "Table", + "expression": { + "language": "spel", + "expression": "'client'" + } + }, + { + "name": "Cache TTL", + "expression": { + "language": "spel", + "expression": "T(java.time.Duration).parse('PT5M')" + } + }, + { + "name": "Key column", + "expression": { + "language": "spel", + "expression": "'id'" + } + }, + { + "name": "Key value", + "expression": { + "language": "spel", + "expression": "#input.clientId" + } + } + ] + }, + "output": "clientData", + "additionalFields": { + "description": null, + "layoutData": { + "x": 360, + "y": 900 + } + }, + "type": "Enricher" + }, + { + "id": "extract consents from client data", + "varName": "consents", + "value": { + "language": "spel", + "expression": "#UTIL.split(#clientData.consents, '_')" + }, + "additionalFields": { + "description": "We are transforming and then assigning consents to variable consents", + "layoutData": { + "x": 360, + "y": 1080 + } + }, + "type": "Variable" + }, + { + "nextFalse": [ + ], + "id": "client has marketing consents", + "expression": { + "language": "spel", + "expression": "NOT #consents.isEmpty" + }, + "isDisabled": false, + "additionalFields": { + "description": "#clientData?.consents?", + "layoutData": { + "x": 360, + "y": 1260 + } + }, + "type": "Filter" + }, + { + "id": "enrich with POS info", + "service": { + "id": "pos-data-lookup-enricher", + "parameters": [ + { + "name": "Table", + "expression": { + "language": "spel", + "expression": "'pos'" + } + }, + { + "name": "Cache TTL", + "expression": { + "language": "spel", + "expression": "T(java.time.Duration).parse('PT10M')" + } + }, + { + "name": "Key column", + "expression": { + "language": "spel", + "expression": "'id'" + } + }, + { + "name": "Key value", + "expression": { + "language": "spel", + "expression": "#clientData.pos_id" + } + } + ] + }, + "output": "pos_data", + "additionalFields": { + "description": null, + "layoutData": { + "x": 360, + "y": 1440 + } + }, + "type": "Enricher" + }, + { + "nextFalse": [ + ], + "id": "POS is currently opened", + "expression": { + "language": "spel", + "expression": "#pos_data != null AND #pos_data.open_hour != null AND #pos_data.close_hour != null AND #DATE.isBetween(#DATE.nowAtZone('Europe/Warsaw').toLocalTime, #pos_data.open_hour, #pos_data.close_hour)" + }, + "isDisabled": null, + "additionalFields": { + "description": "POS open weekdays should be checked too", + "layoutData": { + "x": 360, + "y": 1620 + } + }, + "type": "Filter" + }, + { + "id": "Count when distance to POS is lower than 1 km", + "outputVar": "agg_out", + "nodeType": "aggregate-session", + "parameters": [ + { + "name": "groupBy", + "expression": { + "language": "spel", + "expression": "#input.clientId + ''" + } + }, + { + "name": "aggregator", + "expression": { + "language": "spel", + "expression": "#AGG.countWhen" + } + }, + { + "name": "aggregateBy", + "expression": { + "language": "spel", + "expression": "#GEO.distanceInKm(#input.geo.lat, #input.geo.lon, #pos_data.location_lat, #pos_data.location_lon) <= 1" + } + }, + { + "name": "endSessionCondition", + "expression": { + "language": "spel", + "expression": "false" + } + }, + { + "name": "sessionTimeout", + "expression": { + "language": "spel", + "expression": "T(java.time.Duration).parse('PT10S')" + } + }, + { + "name": "emitWhen", + "expression": { + "language": "spel", + "expression": "T(pl.touk.nussknacker.engine.flink.util.transformer.aggregate.SessionWindowTrigger).OnEvent" + } + } + ], + "additionalFields": { + "description": "We are counting events when a customer is within one kilometre of the point of sale assigned to that customer. For the purpose of the example we set the timeout to 10 seconds, in the real scenario the value should be higher.", + "layoutData": { + "x": 360, + "y": 1800 + } + }, + "type": "CustomNode" + }, + { + "nextFalse": [ + ], + "id": "2 events received with location near POS from session of X time", + "expression": { + "language": "spel", + "expression": "#agg_out == 2" + }, + "isDisabled": null, + "additionalFields": { + "description": "If we receive at least 2 events where the customer location meets our requirements within 10 seconds, we process further.", + "layoutData": { + "x": 360, + "y": 1980 + } + }, + "type": "Filter" + }, + { + "id": "decisions priorities and notification content by consent and client type", + "service": { + "id": "decision-table", + "parameters": [ + { + "name": "Decision Table", + "expression": { + "language": "tabularDataDefinition", + "expression": "{\n \"rows\": [\n [\n \"SMS\",\n \"INDIVIDUAL\",\n \"3\",\n \"You are close to our shop come and see new offers!\"\n ],\n [\n \"EMAIL\",\n \"INDIVIDUAL\",\n \"1\",\n \"You are close to our shop come and see new offers!\"\n ],\n [\n \"PUSH\",\n \"INDIVIDUAL\",\n \"2\",\n \"You are close to our shop come and see new offers!\"\n ],\n [\n \"SMS\",\n \"BUSINESS\",\n \"2\",\n \"You are close to our shop come and see new business offers!\"\n ],\n [\n \"EMAIL\",\n \"BUSINESS\",\n \"3\",\n \"You are close to our shop come and see new business offers!\"\n ],\n [\n \"PUSH\",\n \"BUSINESS\",\n \"1\",\n \"You are close to our shop come and see new business offers!\"\n ]\n ],\n \"columns\": [\n {\n \"name\": \"Consent\",\n \"type\": \"java.lang.String\"\n },\n {\n \"name\": \"Client type\",\n \"type\": \"java.lang.String\"\n },\n {\n \"name\": \"Priority\",\n \"type\": \"java.lang.Integer\"\n },\n {\n \"name\": \"Notification content\",\n \"type\": \"java.lang.String\"\n }\n ]\n}" + } + }, + { + "name": "Match condition", + "expression": { + "language": "spel", + "expression": "#consents.^[#this == #ROW.Consent] != null AND #clientData.client_type == #ROW['Client type']" + } + } + ] + }, + "output": "decisionsWithPrioritiesAndNotificationContent", + "additionalFields": { + "description": "In the decision table, we define the business rules that we are trying to match with the 'matching condition' and then pass the matched decisions on.", + "layoutData": { + "x": 360, + "y": 2160 + } + }, + "type": "Enricher" + }, + { + "id": "choose best decision", + "varName": "decision", + "value": { + "language": "spel", + "expression": "#decisionsWithPrioritiesAndNotificationContent?.^[#this.Priority == 3] ?: (#decisionsWithPrioritiesAndNotificationContent?.^[#this.Priority == 2] ?: (#decisionsWithPrioritiesAndNotificationContent?.^[#this.Priority == 1]))" + }, + "additionalFields": { + "description": "We can apply various business rules to choose best decisions here", + "layoutData": { + "x": 360, + "y": 2340 + } + }, + "type": "Variable" + }, + { + "defaultNext": [ + ], + "nexts": [ + { + "expression": { + "language": "spel", + "expression": "#decision?.Consent == \"SMS\"" + }, + "nodes": [ + { + "id": "SMS", + "ref": { + "typ": "kafka", + "parameters": [ + { + "name": "Topic", + "expression": { + "language": "spel", + "expression": "'GeoLocationsOutputSms'" + } + }, + { + "name": "Schema version", + "expression": { + "language": "spel", + "expression": "'latest'" + } + }, + { + "name": "Key", + "expression": { + "language": "spel", + "expression": "" + } + }, + { + "name": "Raw editor", + "expression": { + "language": "spel", + "expression": "false" + } + }, + { + "name": "msisdn", + "expression": { + "language": "spel", + "expression": "#clientData.msisdn" + } + }, + { + "name": "content", + "expression": { + "language": "spel", + "expression": "#decision['Notification content']" + } + } + ] + }, + "endResult": null, + "isDisabled": null, + "additionalFields": { + "description": null, + "layoutData": { + "x": 0, + "y": 2700 + } + }, + "type": "Sink" + } + ] + }, + { + "expression": { + "language": "spel", + "expression": "#decision?.Consent == \"EMAIL\"" + }, + "nodes": [ + { + "id": "EMAIL", + "ref": { + "typ": "kafka", + "parameters": [ + { + "name": "Topic", + "expression": { + "language": "spel", + "expression": "'GeoLocationsOutputEmail'" + } + }, + { + "name": "Schema version", + "expression": { + "language": "spel", + "expression": "'latest'" + } + }, + { + "name": "Key", + "expression": { + "language": "spel", + "expression": "" + } + }, + { + "name": "Raw editor", + "expression": { + "language": "spel", + "expression": "false" + } + }, + { + "name": "email", + "expression": { + "language": "spel", + "expression": "#clientData.email" + } + }, + { + "name": "content", + "expression": { + "language": "spel", + "expression": "#decision['Notification content']" + } + } + ] + }, + "endResult": null, + "isDisabled": null, + "additionalFields": { + "description": null, + "layoutData": { + "x": 360, + "y": 2700 + } + }, + "type": "Sink" + } + ] + }, + { + "expression": { + "language": "spel", + "expression": "#decision?.Consent == 'PUSH'" + }, + "nodes": [ + { + "id": "PUSH", + "ref": { + "typ": "kafka", + "parameters": [ + { + "name": "Topic", + "expression": { + "language": "spel", + "expression": "'GeoLocationsOutputPush'" + } + }, + { + "name": "Schema version", + "expression": { + "language": "spel", + "expression": "'latest'" + } + }, + { + "name": "Key", + "expression": { + "language": "spel", + "expression": "" + } + }, + { + "name": "Raw editor", + "expression": { + "language": "spel", + "expression": "false" + } + }, + { + "name": "msisdn", + "expression": { + "language": "spel", + "expression": "#clientData.msisdn" + } + }, + { + "name": "content", + "expression": { + "language": "spel", + "expression": "#decision['Notification content']" + } + } + ] + }, + "endResult": null, + "isDisabled": null, + "additionalFields": { + "description": null, + "layoutData": { + "x": 720, + "y": 2700 + } + }, + "type": "Sink" + } + ] + } + ], + "id": "split by notification type", + "expression": null, + "exprVal": null, + "additionalFields": { + "description": "We direct the results to the appropriate topic", + "layoutData": { + "x": 360, + "y": 2520 + } + }, + "type": "Switch" + } + ], + "additionalBranches": [ + ] +} \ No newline at end of file diff --git a/scenario-examples-library/rtm-client-near-pos/data/kafka/generated/geoLocations.sh b/scenario-examples-library/rtm-client-near-pos/data/kafka/generated/geoLocations.sh new file mode 100755 index 0000000..3f247ee --- /dev/null +++ b/scenario-examples-library/rtm-client-near-pos/data/kafka/generated/geoLocations.sh @@ -0,0 +1,25 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source /app/utils/lib.sh + +case $(($(random_1digit_number) % 3)) in + 0) + LAT="52.23$(random_3digit_number)" + LON="21.01$(random_3digit_number)" + ;; + 1) + LAT="50.04$(random_3digit_number)" + LON="19.94$(random_3digit_number)" + ;; + *) + LAT="51.10$(random_3digit_number)" + LON="17.03$(random_3digit_number)" + ;; +esac + +CLIENT_ID=$(random_1digit_number) +TIME=$(date +%s) + +echo "{\"clientId\": $CLIENT_ID, \"geo\": { \"lat\": $LAT, \"lon\": $LON }, \"eventTime\": $TIME }" diff --git a/scenario-examples-library/rtm-client-near-pos/data/kafka/static/geoLocations.txt b/scenario-examples-library/rtm-client-near-pos/data/kafka/static/geoLocations.txt new file mode 100644 index 0000000..84c8efc --- /dev/null +++ b/scenario-examples-library/rtm-client-near-pos/data/kafka/static/geoLocations.txt @@ -0,0 +1,3 @@ +# Example messages below (message per line) +# {"clientId": 1,"geo": {"lat": 52.237049, "lon": 21.017532},"eventTime": 1720166429} +# {"clientId": 9,"geo": {"lat": 51.107883, "lon": 17.038538},"eventTime": 1720166429} diff --git a/scenario-examples-library/rtm-client-near-pos/mocks/db/rtm_near_pos.sql b/scenario-examples-library/rtm-client-near-pos/mocks/db/rtm_near_pos.sql new file mode 100644 index 0000000..6079bbf --- /dev/null +++ b/scenario-examples-library/rtm-client-near-pos/mocks/db/rtm_near_pos.sql @@ -0,0 +1,61 @@ +-- cleanup for the sake idempotent run +drop table if exists contact_history; +drop table if exists blocked_list; +drop table if exists client; +drop table if exists pos; +drop type if exists consent_enum; +drop type if exists client_type_enum; + +create table pos +( + id SERIAL PRIMARY KEY, + location_lat NUMERIC(10, 6) NOT NULL, + location_lon NUMERIC(10, 6) NOT NULL, + open_hour TIME NOT NULL, + close_hour TIME NOT NULL +); + +create type consent_enum AS ENUM ('SMS', 'EMAIL', 'PUSH', 'SMS_EMAIL', 'EMAIL_PUSH', 'SMS_PUSH', 'SMS_EMAIL_PUSH'); +create type client_type_enum AS ENUM ('INDIVIDUAL', 'BUSINESS'); + +create table client +( + id SERIAL PRIMARY KEY, + pos_id SERIAL REFERENCES pos(id) NOT NULL, + msisdn CHAR(11), + email VARCHAR(100), + consents consent_enum, + client_type client_type_enum NOT NULL +); + +create table blocked_list( + client_id INT PRIMARY KEY references client(id) +); + +create table contact_history( + id SERIAL PRIMARY KEY, + client_id INT NOT NULL references client(id), + event_time TIMESTAMP NOT NULL +); + +---- POS +insert into pos(id, location_lat, location_lon, open_hour, close_hour) VALUES (1, 52.237049, 21.017532, '00:00:00', '23:59:59'); +insert into pos(id, location_lat, location_lon, open_hour, close_hour) VALUES (2, 50.049683, 19.944544, '08:00:00', '15:00:00'); +insert into pos(id, location_lat, location_lon, open_hour, close_hour) VALUES (3, 51.107883, 17.038538, '00:00:00', '23:59:59'); + +---- Clients +insert into client(id, pos_id, msisdn, email, consents, client_type) VALUES (1, 1, '48500500500', 'jan.kowalski@nussknacker.io', 'SMS', 'INDIVIDUAL'); +insert into client(id, pos_id, msisdn, email, consents, client_type) VALUES (2, 1, '48500500501', 'zbigniew.paleta@nussknacker.io', 'SMS_EMAIL', 'BUSINESS'); +insert into client(id, pos_id, msisdn, email, consents, client_type) VALUES (3, 1, '48500500502', 'genia.nowak@nussknacker.io', 'PUSH', 'INDIVIDUAL'); +insert into client(id, pos_id, msisdn, email, consents, client_type) VALUES (4, 2, '48500500503', 'klaudia.wisniewska@nussknacker.io', 'SMS_EMAIL_PUSH', 'INDIVIDUAL'); +insert into client(id, pos_id, msisdn, email, consents, client_type) VALUES (5, 2, '48500500504', 'teofil.benc@nussknacker.io', 'EMAIL', 'BUSINESS'); +insert into client(id, pos_id, msisdn, email, consents, client_type) VALUES (6, 2, '48500500505', 'zdzislaw.lecina@nussknacker.io', null, 'BUSINESS'); +insert into client(id, pos_id, msisdn, email, consents, client_type) VALUES (7, 2, '48500500506', 'ksenia.gorka@nussknacker.io', 'EMAIL_PUSH', 'INDIVIDUAL'); +insert into client(id, pos_id, msisdn, email, consents, client_type) VALUES (8, 3, '48500500507', 'anna.milkowska@nussknacker.io', 'SMS_PUSH', 'BUSINESS'); +insert into client(id, pos_id, msisdn, email, consents, client_type) VALUES (9, 3, '48500500508', 'john.doe@nussknacker.io', 'EMAIL', 'INDIVIDUAL'); + +---- Blocked +insert into blocked_list(client_id) values (5); + +-- Contact history +insert into contact_history(client_id, event_time) VALUES (9, NOW() - INTERVAL '1 minutes'); diff --git a/scenario-examples-library/rtm-client-near-pos/setup/kafka/topics.txt b/scenario-examples-library/rtm-client-near-pos/setup/kafka/topics.txt new file mode 100644 index 0000000..5d9d2ae --- /dev/null +++ b/scenario-examples-library/rtm-client-near-pos/setup/kafka/topics.txt @@ -0,0 +1,4 @@ +GeoLocations +GeoLocationsOutputEmail +GeoLocationsOutputSms +GeoLocationsOutputPush diff --git a/scenario-examples-library/rtm-client-near-pos/setup/nu-designer/custom-configuration.conf b/scenario-examples-library/rtm-client-near-pos/setup/nu-designer/custom-configuration.conf new file mode 100644 index 0000000..61b3869 --- /dev/null +++ b/scenario-examples-library/rtm-client-near-pos/setup/nu-designer/custom-configuration.conf @@ -0,0 +1,41 @@ +scenarioTypes { + "streaming" { + # customize Flink streaming scenario type + modelConfig { + components { + # Database entichers + "customersDataEnricher" { + providerType: databaseEnricher + config: { + databaseQueryEnricher { + name: "customers-data-query-enricher" + dbPool: ${rtmNearPosExampleDatabasePool} #refers to your database pool definition + } + databaseLookupEnricher { + name: "customers-data-lookup-enricher" + dbPool: ${rtmNearPosExampleDatabasePool} + } + } + } + "posDataEnricher" { + providerType: databaseEnricher + config: { + databaseLookupEnricher { + name: "pos-data-lookup-enricher" + dbPool: ${rtmNearPosExampleDatabasePool} + } + } + } + } + } + } +} + +# Database definition +rtmNearPosExampleDatabasePool { + driverClassName: "org.postgresql.Driver" + url: "jdbc:postgresql://quickstart-setup:5432/mocks" + username: "mocks" + password: "mocks_pass" + schema: "rtm_near_pos" +} diff --git a/scenario-examples-library/rtm-client-near-pos/setup/schema-registry/GeoLocations.schema.json b/scenario-examples-library/rtm-client-near-pos/setup/schema-registry/GeoLocations.schema.json new file mode 100644 index 0000000..6e2aaa3 --- /dev/null +++ b/scenario-examples-library/rtm-client-near-pos/setup/schema-registry/GeoLocations.schema.json @@ -0,0 +1,38 @@ +{ + "type": "object", + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema", + "required": [ + "clientId", + "geo", + "eventTime" + ], + "properties": { + "geo": { + "type": "object", + "description": "This property contains geographical coordinates (latitude and longitude).", + "required": [ + "lat", + "lon" + ], + "properties": { + "lon": { + "type": "number", + "description": "Longitude coordinate." + }, + "lat": { + "type": "number", + "description": "Latitude coordinate." + } + } + }, + "clientId": { + "type": "integer", + "description": "A unique identifier for the client." + }, + "eventTime": { + "type": "integer", + "description": "A timestamp indicating the time of the event." + } + } +} diff --git a/scenario-examples-library/rtm-client-near-pos/setup/schema-registry/GeoLocationsOutputEmail.schema.json b/scenario-examples-library/rtm-client-near-pos/setup/schema-registry/GeoLocationsOutputEmail.schema.json new file mode 100644 index 0000000..2d550d3 --- /dev/null +++ b/scenario-examples-library/rtm-client-near-pos/setup/schema-registry/GeoLocationsOutputEmail.schema.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "additionalProperties": false, + "description": "A schema for a topic used to send emails.", + "$schema": "http://json-schema.org/draft-07/schema", + "required": [ + "email", + "content" + ], + "properties": { + "email": { + "type": "string", + "description": "The email address of the client." + }, + "content": { + "type": "string", + "description": "The content or message to be associated with the email." + } + } +} diff --git a/scenario-examples-library/rtm-client-near-pos/setup/schema-registry/GeoLocationsOutputPush.schema.json b/scenario-examples-library/rtm-client-near-pos/setup/schema-registry/GeoLocationsOutputPush.schema.json new file mode 100644 index 0000000..d9d2d2e --- /dev/null +++ b/scenario-examples-library/rtm-client-near-pos/setup/schema-registry/GeoLocationsOutputPush.schema.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "additionalProperties": false, + "description": "A schema for a topic used to send pushes.", + "$schema": "http://json-schema.org/draft-07/schema", + "required": [ + "msisdn", + "content" + ], + "properties": { + "msisdn": { + "type": "string", + "description": "The phone number of the client." + }, + "content": { + "type": "string", + "description": "The content or message to be associated with the email." + } + } +} diff --git a/scenario-examples-library/rtm-client-near-pos/setup/schema-registry/GeoLocationsOutputSms.schema.json b/scenario-examples-library/rtm-client-near-pos/setup/schema-registry/GeoLocationsOutputSms.schema.json new file mode 100644 index 0000000..c86f763 --- /dev/null +++ b/scenario-examples-library/rtm-client-near-pos/setup/schema-registry/GeoLocationsOutputSms.schema.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "additionalProperties": false, + "description": "A schema for a topic used to send smses.", + "$schema": "http://json-schema.org/draft-07/schema", + "required": [ + "msisdn", + "content" + ], + "properties": { + "msisdn": { + "type": "string", + "description": "The phone number of the client." + }, + "content": { + "type": "string", + "description": "The content or message to be associated with the email." + } + } +} diff --git a/version b/version new file mode 100644 index 0000000..9e1ae39 --- /dev/null +++ b/version @@ -0,0 +1 @@ +LIBRARY_DOCKER_IMAGE_VERSION=0.1.0-SNAPSHOT \ No newline at end of file