├── .envrc ├── .gitignore ├── services ├── websocket │ ├── .gitignore │ ├── package.json │ └── index.js ├── lib │ ├── .gitignore │ ├── index.js │ ├── package.json │ ├── kafka-logger.js │ ├── kafka-stream.js │ ├── kafka-sink.js │ ├── db.js │ └── db.test.js ├── pets │ ├── .gitignore │ ├── package.json │ ├── index.js │ └── yarn.lock ├── adoptions │ ├── .gitignore │ ├── package.json │ └── index.js ├── kafka │ ├── pets.added.delete.json │ ├── pets.statusChanged.delete.json │ ├── adoptions.requested.delete.json │ ├── adoptions.statusChanged.delete.json │ ├── all-topics.delete.json │ └── docker-compose.yml ├── Dockerfile.pets ├── Dockerfile.adoptions └── Dockerfile.websocket ├── web-ui ├── .envrc ├── .dockerignore ├── src │ ├── react-app-env.d.ts │ ├── utils.ts │ ├── setupTests.ts │ ├── App.test.tsx │ ├── index.css │ ├── reportWebVitals.ts │ ├── query.ts │ ├── pets.ts │ ├── index.tsx │ ├── WebsocketConsole.tsx │ ├── websocket.ts │ ├── UI.tsx │ ├── Adoptions.tsx │ ├── pets.store.ts │ ├── adoptions.store.ts │ ├── App.tsx │ ├── Pets.tsx │ ├── pet.names.ts │ └── pets.name.ts ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── postcss.config.js ├── dev.env ├── tailwind.config.js ├── .gitignore ├── Dockerfile ├── tsconfig.json ├── Caddyfile ├── package.json └── README.md ├── start.sh ├── stop.sh ├── .editorconfig ├── shell.nix ├── .tmuxinator.yml ├── docker-compose.yml ├── .emacs-commands.xml ├── README.md ├── Makefile ├── LICENSE └── ReadyAPI-tests └── Kafka-Petstore-Adding-Pets-readyapi-project.xml /.envrc: -------------------------------------------------------------------------------- 1 | eval "$(lorri direnv)" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.log/ 2 | *.db 3 | -------------------------------------------------------------------------------- /services/websocket/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /web-ui/.envrc: -------------------------------------------------------------------------------- 1 | eval "$(lorri direnv)" 2 | -------------------------------------------------------------------------------- /services/lib/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.db 3 | -------------------------------------------------------------------------------- /services/pets/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | pets.db 3 | -------------------------------------------------------------------------------- /services/adoptions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | pets.db 3 | -------------------------------------------------------------------------------- /web-ui/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | tmuxinator start kafka-dev 4 | -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | tmuxinator stop kafka-dev 4 | -------------------------------------------------------------------------------- /web-ui/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web-ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /web-ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swagger-api/petstore-kafka/HEAD/web-ui/public/favicon.ico -------------------------------------------------------------------------------- /web-ui/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swagger-api/petstore-kafka/HEAD/web-ui/public/logo192.png -------------------------------------------------------------------------------- /web-ui/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swagger-api/petstore-kafka/HEAD/web-ui/public/logo512.png -------------------------------------------------------------------------------- /web-ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /web-ui/dev.env: -------------------------------------------------------------------------------- 1 | CADDY_PORT=4000 2 | PETS_HOST=http://localhost:3100 3 | ADOPTIONS_HOST=http://localhost:3200 4 | WEBSOCKET_HOST=ws://localhost:3300 5 | -------------------------------------------------------------------------------- /web-ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./src/**/*.{js,jsx,ts,tsx}", 4 | ], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | } 10 | -------------------------------------------------------------------------------- /services/kafka/pets.added.delete.json: -------------------------------------------------------------------------------- 1 | { 2 | "partitions": [ 3 | { 4 | "topic": "pets.added", 5 | "partition": 0, 6 | "offset": -1 7 | } 8 | ], 9 | "version": 1 10 | } 11 | -------------------------------------------------------------------------------- /web-ui/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function arrayToObject(arr: object[], key: string = 'id') { 2 | let obj = {} 3 | for(let el of arr) { 4 | // @ts-ignore 5 | obj[el[key]] = el 6 | } 7 | return obj 8 | } 9 | -------------------------------------------------------------------------------- /services/kafka/pets.statusChanged.delete.json: -------------------------------------------------------------------------------- 1 | { 2 | "partitions": [ 3 | { 4 | "topic": "pets.statusChanged", 5 | "partition": 0, 6 | "offset": -1 7 | } 8 | ], 9 | "version": 1 10 | } 11 | -------------------------------------------------------------------------------- /services/kafka/adoptions.requested.delete.json: -------------------------------------------------------------------------------- 1 | { 2 | "partitions": [ 3 | { 4 | "topic": "adoptions.requested", 5 | "partition": 0, 6 | "offset": -1 7 | } 8 | ], 9 | "version": 1 10 | } 11 | -------------------------------------------------------------------------------- /services/kafka/adoptions.statusChanged.delete.json: -------------------------------------------------------------------------------- 1 | { 2 | "partitions": [ 3 | { 4 | "topic": "adoptions.statusChanged", 5 | "partition": 0, 6 | "offset": -1 7 | } 8 | ], 9 | "version": 1 10 | } 11 | -------------------------------------------------------------------------------- /services/lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports.FlatDB = require('./db') 2 | module.exports.KafkaSink = require('./kafka-sink.js') 3 | module.exports.KafkaLogger = require('./kafka-logger.js') 4 | module.exports.KafkaStream = require('./kafka-stream.js') 5 | -------------------------------------------------------------------------------- /web-ui/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /web-ui/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /services/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@types/jest": "^28.1.1", 8 | "flat-file-db": "^1.0.0", 9 | "kafkajs": "^2.0.2" 10 | }, 11 | "scripts": { 12 | "test": "jest", 13 | "test:watch": "jest --watch" 14 | }, 15 | "devDependencies": { 16 | "jest": "^28.1.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | package-lock.json 25 | -------------------------------------------------------------------------------- /web-ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine as static-site 2 | 3 | # Install deps 4 | RUN mkdir -p /build 5 | WORKDIR /build 6 | COPY package.json /build/ 7 | COPY yarn.lock /build/ 8 | RUN yarn 9 | 10 | # Build the SPA 11 | COPY . /build 12 | RUN yarn run build 13 | 14 | FROM caddy:2-alpine 15 | 16 | COPY Caddyfile /etc/caddy/Caddyfile 17 | RUN mkdir -p /srv 18 | COPY --from=static-site /build/build /srv/build 19 | WORKDIR /srv 20 | ENV CADDY_PORT=80 21 | EXPOSE 80 22 | -------------------------------------------------------------------------------- /services/pets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-api-rest", 3 | "scripts": { 4 | "start": "node ./index.js", 5 | "dev": "nodemon ./index.js" 6 | }, 7 | "version": "1.0.0", 8 | "main": "index.js", 9 | "license": "MIT", 10 | "dependencies": { 11 | "body-parser": "^1.20.0", 12 | "cors": "^2.8.5", 13 | "express": "^4.18.1", 14 | "kafkajs": "^2.0.2", 15 | "morgan": "^1.10.0", 16 | "uuid": "^8.3.2" 17 | }, 18 | "devDependencies": { 19 | "nodemon": "^2.0.16" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /services/Dockerfile.pets: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | 3 | RUN mkdir -p /srv 4 | WORKDIR /srv 5 | 6 | # Install before copying over source. Speeds up docker builds 7 | COPY pets/package.json /srv/ 8 | COPY pets/yarn.lock /srv/ 9 | RUN yarn 10 | 11 | # Install deps found in lib 12 | COPY lib/package.json /lib/ 13 | COPY lib/yarn.lock /lib/ 14 | RUN cd /lib && yarn 15 | 16 | # Files 17 | COPY lib /lib 18 | COPY pets /srv 19 | 20 | # Runtime 21 | ENV NODE_PORT=80 22 | EXPOSE 80 23 | CMD node index.js 24 | -------------------------------------------------------------------------------- /web-ui/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 16 | monospace; 17 | } 18 | -------------------------------------------------------------------------------- /web-ui/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /services/Dockerfile.adoptions: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | 3 | RUN mkdir -p /srv 4 | WORKDIR /srv 5 | 6 | # Install before copying over source. Speeds up docker builds 7 | COPY adoptions/package.json /srv/ 8 | COPY adoptions/yarn.lock /srv/ 9 | RUN yarn 10 | 11 | # Install deps found in lib 12 | COPY lib/package.json /lib/ 13 | COPY lib/yarn.lock /lib/ 14 | RUN cd /lib && yarn 15 | 16 | # Files 17 | COPY lib /lib 18 | COPY adoptions /srv 19 | 20 | # Runtime 21 | ENV NODE_PORT=80 22 | EXPOSE 80 23 | CMD node index.js 24 | -------------------------------------------------------------------------------- /services/Dockerfile.websocket: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | 3 | RUN mkdir -p /srv 4 | WORKDIR /srv 5 | 6 | # Install before copying over source. Speeds up docker builds 7 | COPY websocket/package.json /srv/ 8 | COPY websocket/yarn.lock /srv/ 9 | RUN yarn 10 | 11 | # Install deps found in lib 12 | COPY lib/package.json /lib/ 13 | COPY lib/yarn.lock /lib/ 14 | RUN cd /lib && yarn 15 | 16 | # Files 17 | COPY lib /lib 18 | COPY websocket /srv 19 | 20 | # Runtime 21 | ENV NODE_PORT=81 22 | EXPOSE 81 23 | CMD node index.js 24 | -------------------------------------------------------------------------------- /services/websocket/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-api-rest", 3 | "scripts": { 4 | "start": "node ./index.js", 5 | "dev": "nodemon ./index.js" 6 | }, 7 | "version": "1.0.0", 8 | "main": "index.js", 9 | "license": "MIT", 10 | "dependencies": { 11 | "body-parser": "^1.20.0", 12 | "cors": "^2.8.5", 13 | "express": "^4.18.1", 14 | "kafkajs": "^2.0.2", 15 | "morgan": "^1.10.0", 16 | "uuid": "^8.3.2", 17 | "ws": "^8.7.0" 18 | }, 19 | "devDependencies": { 20 | "nodemon": "^2.0.16" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services/adoptions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-api-rest", 3 | "scripts": { 4 | "start": "node ./index.js", 5 | "dev": "nodemon ./index.js" 6 | }, 7 | "version": "1.0.0", 8 | "main": "index.js", 9 | "license": "MIT", 10 | "dependencies": { 11 | "body-parser": "^1.20.0", 12 | "cors": "^2.8.5", 13 | "express": "^4.18.1", 14 | "flat-file-db": "^1.0.0", 15 | "kafkajs": "^2.0.2", 16 | "morgan": "^1.10.0", 17 | "uuid": "^8.3.2" 18 | }, 19 | "devDependencies": { 20 | "nodemon": "^2.0.16" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /web-ui/src/query.ts: -------------------------------------------------------------------------------- 1 | 2 | let queries = new URLSearchParams(window.location.search); 3 | 4 | export const setQuery = (key: string, value: string) => { 5 | if (value.trim() !== '') { 6 | queries.set(key, value+'') 7 | const { protocol, pathname, host } = window.location; 8 | const newUrl = `${protocol}//${host}${pathname}?${queries.toString()}` 9 | window.location.assign(newUrl) 10 | } 11 | } 12 | 13 | export const getQuery = (key: string) => { 14 | let value = queries.get(key) || '' 15 | return value.trim() 16 | } 17 | -------------------------------------------------------------------------------- /services/kafka/all-topics.delete.json: -------------------------------------------------------------------------------- 1 | { 2 | "partitions": [ 3 | { 4 | "topic": "pets.added", 5 | "partition": 0, 6 | "offset": -1 7 | }, 8 | { 9 | "topic": "pets.statusChanged", 10 | "partition": 0, 11 | "offset": -1 12 | }, 13 | { 14 | "topic": "adoptions.requested", 15 | "partition": 0, 16 | "offset": -1 17 | }, 18 | { 19 | "topic": "adoptions.statusChanged", 20 | "partition": 0, 21 | "offset": -1 22 | } 23 | ], 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /web-ui/src/pets.ts: -------------------------------------------------------------------------------- 1 | import create from 'zustand' 2 | import uuid from 'uuid' 3 | 4 | 5 | interface Pet { 6 | name: string; 7 | status: 'available' | 'onhold' | 'adopted'; 8 | } 9 | 10 | interface State { 11 | pets: { 12 | [id: string]: Pet; 13 | } 14 | } 15 | 16 | const useStore = create(set => ({ 17 | pets: {}, 18 | addPet: (newPet: {name: string}) => set(state => { 19 | const id = uuid.v4() 20 | const pet: Pet = { 21 | name: newPet.name, 22 | status: 'available', 23 | } 24 | state.pets[id] = pet 25 | return state 26 | }), 27 | })) 28 | -------------------------------------------------------------------------------- /web-ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /web-ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /web-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | pkgs.mkShell { 4 | buildInputs = with pkgs; [ 5 | docker ## https://docs.docker.com/get-docker/ 6 | docker-compose ## https://docs.docker.com/compose/install/ 7 | 8 | # Nodejs / web-ui 9 | nodejs-16_x ## https://nodejs.org/en/download/ 10 | yarn ## https://yarnpkg.com/getting-started/install 11 | caddy ## https://caddyserver.com/docs/install 12 | 13 | # Optional for dev 14 | tmuxinator ## (optional) https://github.com/tmuxinator/tmuxinator 15 | tmux ## (optional) https://github.com/tmux/tmux/wiki/Installing 16 | apacheKafka ## (optional) https://kafka.apache.org/downloads 17 | ]; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /web-ui/Caddyfile: -------------------------------------------------------------------------------- 1 | :{$CADDY_PORT} { 2 | @pets path /api/pets /api/pets/* 3 | @adoptions path /api/adoptions /api/adoptions/* 4 | @websocket path /websocket 5 | 6 | uri strip_suffix / 7 | 8 | # Pets service 9 | handle @pets { 10 | reverse_proxy {$PETS_HOST} 11 | } 12 | 13 | # Adoptions service 14 | handle @adoptions { 15 | reverse_proxy {$ADOPTIONS_HOST} 16 | } 17 | 18 | # Websocket 19 | handle @websocket { 20 | reverse_proxy {$WEBSOCKET_HOST} 21 | } 22 | 23 | # SPA 24 | handle { 25 | try_files {path} / 26 | header /img/* Cache-Control max-age=31536000 27 | header /js/* Cache-Control max-age=31536000 28 | header /css/* Cache-Control max-age=31536000 29 | header /fonts/* Cache-Control max-age=31536000 30 | encode gzip 31 | root * ./build 32 | file_server 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /services/kafka/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | zookeeper: 5 | image: docker.io/bitnami/zookeeper:3.8 6 | ports: 7 | - "2181:2181" 8 | volumes: 9 | - "zookeeper_data:/bitnami" 10 | environment: 11 | - ALLOW_ANONYMOUS_LOGIN=yes 12 | kafka: 13 | image: docker.io/bitnami/kafka:3.2 14 | ports: 15 | - "9092:9092" 16 | volumes: 17 | - "kafka_data:/bitnami" 18 | environment: 19 | - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 20 | - ALLOW_PLAINTEXT_LISTENER=yes 21 | hostname: kafka.local 22 | depends_on: 23 | - zookeeper 24 | cmak: 25 | image: ghcr.io/eshepelyuk/dckr/cmak-3.0.0.5:latest 26 | restart: always 27 | ports: 28 | - "9000:9000" 29 | environment: 30 | ZK_HOSTS: "zookeeper:2181" 31 | depends_on: 32 | - zookeeper 33 | - kafka 34 | 35 | volumes: 36 | zookeeper_data: 37 | driver: local 38 | kafka_data: 39 | driver: local 40 | -------------------------------------------------------------------------------- /web-ui/src/WebsocketConsole.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | export default function WebsocketConsole({ location, websocketUrl }: { location: string; websocketUrl: string }) { 4 | const [readyState, setReadyState] = useState('') 5 | const [logs, setLogs] = useState([]) 6 | 7 | useEffect(() => { 8 | let websocket = new WebSocket(websocketUrl) 9 | setReadyState('Connecting') 10 | websocket.addEventListener('message', (msg) => { 11 | const log = msg.data 12 | 13 | setLogs(logs => ([...logs, log])) 14 | }) 15 | 16 | websocket.addEventListener('open', () => { 17 | setReadyState('Open') 18 | websocket.send(JSON.stringify({ location })) 19 | }) 20 | 21 | websocket.addEventListener('error', (err) => { 22 | setReadyState('Error') 23 | console.error(err) 24 | }) 25 | return () => websocket.close() 26 | }, [location, setLogs, setReadyState]) 27 | 28 | return ( 29 |
30 |

Console

31 | {readyState} 32 |
{logs.map((log, i) => (
33 |         {log}
34 |       ))}
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /services/lib/kafka-logger.js: -------------------------------------------------------------------------------- 1 | const { logLevel } = require('kafkajs') 2 | module.exports = function KafkaLogger() { 3 | return ({ namespace, level, label, log }) => { 4 | const { message, groupId, timestamp, extra=[] } = log 5 | let msgs = extra.filter(Boolean).map(e => typeof e === 'string' ? e : JSON.stringify(e)) 6 | module.exports.log({prefix: 'K', timestamp, level, msgs: [groupId, namespace, ...msgs, message]}) 7 | } 8 | } 9 | 10 | module.exports.log = function log({level, prefix=' ', timestamp=new Date(), msgs=[]}) { 11 | let levelStr = typeof level === 'number' ? kafkaLevel(level) : level 12 | let msgStr = msgs.filter(a => a).join(' - ') 13 | consoleFn(level, `[${prefix}][${levelStr}] ${timestamp} - ${msgStr}`) 14 | } 15 | 16 | function consoleFn(level, ...args) { 17 | switch(level) { 18 | case logLevel.ERROR: 19 | case logLevel.NOTHING: 20 | return console.error(...args) 21 | case logLevel.WARN: 22 | return console.warn(...args) 23 | case logLevel.INFO: 24 | return console.info(...args) 25 | case logLevel.DEBUG: 26 | return console.debug(...args) 27 | } 28 | } 29 | 30 | function kafkaLevel(level) { 31 | switch(level) { 32 | case logLevel.ERROR: 33 | case logLevel.NOTHING: 34 | return 'ERROR' 35 | case logLevel.WARN: 36 | return 'WARN' 37 | case logLevel.INFO: 38 | return 'I' 39 | case logLevel.DEBUG: 40 | return 'D' 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /web-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.4", 7 | "@testing-library/react": "^13.2.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.5.1", 10 | "@types/node": "^16.11.36", 11 | "@types/react": "^18.0.9", 12 | "@types/react-dom": "^18.0.5", 13 | "eventemitter3": "^4.0.7", 14 | "react": "^18.1.0", 15 | "react-dom": "^18.1.0", 16 | "react-scripts": "5.0.1", 17 | "typescript": "^4.7.2", 18 | "web-vitals": "^2.1.4" 19 | }, 20 | "scripts": { 21 | "gateway": "caddy run --envfile dev.env", 22 | "start": "REACT_APP_WEBSOCKET_HOST=ws://localhost:4000/websocket react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | }, 45 | "proxy": "http://localhost:4000", 46 | "devDependencies": { 47 | "@heroicons/react": "^1.0.6", 48 | "@types/uuid": "^8.3.4", 49 | "autoprefixer": "^10.4.7", 50 | "postcss": "^8.4.14", 51 | "react-query": "^3.39.1", 52 | "tailwindcss": "^3.0.24", 53 | "uuid": "^8.3.2", 54 | "zustand": "^4.0.0-rc.1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /services/lib/kafka-stream.js: -------------------------------------------------------------------------------- 1 | // Kafka will manage the offsets and commiting them. 2 | // This is useful for handlers that want to produce messages as a result of consuming some. 3 | // We don't want to manage the offsets ourselves in that case because the source of truth is the Topic 4 | // not the local disk. 5 | const defaultOnLog = module.exports = class KafkaStream { 6 | 7 | constructor({name, topics, onLog, kafka }) { 8 | this.kafka = kafka 9 | this.topics = topics 10 | this.onLog = onLog || (() => {}) 11 | this.name = name 12 | this.consumer = this.kafka.consumer({ groupId: this.name }) 13 | this.subscribe() 14 | } 15 | 16 | async processLog({ topic, partition, message }) { 17 | const log = JSON.parse(message.value.toString()) 18 | await this.onLog({ 19 | log, 20 | topic, partition, message, 21 | }) 22 | } 23 | 24 | info(...msgs) { 25 | this.consumer.logger().info(null, {extra: [this.name, ...msgs]}) 26 | } 27 | 28 | error(...msgs) { 29 | this.consumer.logger().error(null, {extra: [this.name, ...msgs]}) 30 | } 31 | 32 | async subscribe() { 33 | try { 34 | await this.consumer.connect() 35 | await this.consumer.subscribe({ topics: this.topics, fromBeginning: false }) 36 | this.consumer.run({ 37 | autoCommit: true, // We are going to leave Kafka to manage offsets 38 | eachMessage: async ({ topic, partition, message }) => { 39 | await this.processLog({ topic, partition, message }) 40 | }, 41 | }).then(() => this.info(`Consumer initialized`)) 42 | } catch(e) { 43 | this.error(`${e.message}`, ['Retrying', e.stack]) 44 | setTimeout(() => this.subscribe(), 1500) // retry 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.tmuxinator.yml: -------------------------------------------------------------------------------- 1 | # Optional tmux socket 2 | # socket_name: foo 3 | 4 | # Note that the pre and post options have been deprecated and will be replaced by 5 | # project hooks. 6 | 7 | # Project hooks 8 | 9 | # Runs on project start, always 10 | # on_project_start: command 11 | 12 | # Run on project start, the first time 13 | # on_project_first_start: command 14 | 15 | # Run on project start, after the first time 16 | # on_project_restart: command 17 | 18 | # Run on project exit ( detaching from tmux session ) 19 | # on_project_exit: command 20 | 21 | # Run on project stop 22 | # on_project_stop: command 23 | 24 | # Runs in each window and pane before window/pane specific commands. Useful for setting up interpreter versions. 25 | # pre_window: rbenv shell 2.0.0-p247 26 | 27 | # Pass command line options to tmux. Useful for specifying a different tmux.conf. 28 | # tmux_options: -f ~/.tmux.mac.conf 29 | 30 | # Change the command to call tmux. This can be used by derivatives/wrappers like byobu. 31 | # tmux_command: byobu 32 | 33 | # Specifies (by name or index) which window will be selected on project startup. If not set, the first window is used. 34 | # startup_window: editor 35 | 36 | # Specifies (by index) which pane of the specified window will be selected on project startup. If not set, the first pane is used. 37 | # startup_pane: 1 38 | 39 | # Controls whether the tmux session should be attached to automatically. Defaults to true. 40 | # attach: false 41 | 42 | project_name: petstore-kafka 43 | 44 | windows: 45 | - servers: 46 | layout: even-vertical 47 | panes: 48 | - pets: make dev-pets 49 | - adoptions: make dev-adoptions 50 | - websocket: make dev-websocket 51 | - infra: 52 | layout: even-vertical 53 | panes: 54 | - kafka: make dev-kafka 55 | - gateway: make dev-gateway 56 | - web-ui: make dev-web-ui 57 | -------------------------------------------------------------------------------- /web-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /web-ui/src/websocket.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3' 2 | 3 | const CONNECTING = 0 4 | const OPEN = 1 5 | const CLOSING = 2 6 | const CLOSED = 3 7 | 8 | let websocket: WebSocket | null = null 9 | let ee = new EventEmitter() 10 | 11 | export function disconnect() { 12 | if(websocket) { 13 | console.log('Disconnecting websocket') 14 | if(websocket.readyState < CLOSING) 15 | websocket.close() 16 | websocket = null 17 | } 18 | } 19 | 20 | export function connect({ location, url }: { location: string; url: string}): () => void { 21 | disconnect() 22 | console.log("Connecting to WebSocket with location = " + location) 23 | 24 | websocket = new WebSocket(url) 25 | websocket.addEventListener('open', function(...args: any[]) { 26 | this.send(JSON.stringify({ type: 'handshake.request', location })) 27 | }) 28 | 29 | websocket.addEventListener('message', function(event: MessageEvent) { 30 | ee.emit('message', event) 31 | }) 32 | 33 | return disconnect 34 | } 35 | 36 | const idToListener: { 37 | [key: string]: (...args: any[]) => void; 38 | } = {} 39 | export function onMessage(id: string, cb: (json: any, websocket: WebSocket) => void) { 40 | if(idToListener[id]) { 41 | ee.removeListener('message', idToListener[id]) 42 | } 43 | 44 | idToListener[id] = function (msg: any) { 45 | try { 46 | if(websocket) 47 | cb(JSON.parse(msg.data), websocket) 48 | } catch(e) { 49 | console.error(e) 50 | } 51 | } 52 | ee.addListener('message', idToListener[id]) 53 | } 54 | 55 | export function onError(cb: (websocket: WebSocket) => void) { 56 | ee.addListener('error', function (msg: any) { 57 | try { 58 | console.error('Websocket Error: ' + msg) 59 | if(websocket) 60 | cb(websocket) 61 | } catch(e) { 62 | console.error(e) 63 | } 64 | }) 65 | } 66 | 67 | 68 | export function onOpen(cb: (websocket: WebSocket) => void) { 69 | ee.addListener('open', function () { 70 | try { 71 | if(websocket) 72 | cb(websocket) 73 | } catch(e) { 74 | console.error(e) 75 | } 76 | }) 77 | } 78 | 79 | -------------------------------------------------------------------------------- /web-ui/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /web-ui/src/UI.tsx: -------------------------------------------------------------------------------- 1 | import {ButtonHTMLAttributes, HTMLProps } from 'react' 2 | 3 | export type COLORS = { 4 | red: string; 5 | blue: string; 6 | orange: string; 7 | yellow: string; 8 | lime: string; 9 | green: string; 10 | } 11 | 12 | export type SIZES = { 13 | xs: string; 14 | md: string; 15 | lg: string; 16 | } 17 | 18 | const PILL_COLORS: COLORS = { 19 | 'red': 'bg-red-100 text-red-800', 20 | 'blue': 'bg-blue-100 text-blue-800', 21 | 'orange': 'bg-orange-100 text-orange-800', 22 | 'yellow': 'bg-yellow-100 text-yellow-800', 23 | 'lime': 'bg-lime-100 text-lime-800', 24 | 'green': 'bg-green-100 text-green-800', 25 | } 26 | 27 | export function Pill( 28 | { color, className='', ...props }: 29 | { color?: keyof COLORS; } & HTMLProps) { 30 | let colorClasses = color ? PILL_COLORS[color] : PILL_COLORS['blue'] 31 | 32 | return ( 33 | 34 | ) 35 | } 36 | 37 | interface IButton extends ButtonHTMLAttributes { 38 | color?: keyof COLORS; 39 | size?: keyof SIZES; 40 | } 41 | 42 | const BUTTON_SIZES: SIZES = { 43 | xs: 'text-xs', 44 | md: 'text-md', 45 | lg: 'text-lg', 46 | } 47 | 48 | const BUTTON_COLORS: COLORS = { 49 | blue: 'bg-blue-600 hover:bg-blue-700 focus:bg-blue-700 active:bg-blue-800', 50 | red: 'bg-red-500 hover:bg-red-600 focus:bg-red-600 active:bg-red-700', 51 | orange: 'bg-orange-500 hover:bg-orange-600 focus:bg-orange-600 active:bg-orange-700', 52 | yellow: 'bg-yellow-500 hover:bg-yellow-600 focus:bg-yellow-600 active:bg-yellow-700', 53 | lime: 'bg-lime-500 hover:bg-lime-600 focus:bg-lime-600 active:bg-lime-700 hover:text-black text-gray-800', 54 | green: 'bg-green-500 hover:bg-green-600 focus:bg-green-600 active:bg-green-700', 55 | } 56 | 57 | export const Button = ({ className='', color='blue', size='xs', ...props}: IButton ) => { 58 | const colorClasses = BUTTON_COLORS[color] 59 | const sizeClasses = BUTTON_SIZES[size] 60 | return ( 61 |
62 |
64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /web-ui/src/Adoptions.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo } from 'react' 2 | import useAdoptionsStore from './adoptions.store' 3 | import usePetsStore from './pets.store' 4 | import { Button } from './UI' 5 | import { Pill } from './UI' 6 | import { getQuery } from './query' 7 | 8 | export default function Adoptions() { 9 | const location = getQuery('location') 10 | 11 | const adoptions = Object.values(useAdoptionsStore(s => s.adoptions)) 12 | adoptions.reverse() 13 | const fetchAdoptions = useAdoptionsStore(s => s.fetchAdoptions) 14 | const changeStatus = useAdoptionsStore(s => s.changeStatus) 15 | 16 | useEffect(() => { 17 | fetchAdoptions({ location, status: '!approved&!denied&!rejected'}) 18 | }, [location]) 19 | 20 | // Use local cache of pets to replace names. 21 | const pets = usePetsStore(s => s.pets) 22 | 23 | return ( 24 |
25 | {adoptions.map(a => ( 26 |
27 |

28 | Adoption: #{a.id} 29 |

30 | Status: {a.status} 31 | 32 |
33 | {a.pets.map(id => { 34 | const reason = a.reasons?.find(r => r.petId === id) 35 | return ( 36 |
37 | {(pets[id] || {}).name} 38 | {reason ? ( 39 | {reason.message} 40 | ) : null } 41 |
42 | ) 43 | })} 44 |
45 | {a.reasons?.length ? ( 46 |
47 | {a.reasons.length ? ( 48 |
Some of the pets could not be adopted
49 | ) : null} 50 |
51 | ) : null} 52 |
53 | 54 | 55 |
56 |
57 | 58 | ))} 59 | 60 |
61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /web-ui/src/pets.store.ts: -------------------------------------------------------------------------------- 1 | import create from 'zustand' 2 | import { onMessage } from './websocket' 3 | import {arrayToObject} from './utils' 4 | 5 | interface Pet { 6 | id: string; 7 | name: string; 8 | location?: string; 9 | status: 'pending' | 'available' | 'onhold' | 'adopted'; 10 | } 11 | 12 | function newPet(pet: Partial): Pet { 13 | return Object.assign({ 14 | id: '', 15 | name: '', 16 | status: 'pending' 17 | }, pet) 18 | } 19 | 20 | export class PetsAPI { 21 | url: string = ''; 22 | 23 | constructor(url: string) { 24 | this.url = url 25 | } 26 | 27 | getPets = async ({location,status}: {location?: string; status?: string;}) => { 28 | let queries = new URLSearchParams(); 29 | if(location) 30 | queries.set('location', location) 31 | if(status) 32 | queries.set('status', status) 33 | 34 | return fetch(`${this.url}/pets?${queries}`).then(res => res.json()).then((data) => { 35 | return data as Pet[] 36 | }) 37 | } 38 | 39 | addPet = ({name, location}: {name: string; location: string;}) => { 40 | return fetch(`${this.url}/pets`, { 41 | method: 'POST', 42 | headers: { 43 | 'Content-Type': 'application/json' 44 | }, 45 | body: JSON.stringify({ name, location }), 46 | }) 47 | } 48 | } 49 | 50 | const api = new PetsAPI('/api') 51 | 52 | const useStore = create<{ 53 | petError?: string; 54 | pets: { 55 | [key:string]: Pet 56 | }; 57 | addPet(newPet: {name: string; location: string;}): void; 58 | fetchPets({location, status}: {location: string; status: string;}): void; 59 | }>((set) => ({ 60 | pets: {}, 61 | addPet: async ({ name, location }) => { 62 | try { 63 | await api.addPet({ name, location }) 64 | set(() => ({ petError: '' })) 65 | } catch(e) { 66 | set(() => ({ petError: e+'' })) 67 | } 68 | }, 69 | fetchPets: async ({location, status}) => { 70 | try { 71 | const pets = await api.getPets({location, status}) 72 | set(() => ({ petError: '', pets: arrayToObject(pets, 'id') })) 73 | } catch (e) { 74 | set(() => ({ petError: e+'' })) 75 | } 76 | } 77 | })) 78 | 79 | // WebSocket connection 80 | onMessage('pets.store', (json: any) => { 81 | if(json.type === 'kafka' && json.topic.startsWith('pets.')) { 82 | const pet: Pet = json.log 83 | const id = pet.id 84 | useStore.setState(state => { 85 | const oldPet = (state.pets[id]) 86 | return { 87 | pets: { 88 | ...state.pets, 89 | [id]: {...oldPet, ...pet} 90 | } 91 | } 92 | }) 93 | } 94 | }) 95 | 96 | 97 | export default useStore 98 | 99 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | ## NOTE NOTE! 4 | # Because kafka needs to know it's own hostname, you either need to change it in this document (from kafka.local) 5 | ## Or add kafka.local to your /etc/hosts file (and point it at 127.0.0.1). 6 | 7 | services: 8 | 9 | # Main frontend 10 | # Expose port with `- {PORT}:80` (i.e., your port goes on the left). 11 | web-ui: 12 | image: ponelat/petstore-kafka-web-ui:latest 13 | restart: always 14 | ports: 15 | - "80:80" 16 | environment: 17 | CADDY_PORT: 80 18 | PETS_HOST: "http://pets:80" 19 | ADOPTIONS_HOST: "http://adoptions:80" 20 | WEBSOCKET_HOST: "http://websocket:81" 21 | 22 | # Secondary frontend, a CMAK app for looking at the Kafka cluster 23 | # Note: You'll need to create a Cluster via its UI. Be sure to add 'zookeeper:2181' as the zookeeper host. 24 | cmak: 25 | image: ghcr.io/eshepelyuk/dckr/cmak-3.0.0.5:latest 26 | restart: always 27 | ports: 28 | - "9000:9000" 29 | environment: 30 | ZK_HOSTS: "zookeeper:2181" 31 | depends_on: 32 | - zookeeper 33 | - kafka 34 | 35 | # Application Services 36 | pets: 37 | image: ponelat/petstore-kafka-pets:latest 38 | restart: always 39 | environment: 40 | KAFKA_HOSTS: "kafka:9092" 41 | DATA_BASEPATH: "/data" 42 | volumes: 43 | - "pets_data:/data" 44 | 45 | adoptions: 46 | image: ponelat/petstore-kafka-adoptions:latest 47 | restart: always 48 | environment: 49 | KAFKA_HOSTS: "kafka:9092" 50 | DATA_BASEPATH: "/data" 51 | volumes: 52 | - "adoptions_data:/data" 53 | 54 | websocket: 55 | image: ponelat/petstore-kafka-websocket:latest 56 | restart: always 57 | environment: 58 | KAFKA_HOSTS: "kafka:9092" 59 | DATA_BASEPATH: "/data" 60 | volumes: 61 | - "websocket_data:/data" 62 | 63 | 64 | # Kafka services 65 | zookeeper: 66 | image: docker.io/bitnami/zookeeper:3.8 67 | volumes: 68 | - "zookeeper_data:/bitnami" 69 | environment: 70 | - ALLOW_ANONYMOUS_LOGIN=yes 71 | kafka: 72 | image: docker.io/bitnami/kafka:3.2 73 | volumes: 74 | - "kafka_data:/bitnami" 75 | ports: 76 | - "9092:9092" 77 | environment: 78 | - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 79 | - ALLOW_PLAINTEXT_LISTENER=yes 80 | hostname: kafka.local 81 | depends_on: 82 | - zookeeper 83 | 84 | volumes: 85 | zookeeper_data: 86 | driver: local 87 | kafka_data: 88 | driver: local 89 | pets_data: 90 | driver: local 91 | adoptions_data: 92 | driver: local 93 | websocket_data: 94 | driver: local 95 | -------------------------------------------------------------------------------- /services/lib/kafka-sink.js: -------------------------------------------------------------------------------- 1 | const { FlatDB } = require('./index') 2 | const path = require('path') 3 | 4 | const defaultOnLog = ({log, sink, id}) => { 5 | if(!id) 6 | return 7 | return sink.db.dbPut(id, log) 8 | } 9 | 10 | module.exports = class KafkaSink { 11 | constructor({name, topics, onLog, kafka, makeIdFn, basePath=__dirname }) { 12 | this.kafka = kafka 13 | this.makeIdFn = makeIdFn || ((log) => log.id) // Default discriminator 14 | this.topics = topics 15 | this.onLog = onLog || defaultOnLog // Persist it by default 16 | this.name = name 17 | this.admin = this.kafka.admin() 18 | this.consumer = this.kafka.consumer({ groupId: this.name }) 19 | 20 | this.db = new FlatDB(path.resolve(basePath, `./${name}.db`)) 21 | this.subscribe() 22 | } 23 | 24 | async processLog({ topic, partition, message }) { 25 | const log = JSON.parse(message.value.toString()) 26 | const id = this.makeIdFn(log) 27 | await this.onLog({ 28 | log, 29 | sink: this, 30 | id, 31 | topic, partition, message, 32 | }) 33 | // Note: This will still an out-of-bound offset, which may not exist on next start up. 34 | // Ensure that fromBeginning = false, and try to seek to low water mark ourselves. 35 | this.db.dbPutMeta(`${topic}-partition-${partition}-offset`, message.offset + 1) 36 | } 37 | 38 | async seek() { 39 | return Promise.all(this.topics.map(async (topic) => { 40 | const offset = this.db.dbGetMeta(`${topic}-partition-0-offset`) 41 | if(offset) { 42 | this.info(`Cache exists, using ${offset} for ${topic}.partition-0`) 43 | return this.consumer.seek({ topic, partition: 0, offset: +offset }) 44 | } 45 | this.info(`Cache does not exist for ${topic}.partition-0. Seeking to beginning...`) 46 | const partitions = await this.admin.fetchTopicOffsets(topic) 47 | const low = (partitions.find(p => p.partition === 0) || {low: 0}).low 48 | this.info(`Low is ${low}. Offsets for ${topic} are ${JSON.stringify(partitions)}`) 49 | return this.consumer.seek({ topic, partition: 0, offset: +low }) 50 | })) 51 | } 52 | 53 | get(id, defaultValue={}) { 54 | const val = this.db.dbGet(id) 55 | return typeof val === 'undefined' ? defaultValue : val 56 | } 57 | 58 | info(...msgs) { 59 | this.consumer.logger().info(null, {extra: [this.name, ...msgs]}) 60 | } 61 | 62 | error(...msgs) { 63 | this.consumer.logger().error(null, {extra: [this.name, ...msgs]}) 64 | } 65 | 66 | async subscribe() { 67 | try { 68 | await this.consumer.connect() 69 | await this.consumer.subscribe({ topics: this.topics, fromBeginning: false }) 70 | this.consumer.run({ 71 | autoCommit: false, 72 | eachMessage: async ({ topic, partition, message }) => { 73 | await this.processLog({ topic, partition, message }) 74 | }, 75 | }).then(() => this.info(`Consumer initialized`)) 76 | await this.seek() 77 | } catch(e) { 78 | this.error(`${e.message}`, ['Retrying', e.stack]) 79 | setTimeout(() => this.subscribe(), 1500) // retry 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /.emacs-commands.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | nix-shell --run "docker-compose up" 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | nix-shell --run "kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic $TOPIC --from-beginning" 14 | 15 | 16 | 17 | pets.added 18 | pets.statusChanged 19 | adoptions.requested 20 | adoptions.statusChanged 21 | __consumer_offsets 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | nix-shell --run "echo '{\"name\": \"$NAME\"}' | kafka-console-producer.sh --bootstrap-server localhost:9092 --topic $TOPIC" 32 | 33 | 34 | 35 | 36 | 37 | 38 | pets.added 39 | pets.statusChanged 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | nix-shell --run "kafka-delete-records.sh --bootstrap-server localhost:9092 --offset-json-file $TOPIC_FILE" 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | nix-shell --run "yarn start" 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | nix-shell --run "yarn run dev" 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | nix-shell --run "yarn run dev" 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | nix-shell --run "yarn run dev" 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | nix-shell --run "make dev" 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | nix-shell --run "./stop.sh" 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /web-ui/src/adoptions.store.ts: -------------------------------------------------------------------------------- 1 | import create from 'zustand' 2 | import { onMessage } from './websocket' 3 | import {arrayToObject} from './utils' 4 | 5 | interface AdoptionReason { 6 | petId: string; 7 | message: string; 8 | } 9 | interface Adoption { 10 | id: string; 11 | status: 'requested' | 'pending' | 'available' | 'denied' | 'approved'; 12 | pets: string[]; 13 | reasons?: AdoptionReason[]; 14 | } 15 | 16 | const baseAdoption: Adoption = { 17 | id: '', 18 | status: 'requested', 19 | pets: [], 20 | } 21 | 22 | export class AdoptionsAPI { 23 | url: string = ''; 24 | 25 | constructor(url: string) { 26 | this.url = url 27 | } 28 | 29 | getAdoptions = async ({ location, status }: { location: string; status: string; }) => { 30 | let queries = new URLSearchParams(); 31 | if(location) 32 | queries.set('location', location) 33 | if(status) 34 | queries.set('status', status) 35 | 36 | return fetch(`${this.url}/adoptions?${queries}`).then(res => res.json()).then((data) => { 37 | return data as Adoption[] 38 | }) 39 | } 40 | 41 | changeStatus = async ({status, id}: {status: string; id: string }) => { 42 | return fetch(`${this.url}/adoptions/${id}`,{ 43 | method: 'PATCH', 44 | headers: { 45 | 'Content-Type': 'application/json' 46 | }, 47 | body: JSON.stringify({status}) 48 | }) 49 | } 50 | 51 | requestAdoption = ({pets, location}: {pets: string[]; location: string;}) => { 52 | return fetch(`${this.url}/adoptions`, { 53 | method: 'POST', 54 | headers: { 55 | 'Content-Type': 'application/json' 56 | }, 57 | body: JSON.stringify({ pets, location }), 58 | }) 59 | } 60 | 61 | } 62 | 63 | const api = new AdoptionsAPI('/api') 64 | 65 | const useStore = create<{ 66 | adoptions: { 67 | [key: string]: Adoption 68 | }; 69 | requestAdoptions({pets,location}: {pets: string[]; location: string;}): void; 70 | fetchAdoptions({location, status}: {location: string; status: string;}): void; 71 | changeStatus({status, id}: {id: string; status: string;}): void; 72 | }>((set) => ({ 73 | adoptions: {}, 74 | fetchAdoptions: async ({ location, status }) => { 75 | try { 76 | const adoptions = await api.getAdoptions({location, status}) 77 | set(() => ({ adoptions: arrayToObject(adoptions, 'id') })) 78 | } catch (e) { 79 | console.error(e) 80 | // TODO 81 | } 82 | }, 83 | changeStatus: async ({ id, status }) => { 84 | try { 85 | const adoptions = await api.changeStatus({status, id}) 86 | } catch (e) { 87 | console.error(e) 88 | // TODO 89 | } 90 | }, 91 | requestAdoptions: async ({ pets, location }) => { 92 | try { 93 | await api.requestAdoption({pets, location}) 94 | } catch (e) { 95 | console.error(e) 96 | // TODO 97 | } 98 | } 99 | })) 100 | 101 | // WebSocket connection 102 | onMessage('adoptions.store', (json: any, websocket: WebSocket) => { 103 | if(json.type === 'kafka' && json.topic.startsWith('adoptions.')) { 104 | useStore.setState(state => { 105 | const adoption: Adoption = json.log 106 | const oldAdoption = state.adoptions[adoption.id] || {} 107 | const newAdoption = {...baseAdoption, ...oldAdoption, ...adoption} 108 | 109 | return { 110 | adoptions: { 111 | ...state.adoptions, 112 | [adoption.id]: newAdoption, 113 | } 114 | } 115 | }) 116 | } 117 | }) 118 | 119 | 120 | export default useStore 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Petstore Kafka demo site 2 | 3 | This is a contrived demo of a petstore with adoptions. 4 | It is build on Kafka topics written in Nodejs. 5 | 6 | It exists as a functional example for describing with AsyncAPI and OpenAPI. 7 | 8 | You can see a live demo of this running at https://petstore-kafka.swagger.io 9 | 10 | > NOTE: Given the way Kafka rebalances and the need for cacheing, it may take a while (a few minutes) for caches to be built, depending on the size of the Topic. 11 | 12 | 13 | ## User Flows 14 | 15 | ![image](https://user-images.githubusercontent.com/8438485/228814936-378faa65-809f-412d-95a2-ee59c6bad7f1.png) 16 | 17 | 18 | ## Technical stack 19 | 20 | ![image](https://user-images.githubusercontent.com/8438485/228814110-04ec68e6-4e2e-4d91-9977-d243e2b55a59.png) 21 | 22 | - There is a browser app (SPA) written in Typescript using Create-React-APP, it's found under [[./web-ui]]. 23 | - There is a gateway for serving the SPA and proxying API calls to the other services. It is also under [[./web-ui]] and is a single Caddyfile. 24 | - There is a Pets service (nodejs + kafka sink) [[./services/pets]]. 25 | - There is a Adoptions service (nodejs + kafka sink) [[./services/adoptions]]. 26 | - There is a Websocket service (nodejs + kafka sink + publishes events) [[./services/websocket]]. 27 | - There is a docker-compose file just for Kafka services [[./services/kafka/docker-compose.yml]]. 28 | - There is a docker-compose file for the whole stack [[./docker-compose.yml]]. 29 | 30 | 31 | ## OpenAPI and AsyncAPI 32 | 33 | - OpenAPI for **Gateway** (includes both Pets and Adoptiosn services) https://app.swaggerhub.com/apis/SwaggerPMTests/Pets-Adoption-API 34 | - AsyncAPI for **Pets** service https://app.swaggerhub.com/apis/SwaggerPMTests/petstore-kafka-pets 35 | - AsyncAPI for **Adoptions** service https://app.swaggerhub.com/apis/SwaggerPMTests/petstore-kafka-adoptions 36 | 37 | 38 | ## Developing 39 | 40 | To develop, first make sure you have all the dependencies installed. 41 | 42 | > WARNING: Kafka needs to know its own hostname. Add an entry to your hosts file mapping kafka.local -> 127.0.0.1. Or tweak the docker-compose file to adjust. 43 | 44 | A list can be found by running `make dependencies`. There are optional dependencie for Tmux/inator, which are helpful for spinning up several servers at once each with their own terminal pane. But that does require a little bit of tmux knowledge. 45 | 46 | You can launch each service with `make dev-{service}` where `{service}` is the name of the service. A list can be seen by running `make help`. 47 | 48 | Alternatively, if you have Tmuxinator install you can run `make dev` which will launch all services. 49 | 50 | All services have hot-reload, but they make require a restart now and then if there are kafka delays/issues. 51 | 52 | ## Building 53 | 54 | All services have a Docker image and the entire stack can be brought up with `docker-compose up` if the images are built and/or already hosted in hub.docker.com. 55 | 56 | To build all images, run `make build` which will build them one-by-one. All builds happen within the Docker context so it isn't required to run `yarn run build` beforehand. 57 | 58 | Alternatively you can build the individual docker images with `make build-{service}` where `{service}` is the service. For a full list see `make help`. 59 | 60 | ## Testing 61 | 62 | Test projects for this application have been added to the [ReadyAPI-tests](./ReadyAPI-tests) folder. The projects have test across the OpenAPI and AsyncAPIs as described in the [API Section](#openapi-and-asyncapi). In order to run these tests you need to have ReadyAPI installed. A free-trial can be obtained via https://www.soapui.org/downloads/download-readyapi-trial/ 63 | 64 | -------------------------------------------------------------------------------- /web-ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import {useState, useEffect} from 'react'; 2 | import Pets from './Pets' 3 | import Adoptions from './Adoptions' 4 | import useAdoptionsStore from './adoptions.store' 5 | import { Button } from './UI' 6 | import {setQuery, getQuery} from './query' 7 | import WebsocketConsole from './WebsocketConsole' 8 | import usePetsStore from './pets.store' 9 | import {connect as wsConnect, onMessage } from './websocket' 10 | 11 | const CONFIGS = { 12 | // Issues with websocket + cra proxy + caddy. Requires me to add this env variable to override it more naturally. 13 | websocketUrl: process.env.REACT_APP_WEBSOCKET_HOST || relativeWebsocketUrl('/websocket'), 14 | petsUrl: '/api', 15 | adoptionsUrl: '/api', 16 | } 17 | 18 | const newTab = () => { 19 | window.open(window.location.href, '_blank') 20 | } 21 | 22 | function App() { 23 | const location = getQuery('location') 24 | const adoptions = Object.values(useAdoptionsStore(s => s.adoptions)) 25 | const [locationInput, setLocationInput] = useState(location) 26 | const [websocketMsgCount, setWebsocketMsgCount] = useState(0) 27 | 28 | useEffect(() => { 29 | if (!location) { 30 | // Reloads the page 31 | setQuery('location', 'Plett') 32 | return 33 | } 34 | 35 | onMessage('app', () => { 36 | setWebsocketMsgCount(s => s+1) 37 | }) 38 | 39 | return wsConnect({ 40 | location, 41 | url: CONFIGS.websocketUrl, 42 | }) 43 | 44 | }, []) 45 | 46 | const pets = Object.values(usePetsStore(s => s.pets)) 47 | 48 | return ( 49 |
50 | 51 |
52 |

Location

53 |
54 | 55 | { setLocationInput(e.target.value) }} type="text" /> 56 | 57 | 58 |
59 |
60 | 61 |
62 |

The game

63 |

64 | WebSocket messages: {websocketMsgCount} 65 |

66 |

67 | {!pets.length ? ( 68 | No pets. Go rescue some pets! 69 | ) : ( 70 | Pets in {location}: {pets.length} 71 | )} 72 |

73 |

74 | {!adoptions.length ? ( 75 | No adoptions. Go get those pets adopted! 76 | ) : ( 77 | Adoptions in {location}: {adoptions.length} 78 | )} 79 |

80 |
81 | 82 |
83 |
84 |

Pets in {location}

85 |
86 | 87 |
88 |
89 | 90 |
91 |

Adoptions in {location}

92 |
93 | 94 |
95 |
96 |
97 | 98 |
99 | 100 |
101 | 102 |
103 | ); 104 | } 105 | 106 | export default App; 107 | 108 | function relativeWebsocketUrl(path: string) { 109 | let url = new URL(window.location.href) 110 | let protocol = url.protocol === 'https:' ? 'wss:' : 'ws:' 111 | return `${protocol}//${url.host}${path}` 112 | } 113 | -------------------------------------------------------------------------------- /services/websocket/index.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | const { Kafka, logLevel } = require('kafkajs') 3 | const { KafkaSink, KafkaLogger } = require('../lib') 4 | 5 | const PORT = process.env.NODE_PORT || 3300 6 | const KAFKA_HOSTS = (process.env.KAFKA_HOSTS || 'localhost:9092').split(',').map(s => s.trim()) 7 | const DATA_BASEPATH = process.env.DATA_BASEPATH || __dirname 8 | const CLIENT_ID = 'websocket' 9 | 10 | // Global state 11 | let sockets = [] 12 | const socketsForLocation = {} 13 | let consumer 14 | 15 | // --------------------------------------------------------------- 16 | // Kafka 17 | const kafka = new Kafka({ 18 | logLevel: logLevel.INFO, 19 | logCreator: KafkaLogger, 20 | brokers: KAFKA_HOSTS, 21 | clientId: CLIENT_ID, 22 | }) 23 | 24 | const allTopics = [ 25 | 'pets.added', 26 | 'pets.statusChanged', 27 | 'adoptions.requested', 28 | 'adoptions.statusChanged', 29 | ] 30 | 31 | const locationCache = new KafkaSink({ 32 | kafka, 33 | basePath: DATA_BASEPATH, 34 | name: 'websocket-location-cache', 35 | topics: ['pets.added', 'adoptions.requested'], 36 | onLog: ({log, sink}) => { 37 | if(!log.location) { 38 | return old 39 | } 40 | console.log(`Cacheing to location disk: ${log.id} - ${log.location}`) 41 | return sink.db.dbPut(log.id, {location: log.location}) 42 | } 43 | }) 44 | 45 | let retryAttempts = 0 46 | subscribeToNew() 47 | 48 | async function subscribeToNew () { 49 | if(retryAttempts > 9) 50 | return 51 | retryAttempts++ 52 | const consumerGroup = 'websocket-new' // Add random suffix to make each instance unique?? 53 | try { 54 | const consumer = kafka.consumer({ groupId: consumerGroup}) 55 | await consumer.connect() 56 | await consumer.subscribe({ topics: allTopics, fromBeginning: false }) 57 | await consumer.run({ 58 | autoCommit: false, 59 | eachMessage: async ({ topic, partition, message }) => { 60 | const log = JSON.parse(message.value.toString()) 61 | const location = locationCache.get(log.id, {}).location || log.location 62 | if(!location) { 63 | console.error(`Log doesnt have location or cached location ${JSON.stringify(log)} ${topic}`) 64 | return 65 | } 66 | eachSocketInLocation(location.toLowerCase(), (socket) => { 67 | socket.send(JSON.stringify({ type: 'kafka', topic, log })) 68 | }) 69 | }, 70 | }) 71 | } catch(e) { 72 | setTimeout(() => subscribeToNew(), 1500) // Try again 73 | console.error(`[${consumerGroup}] ${e.message}`, e) 74 | } 75 | } 76 | 77 | // --------------------------------------------------------------- 78 | // Websocket server 79 | const ws = new WebSocket.Server({ 80 | // host: '0.0.0.0', 81 | port: PORT, 82 | }, (a) => { 83 | const address = ws._server.address() 84 | console.log('WebSocket listening at ', address.address, address.port, address.family) 85 | }); 86 | 87 | 88 | ws.on('error', (e) => { 89 | console.error(e) 90 | }) 91 | 92 | ws.on('connection', (socket) => { 93 | console.log('Connection recieved for Websocket') 94 | sockets.push(socket) 95 | 96 | socket.on('error', console.error) 97 | socket.on('message', (str) => { 98 | try { 99 | let msg = JSON.parse(str) 100 | 101 | if(!msg.location) { 102 | socket.send(JSON.stringify({type: 'handshake.ack', ok: false, reasons: ['Missing .location field in handshake']})) 103 | return 104 | } 105 | const location = (msg.location+'').toLowerCase() 106 | socketsForLocation[location] = socketsForLocation[location] || [] 107 | socketsForLocation[location].push(socket) 108 | socket.send(JSON.stringify({type: 'handshake.ack', ok: true, reasons: [`Successfully subscribed to "${location}" changes`]})) 109 | 110 | } catch(e) { 111 | console.error(e) 112 | socket.close() 113 | } 114 | 115 | }) 116 | 117 | socket.on('close', function() { 118 | sockets = sockets.filter(s => s !== socket); 119 | }) 120 | 121 | }) 122 | 123 | function eachSocketInLocation(location, cb) { 124 | const sockets = socketsForLocation[location.toLowerCase()] || [] 125 | if(!sockets.length) { 126 | return 127 | } 128 | console.log(`Broadcasting for ${location}`) 129 | sockets.forEach(cb) 130 | } 131 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NIX_RUN = nix-shell --run 2 | BASH_RUN = bash -c 3 | DOCKER_ORG = ponelat 4 | 5 | # Check if nix-shell exists, use that. Else use bash. 6 | # Nix is a package manager that will install all the packages needed, 7 | # else you'll need to manually ensure you have all dependencies (like docker, docker-compose, nodejs, caddy, etc) 8 | ifneq (, $(shell which nix-shell)) 9 | RUN := $(NIX_RUN) 10 | else 11 | RUN := $(BASH_RUN) 12 | endif 13 | 14 | help: ## Prints help for targets with comments 15 | @grep -E '^[a-zA-Z._-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ 16 | sort | \ 17 | awk 'BEGIN {\ 18 | FS = ":.*?## "; \ 19 | print "If you are lost, read the README.md.\nList of tasks.\n";\ 20 | }; { \ 21 | printf "\033[36m%-30s\033[0m %s\n", $$1, $$2; \ 22 | }' 23 | 24 | dependencies: ## Prints list of dependencies in the project (for non-nix users) 25 | @grep -E '^ *[a-zA-Z._-]+ *.*?## .*$$' shell.nix | \ 26 | sort | \ 27 | awk 'BEGIN {FS = "## "; \ 28 | print "List of dependencies required.\n";\ 29 | }; { \ 30 | split($$2, descArr, "\\(optional\\)"); \ 31 | dep = $$1; \ 32 | desc = $$2; \ 33 | color = "35m"; \ 34 | if (length(descArr) > 1) {\ 35 | dep = dep " (optional)"; \ 36 | desc = descArr[2]; \ 37 | color = "33m"; \ 38 | } \ 39 | gsub(/^[ \t]+/,"",dep); \ 40 | gsub(/^[ \t]+/,"",desc); \ 41 | printf "\033[%s%-30s\033[0m%s\n",color,dep,desc \ 42 | }' 43 | 44 | dev-kafka: ## Runs just the Kafka services (kafka, zookeeper and CMAK) 45 | $(RUN) "cd ./services/kafka && docker-compose up" 46 | 47 | dev-pets: ## Runs the Pets service 48 | $(RUN) "cd ./services/pets && yarn && yarn run dev" 49 | 50 | dev-adoptions: ## Runs the Adoptions service 51 | $(RUN) "cd ./services/adoptions && yarn && yarn run dev" 52 | 53 | dev-websocket: ## Runs the Websocket service 54 | $(RUN) "cd ./services/websocket && yarn && yarn run dev" 55 | 56 | dev-gateway: ## Runs a gateway, proxying to other services and used by web-ui 57 | $(RUN) "cd ./web-ui && caddy run --envfile ./dev.env" 58 | 59 | dev-web-ui: ## Runs a create-react-app 60 | $(RUN) "cd ./web-ui && yarn && yarn start" 61 | 62 | dev: ## Start a Tmuxinator project running all dev services 63 | $(RUN) "tmuxinator start" 64 | 65 | build-pets: ## Build docker image for Pets service 66 | $(RUN) "cd ./services && docker build -f Dockerfile.pets -t $(DOCKER_ORG)/petstore-kafka-pets:latest ." 67 | 68 | build-adoptions: ## Build docker image for Adoptions service 69 | $(RUN) "cd ./services && docker build -f Dockerfile.adoptions -t $(DOCKER_ORG)/petstore-kafka-adoptions:latest ." 70 | 71 | build-websocket: ## Build docker image for Websocket service 72 | $(RUN) "cd ./services && docker build -f Dockerfile.websocket -t $(DOCKER_ORG)/petstore-kafka-websocket:latest ." 73 | 74 | build-web-ui: ## Build docker image for SPA (includes caddy gateway) 75 | $(RUN) "cd ./web-ui && docker build -f Dockerfile -t $(DOCKER_ORG)/petstore-kafka-web-ui:latest ." 76 | 77 | build: build-pets build-adoptions build-websocket build-web-ui ## Build all docker images 78 | 79 | clean-main: ## Clean the docker volumes used in `make start` 80 | $(RUN) "docker-compose -f ./docker-compose.yml down -v" 81 | 82 | clean-dev: ## Clean the docker volumes used in `make dev` 83 | $(RUN) "docker-compose -f ./services/kafka/docker-compose.yml down -v" 84 | 85 | docker-push-pets: ## Push docker image for pets 86 | $(RUN) "docker push $(DOCKER_ORG)/petstore-kafka-pets:latest" 87 | 88 | docker-push-adoptions: ## Push docker image for adoptions 89 | $(RUN) "docker push $(DOCKER_ORG)/petstore-kafka-adoptions:latest" 90 | 91 | docker-push-websocket: ## Push docker image for websocket 92 | $(RUN) "docker push $(DOCKER_ORG)/petstore-kafka-websocket:latest" 93 | 94 | docker-push-web-ui: ## Push docker image for web-ui 95 | $(RUN) "docker push $(DOCKER_ORG)/petstore-kafka-web-ui:latest" 96 | 97 | docker-push: docker-push-pets docker-push-adoptions docker-push-websocket docker-push-web-ui ## Push ALL docker images 98 | 99 | build-adoptions: ## Build docker image for Adoptions service 100 | $(RUN) "cd ./services && docker build -f Dockerfile.adoptions -t $(DOCKER_ORG)/petstore-kafka-adoptions:latest ." 101 | 102 | build-websocket: ## Build docker image for Websocket service 103 | $(RUN) "cd ./services && docker build -f Dockerfile.websocket -t $(DOCKER_ORG)/petstore-kafka-websocket:latest ." 104 | 105 | build-web-ui: ## Build docker image for SPA (includes caddy gateway) 106 | $(RUN) "cd ./web-ui && docker build -f Dockerfile -t $(DOCKER_ORG)/petstore-kafka-web-ui:latest ." 107 | 108 | start: ## Start the entire stack via docker-compose. May require building images first with make build. 109 | $(RUN) "docker-compose up" 110 | -------------------------------------------------------------------------------- /services/pets/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const path = require('path') 3 | const cors = require('cors') 4 | const app = express() 5 | const bodyParser = require('body-parser') 6 | const uuid = require('uuid') 7 | const morgan = require('morgan') 8 | const { Kafka, logLevel } = require('kafkajs') 9 | const { KafkaSink, KafkaLogger, KafkaStream, FlatDB: { queryObjToMatchQuery } } = require('../lib') 10 | 11 | // Configs 12 | const KAFKA_HOSTS = (process.env.KAFKA_HOSTS || 'localhost:9092').split(',').map(s => s.trim()) 13 | const DATA_BASEPATH = process.env.DATA_BASEPATH || __dirname 14 | const CLIENT_ID = 'pets' 15 | 16 | // --------------------------------------------------------------- 17 | // Kafka 18 | console.log('Connecting to Kafka on: ' + JSON.stringify(KAFKA_HOSTS)) 19 | const kafka = new Kafka({ 20 | logLevel: logLevel.INFO, 21 | logCreator: KafkaLogger, 22 | brokers: KAFKA_HOSTS, 23 | clientId: CLIENT_ID, 24 | retry: { 25 | initialRetryTime: 1000, 26 | retries: 16 27 | } 28 | }) 29 | 30 | const consumers = [] 31 | const producer = kafka.producer() 32 | producer.connect() 33 | 34 | // Consume kafka 35 | const petsCache = new KafkaSink({ 36 | kafka, 37 | basePath: DATA_BASEPATH, 38 | name: 'pets-cache', 39 | topics: ['pets.added', 'pets.statusChanged'], 40 | onLog: ({log, topic, sink}) => { 41 | 42 | if(topic === 'pets.added') { 43 | console.log(`Adding pet to disk: ${log.id} - ${log.name}`) 44 | sink.db.dbPut(log.id, {...log, status: 'pending'}) 45 | return 46 | } 47 | 48 | if(topic === 'pets.statusChanged') { 49 | console.log(`Updating pet status to disk: ${log.id} - ${log.status}`) 50 | // Save to DB with new status 51 | sink.db.dbMerge(log.id, {status: log.status}) 52 | return 53 | } 54 | } 55 | }) 56 | 57 | new KafkaStream({ 58 | kafka, 59 | name: 'pets-stream', 60 | topics: ['pets.added'], 61 | onLog: async ({ log }) => { 62 | console.log(`Pet added, producing pets.statusChanged ${log.id} - available`) 63 | producer.send({ 64 | topic: 'pets.statusChanged', 65 | messages: [ 66 | { value: JSON.stringify({ ...log, status: 'available'}) }, 67 | ], 68 | }) 69 | } 70 | }) 71 | 72 | // --------------------------------------------------------------- 73 | // Rest 74 | app.use(morgan('short')) 75 | app.use(cors()) 76 | app.use(bodyParser.json()) 77 | 78 | app.get('/api/pets', (req, res) => { 79 | const { location, status } = req.query 80 | 81 | if(!location && !status) { 82 | return res.json(petsCache.db.dbGetAll()) 83 | } 84 | 85 | let query = queryObjToMatchQuery({ status, location }) 86 | return res.json(petsCache.db.dbQuery(query)) 87 | }) 88 | 89 | app.post('/api/pets', (req, res) => { 90 | const pet = req.body 91 | pet.id = pet.id || uuid.v4() 92 | 93 | producer.send({ 94 | topic: 'pets.added', 95 | messages: [ 96 | { value: JSON.stringify(pet) }, 97 | ], 98 | }) 99 | 100 | res.status(201).send(pet) 101 | }) 102 | 103 | app.patch('/api/pets/:id', (req, res) => { 104 | const pet = petsCache.db.dbGet(req.params.id) 105 | const { status } = req.body 106 | if(!pet) 107 | res.status(400).json({ 108 | message: 'Pet not found, cannot patch.' 109 | }) 110 | 111 | const updatedPet = {...pet, status } 112 | 113 | producer.send({ 114 | topic: 'pets.statusChanged', 115 | messages: [ 116 | { value: JSON.stringify(updatedPet) }, 117 | ], 118 | }) 119 | 120 | res.status(201).send(updatedPet) 121 | }) 122 | 123 | // // SPA 124 | // app.use(express.static(path.resolve(__dirname, process.env.SPA_PATH || '../web-ui/build'))) 125 | 126 | 127 | // --------------------------------------------------------------------------------------- 128 | // Boring stuff follows... 129 | // --------------------------------------------------------------------------------------- 130 | 131 | // Start server and handle logic around graceful exit 132 | const server = app.listen(process.env.NODE_PORT || 3100, () => { 133 | console.log('Server listening on http://' + server.address().address + ':' + server.address().port) 134 | }) 135 | // Keep track of connections to kill 'em off later. 136 | let connections = [] 137 | server.on('connection', connection => { 138 | connections.push(connection); 139 | connection.on('close', () => connections = connections.filter(curr => curr !== connection)); 140 | }); 141 | 142 | // Exit gracefully 143 | const errorTypes = ['unhandledRejection', 'uncaughtException'] 144 | const signalTraps = ['SIGTERM', 'SIGINT', 'SIGUSR2'] 145 | errorTypes.forEach(type => { 146 | process.on(type, async e => { 147 | try { 148 | console.log(`process.on ${type}`) 149 | console.error(e) 150 | await shutdown() 151 | } catch (_) { 152 | process.exit(1) 153 | } 154 | }) 155 | }) 156 | 157 | 158 | signalTraps.forEach(type => { 159 | process.once(type, async () => { 160 | try { 161 | await shutdown() 162 | } finally { 163 | process.kill(process.pid, type) 164 | } 165 | }) 166 | }) 167 | 168 | 169 | async function shutdown() { 170 | await Promise.all(consumers.map(consumer => consumer.disconnect())) 171 | 172 | server.close(() => { 173 | console.log('Closed out remaining connections'); 174 | process.exit(0); 175 | }); 176 | 177 | setTimeout(() => { 178 | console.error('Could not close connections in time, forcefully shutting down'); 179 | process.exit(1); 180 | }, 5000); 181 | 182 | connections.forEach(curr => curr.end()); 183 | setTimeout(() => connections.forEach(curr => curr.destroy()), 5000); 184 | } 185 | 186 | -------------------------------------------------------------------------------- /web-ui/src/Pets.tsx: -------------------------------------------------------------------------------- 1 | import {useState, useCallback, useEffect} from 'react' 2 | import usePetStore from './pets.store' 3 | import useAdoptionsStore from './adoptions.store' 4 | import randomName from './pets.name' 5 | import { Button } from './UI' 6 | import { PlusIcon, UploadIcon, CheckIcon } from '@heroicons/react/solid' 7 | import {getQuery} from './query' 8 | 9 | function useSelected(): [Set, (str: string) => void, () => void] { 10 | 11 | const [state, setState] = useState>(new Set()) 12 | 13 | const _toggle = (str: string) => setState(s => { 14 | let newSet = new Set(s) 15 | if(newSet.has(str)) { 16 | newSet.delete(str) 17 | } else { 18 | newSet.add(str) 19 | } 20 | return newSet 21 | }) 22 | 23 | const _clear = () => setState(new Set()) 24 | return [ 25 | state, _toggle, _clear 26 | ] 27 | } 28 | 29 | export default function Pets() { 30 | 31 | const fetchPets = usePetStore(s => s.fetchPets) 32 | const addPet = usePetStore(s => s.addPet) 33 | const pets = usePetStore(s => s.pets) 34 | const location = getQuery('location') 35 | const rows = Object.values(pets) || [] 36 | rows.reverse() 37 | const [selectedRows, toggleRow, deselectAll] = useSelected() 38 | 39 | useEffect(() => { 40 | fetchPets({ location, status: '!adopted' }) 41 | }, [location]) 42 | 43 | const _requestAdoption = useAdoptionsStore(s => s.requestAdoptions) 44 | const requestAdoption = () => { 45 | deselectAll() 46 | _requestAdoption({ pets: Array.from(selectedRows), location }) 47 | } 48 | 49 | const [name, setName] = useState(randomName()) 50 | const onAdd = useCallback(() => { 51 | addPet({ name, location }) 52 | setName(randomName()) 53 | }, [name, addPet, setName, location ]) 54 | 55 | return ( 56 |
57 |
58 |
59 | 60 |
61 |
62 |
63 | { setName(e.target.value) }} type="text" /> 64 | 65 |
66 |
67 | 68 |
69 |
70 |
71 | 72 | 73 | 74 | 77 | 80 | 83 | 84 | 85 | 86 | {rows.map((row) => ( 87 | toggleRow(row.id)} className={`odd:bg-gray-50 cursor-pointer`}> 88 | 89 | 98 | 103 | 111 | 112 | ))} 113 | {rows.length ? null : ( 114 | 115 | 118 | 119 | )} 120 | 121 |
75 | Name 76 | 78 | Status 79 | 81 | 82 |
90 |
91 |
92 |
93 | {row.name} 94 |
95 |
96 |
97 |
99 | 100 | {row.status} 101 | 102 | 104 |
105 |
106 | {selectedRows.has(row.id) && } 107 | e.stopPropagation()} readOnly checked={selectedRows.has(row.id)} /> 108 |
109 |
110 |
116 |
No rows. Try reset filters
117 |
122 |
123 | 124 |
125 |
126 |
127 |
128 | ) 129 | } 130 | -------------------------------------------------------------------------------- /services/lib/db.js: -------------------------------------------------------------------------------- 1 | const FlatFile = require('flat-file-db') 2 | const path = require('path') 3 | 4 | class FlatDb { 5 | constructor(file) { 6 | this.db = FlatFile.sync(file) 7 | this.db.on('open', () => this.log(`${file} opened`)) 8 | } 9 | 10 | log(...msgs) { 11 | console.log(`[DB][I] ${new Date()} - ${msgs.join(' - ')}`) 12 | } 13 | 14 | dbPut(key, value) { 15 | return this.db.put(key, value) 16 | } 17 | 18 | // All keys, except the _meta key. 19 | dbGetAll(withMeta=false) { 20 | let keys = this.db.keys() 21 | keys = withMeta ? keys : keys.filter(a => a !== '_meta') 22 | return keys.map(key => this.db.get(key)) 23 | } 24 | 25 | dbGet(key) { 26 | return this.db.get(key) 27 | } 28 | 29 | // {location: 'plett'} will filter all objects that have obj.location === 'plett' 30 | dbQuery(query) { 31 | return queryFilter(this.dbGetAll(), query) 32 | } 33 | 34 | // Update / Merge 35 | dbMerge(key, newValues) { 36 | const ori = this.dbGet(key) || {} 37 | 38 | if(typeof ori !== 'object' || !ori || typeof newValues !== 'object' || !newValues) { 39 | throw new Error('Tried to merge value(s) that are not objects') 40 | } 41 | 42 | return this.dbPut(key, {...ori, ...newValues}) 43 | } 44 | 45 | // Meta to keep it outside of other commands 46 | dbGetMeta (key) { 47 | let _meta = this.db.get('_meta') || {} 48 | 49 | // Heal from non-object value stored 50 | _meta = typeof _meta === 'object' && _meta ? _meta : {} 51 | return key ? _meta[key] : _meta 52 | } 53 | 54 | dbPutMeta (key, value) { 55 | let _meta = this.dbGetMeta() || {} 56 | if(key) { 57 | _meta[key] = value 58 | } else { 59 | _meta = value 60 | } 61 | return this.db.put('_meta', _meta) 62 | } 63 | 64 | } 65 | 66 | module.exports = FlatDb 67 | module.exports.queryFilter = queryFilter 68 | module.exports.matchesQuery = matchesQuery 69 | module.exports.queryStrToMatchQuery = queryStrToMatchQuery 70 | module.exports.queryObjToMatchQuery = queryObjToMatchQuery 71 | 72 | // ------------------------------------------- 73 | // Utilities 74 | 75 | function queryFilter(values, query) { 76 | if(!Array.isArray(values)) { 77 | return [] 78 | } 79 | return values.filter((value) => matchesQuery(value, query, {})) 80 | } 81 | 82 | function queryStrToMatchQuery(str) { 83 | if(typeof str === 'undefined') 84 | return undefined 85 | str = str+'' 86 | 87 | if(str.includes('|')) 88 | return {$or: str.split('|').map(queryStrToMatchQuery)} 89 | 90 | if(str.includes('&')) 91 | return {$and: str.split('&').map(queryStrToMatchQuery)} 92 | 93 | if(str.startsWith('!')) { 94 | return {$not: queryStrToMatchQuery(str.replace('!', ''))} 95 | } 96 | 97 | return {$eqi: str} 98 | } 99 | 100 | // !test = {$not: test} 101 | // [one,two] = {$and: [one,two]} 102 | function queryObjToMatchQuery(obj) { 103 | let query = {} 104 | for(field in obj) { 105 | let value = obj[field] 106 | if(Array.isArray(value)) { 107 | value = {$and: value.map(v => queryStrToMatchQuery(v))} 108 | } else { 109 | value = queryStrToMatchQuery(value) 110 | } 111 | query[field] = value 112 | } 113 | return query 114 | } 115 | 116 | // query = scalar | {[testField]: query, ...queryObject} 117 | // queryObject = {$eq: scalar, $not: queryObject, $or: queryObj[], $and: queryObj[]} 118 | // ie: a testField is any key that is not part of the predefined query list. To test a field that has a dollar in it, you'll need to escape it with double dollar. Eg: $$eq will be the testField $eq 119 | 120 | const specialKeys = new Set(['$eq', '$eqi', '$or', '$and', '$not']) 121 | function matchesQuery(testValue, query) { 122 | 123 | // Canonicalize the query 124 | if(!isObj(query)) { 125 | query = {$eq: query} 126 | } 127 | 128 | // $eq 129 | if(typeof query.$eq !== 'undefined') { 130 | let scalarValue = query.$eq 131 | 132 | if(typeof testValue !== typeof scalarValue) 133 | return false 134 | 135 | if(!Object.is(testValue, scalarValue)) { 136 | return false 137 | } 138 | } 139 | 140 | if(typeof query.$eqi !== 'undefined') { 141 | let scalarValue = query.$eqi 142 | 143 | if(typeof testValue !== typeof scalarValue) { 144 | return false 145 | } 146 | 147 | if(typeof scalarValue === 'string') { 148 | if(testValue.toLowerCase() !== scalarValue.toLowerCase()) { 149 | return false 150 | } 151 | } else if(!Object.is(testValue, scalarValue)) { 152 | return false 153 | } 154 | 155 | } 156 | 157 | if(typeof query.$not !== 'undefined') { 158 | let childQuery = query.$not 159 | if(matchesQuery(testValue, childQuery)) 160 | return false 161 | } 162 | 163 | 164 | if(typeof query.$or !== 'undefined') { 165 | let clauses = query.$or 166 | if(!Array.isArray(clauses)) { 167 | throw new Error('$or must be an array of queries') 168 | } 169 | 170 | if(!clauses.some(q => matchesQuery(testValue, q))) { 171 | return false 172 | } 173 | } 174 | 175 | if(typeof query.$and !== 'undefined') { 176 | let clauses = query.$and 177 | if(!Array.isArray(clauses)) { 178 | throw new Error('$and must be an array of queries') 179 | } 180 | 181 | if(clauses.some(q => !matchesQuery(testValue, q))) { 182 | return false 183 | } 184 | } 185 | 186 | // Try other fields 187 | let testFields = Object 188 | .keys(query) 189 | .filter(f => !specialKeys.has(f)) 190 | .map(field => { 191 | if(field.startsWith('$$')) { 192 | return field.replace('$$', '$') // Will only replace the first match. 193 | } 194 | return field 195 | }) 196 | 197 | // It has to be an object for this to work. 198 | if(testFields.length && !isObj(testValue)) 199 | return false 200 | 201 | if(testFields.some(field => { 202 | let queryField = field.startsWith('$') ? '$' + field : field 203 | return !matchesQuery(testValue[field], query[queryField]) 204 | })) { 205 | return false 206 | } 207 | 208 | return true 209 | } 210 | 211 | function isObj(test) { 212 | return test && typeof test === 'object' 213 | } 214 | 215 | function omit(obj, keys) { 216 | let newObj = {...obj} 217 | keys.forEach(key => { 218 | delete newObj[key] 219 | }) 220 | return newObj 221 | } 222 | -------------------------------------------------------------------------------- /web-ui/src/pet.names.ts: -------------------------------------------------------------------------------- 1 | 2 | const NAMES = [ 3 | 'Abby', 4 | 'Abe', 5 | 'Addie', 6 | 'Abbott', 7 | 'Alexis', 8 | 'Ace', 9 | 'Alice', 10 | 'Aero', 11 | 'Allie', 12 | 'Aiden', 13 | 'Alyssa', 14 | 'AJ', 15 | 'Amber', 16 | 'Albert', 17 | 'Angel', 18 | 'Alden', 19 | 'Anna', 20 | 'Alex', 21 | 'Annie', 22 | 'Alfie', 23 | 'Ariel', 24 | 'Alvin', 25 | 'Ashley', 26 | 'Amos', 27 | 'Aspen', 28 | 'Andy', 29 | 'Athena', 30 | 'Angus', 31 | 'Autumn', 32 | 'Apollo', 33 | 'Ava', 34 | 'Archie', 35 | 'Avery', 36 | 'Aries', 37 | 'Baby', 38 | 'Artie', 39 | 'Bailey', 40 | 'Ash', 41 | 'Basil', 42 | 'Austin', 43 | 'Bean', 44 | 'Axel', 45 | 'Bella', 46 | 'Bailey', 47 | 'Belle', 48 | 'Bandit', 49 | 'Betsy', 50 | 'Barkley', 51 | 'Betty', 52 | 'Barney', 53 | 'Bianca', 54 | 'Baron', 55 | 'Birdie', 56 | 'Baxter', 57 | 'Biscuit', 58 | 'Bear', 59 | 'Blondie', 60 | 'Beau', 61 | 'Blossom', 62 | 'Benji', 63 | 'Bonnie', 64 | 'Benny', 65 | 'Brandy', 66 | 'Bentley', 67 | 'Brooklyn', 68 | 'Billy', 69 | 'Brownie', 70 | 'Bingo', 71 | 'Buffy', 72 | 'Blake', 73 | 'Callie', 74 | 'Blaze', 75 | 'Camilla', 76 | 'Blue', 77 | 'Candy', 78 | 'Bo', 79 | 'Carla', 80 | 'Boomer', 81 | 'Carly', 82 | 'Brady', 83 | 'Carmela', 84 | 'Brody', 85 | 'Casey', 86 | 'Brownie', 87 | 'Cassie', 88 | 'Bruce', 89 | 'Chance', 90 | 'Bruno', 91 | 'Chanel', 92 | 'Brutus', 93 | 'Chloe', 94 | 'Bubba', 95 | 'Cinnamon', 96 | 'Buck', 97 | 'Cleo', 98 | 'Buddy', 99 | 'Coco', 100 | 'Buster', 101 | 'Cookie', 102 | 'Butch', 103 | 'Cricket', 104 | 'Buzz', 105 | 'Daisy', 106 | 'Cain', 107 | 'Dakota', 108 | 'Captain', 109 | 'Dana', 110 | 'Carter', 111 | 'Daphne', 112 | 'Cash', 113 | 'Darla', 114 | 'Casper', 115 | 'Darlene', 116 | 'Champ', 117 | 'Delia', 118 | 'Chance', 119 | 'Delilah', 120 | 'Charlie', 121 | 'Destiny', 122 | 'Chase', 123 | 'Diamond', 124 | 'Chester', 125 | 'Diva', 126 | 'Chewy', 127 | 'Dixie', 128 | 'Chico', 129 | 'Dolly', 130 | 'Chief', 131 | 'Duchess', 132 | 'Chip', 133 | 'Eden', 134 | 'CJ', 135 | 'Edie', 136 | 'Clifford', 137 | 'Ella', 138 | 'Clyde', 139 | 'Ellie', 140 | 'Coco', 141 | 'Elsa', 142 | 'Cody', 143 | 'Emma', 144 | 'Colby', 145 | 'Emmy', 146 | 'Cooper', 147 | 'Eva', 148 | 'Copper', 149 | 'Faith', 150 | 'Damien', 151 | 'Fanny', 152 | 'Dane', 153 | 'Fern', 154 | 'Dante', 155 | 'Fiona', 156 | 'Denver', 157 | 'Foxy', 158 | 'Dexter', 159 | 'Gabby', 160 | 'Diego', 161 | 'Gemma', 162 | 'Diesel', 163 | 'Georgia', 164 | 'Dodge', 165 | 'Gia', 166 | 'Drew', 167 | 'Gidget', 168 | 'Duke', 169 | 'Gigi', 170 | 'Dylan', 171 | 'Ginger', 172 | 'Eddie', 173 | 'Goldie', 174 | 'Eli', 175 | 'Grace', 176 | 'Elmer', 177 | 'Gracie', 178 | 'Emmett', 179 | 'Greta', 180 | 'Evan', 181 | 'Gypsy', 182 | 'Felix', 183 | 'Hailey', 184 | 'Finn', 185 | 'Hannah', 186 | 'Fisher', 187 | 'Harley', 188 | 'Flash', 189 | 'Harper', 190 | 'Frankie', 191 | 'Hazel', 192 | 'Freddy', 193 | 'Heidi', 194 | 'Fritz', 195 | 'Hershey', 196 | 'Gage', 197 | 'Holly', 198 | 'George', 199 | 'Honey', 200 | 'Gizmo', 201 | 'Hope', 202 | 'Goose', 203 | 'Ibby', 204 | 'Gordie', 205 | 'Inez', 206 | 'Griffin', 207 | 'Isabella', 208 | 'Gunner', 209 | 'Ivy', 210 | 'Gus', 211 | 'Izzy', 212 | 'Hank', 213 | 'Jackie', 214 | 'Harley', 215 | 'Jada', 216 | 'Harvey', 217 | 'Jade', 218 | 'Hawkeye', 219 | 'Jasmine', 220 | 'Henry', 221 | 'Jenna', 222 | 'Hoss', 223 | 'Jersey', 224 | 'Huck', 225 | 'Jessie', 226 | 'Hunter', 227 | 'Jill', 228 | 'Iggy', 229 | 'Josie', 230 | 'Ivan', 231 | 'Julia', 232 | 'Jack', 233 | 'Juliet', 234 | 'Jackson', 235 | 'Juno', 236 | 'Jake', 237 | 'Kali', 238 | 'Jasper', 239 | 'Kallie', 240 | 'Jax', 241 | 'Karma', 242 | 'Jesse', 243 | 'Kate', 244 | 'Joey', 245 | 'Katie', 246 | 'Johnny', 247 | 'Kayla', 248 | 'Judge', 249 | 'Kelsey', 250 | 'Kane', 251 | 'Khloe', 252 | 'King', 253 | 'Kiki', 254 | 'Kobe', 255 | 'Kira', 256 | 'Koda', 257 | 'Koko', 258 | 'Lenny', 259 | 'Kona', 260 | 'Leo', 261 | 'Lacy', 262 | 'Leroy', 263 | 'Lady', 264 | 'Levi', 265 | 'Layla', 266 | 'Lewis', 267 | 'Leia', 268 | 'Logan', 269 | 'Lena', 270 | 'Loki', 271 | 'Lexi', 272 | 'Louie', 273 | 'Libby', 274 | 'Lucky', 275 | 'Liberty', 276 | 'Luke', 277 | 'Lily', 278 | 'Marley', 279 | 'Lizzy', 280 | 'Marty', 281 | 'Lola', 282 | 'Maverick', 283 | 'London', 284 | 'Max', 285 | 'Lucky', 286 | 'Maximus', 287 | 'Lulu', 288 | 'Mickey', 289 | 'Luna', 290 | 'Miles', 291 | 'Mabel', 292 | 'Milo', 293 | 'Mackenzie', 294 | 'Moe', 295 | 'Macy', 296 | 'Moose', 297 | 'Maddie', 298 | 'Morris', 299 | 'Madison', 300 | 'Murphy', 301 | 'Maggie', 302 | 'Ned', 303 | 'Maisy', 304 | 'Nelson', 305 | 'Mandy', 306 | 'Nero', 307 | 'Marley', 308 | 'Nico', 309 | 'Matilda', 310 | 'Noah', 311 | 'Mattie', 312 | 'Norm', 313 | 'Maya', 314 | 'Oakley', 315 | 'Mia', 316 | 'Odie', 317 | 'Mika', 318 | 'Odin', 319 | 'Mila', 320 | 'Oliver', 321 | 'Miley', 322 | 'Ollie', 323 | 'Millie', 324 | 'Oreo', 325 | 'Mimi', 326 | 'Oscar', 327 | 'Minnie', 328 | 'Otis', 329 | 'Missy', 330 | 'Otto', 331 | 'Misty', 332 | 'Ozzy', 333 | 'Mitzi', 334 | 'Pablo', 335 | 'Mocha', 336 | 'Parker', 337 | 'Molly', 338 | 'Peanut', 339 | 'Morgan', 340 | 'Pepper', 341 | 'Moxie', 342 | 'Petey', 343 | 'Muffin', 344 | 'Porter', 345 | 'Mya', 346 | 'Prince', 347 | 'Nala', 348 | 'Quincy', 349 | 'Nell', 350 | 'Radar', 351 | 'Nellie', 352 | 'Ralph', 353 | 'Nikki', 354 | 'Rambo', 355 | 'Nina', 356 | 'Ranger', 357 | 'Noel', 358 | 'Rascal', 359 | 'Nola', 360 | 'Rebel', 361 | 'Nori', 362 | 'Reese', 363 | 'Olive', 364 | 'Reggie', 365 | 'Olivia', 366 | 'Remy', 367 | 'Oreo', 368 | 'Rex', 369 | 'Paisley', 370 | 'Ricky', 371 | 'Pandora', 372 | 'Rider', 373 | 'Paris', 374 | 'Riley', 375 | 'Peaches', 376 | 'Ringo', 377 | 'Peanut', 378 | 'Rocco', 379 | 'Pearl', 380 | 'Rockwell', 381 | 'Pebbles', 382 | 'Rocky', 383 | 'Penny', 384 | 'Romeo', 385 | 'Pepper', 386 | 'Rosco', 387 | 'Phoebe', 388 | 'Rudy', 389 | 'Piper', 390 | 'Rufus', 391 | 'Pippa', 392 | 'Rusty', 393 | 'Pixie', 394 | 'Sam', 395 | 'Polly', 396 | 'Sammy', 397 | 'Poppy', 398 | 'Samson', 399 | 'Precious', 400 | 'Sarge', 401 | 'Princess', 402 | 'Sawyer', 403 | 'Priscilla', 404 | 'Scooby', 405 | 'Raven', 406 | 'Scooter', 407 | 'Reese', 408 | 'Scout', 409 | 'Riley', 410 | 'Scrappy', 411 | 'Rose', 412 | 'Shadow', 413 | 'Rosie', 414 | 'Shamus', 415 | 'Roxy', 416 | 'Shiloh', 417 | 'Ruby', 418 | 'Simba', 419 | 'Sadie', 420 | 'Simon', 421 | 'Sage', 422 | 'Smoky', 423 | 'Sally', 424 | 'Snoopy', 425 | 'Sam', 426 | 'Sparky', 427 | 'Samantha', 428 | 'Spencer', 429 | 'Sammie', 430 | 'Spike', 431 | 'Sandy', 432 | 'Spot', 433 | 'Sasha', 434 | 'Stanley', 435 | 'Sassy', 436 | 'Stewie', 437 | 'Savannah', 438 | 'Storm', 439 | 'Scarlet', 440 | 'Taco', 441 | 'Shadow', 442 | 'Tank', 443 | 'Sheba', 444 | 'Taz', 445 | 'Shelby', 446 | 'Teddy', 447 | 'Shiloh', 448 | 'Tesla', 449 | 'Sierra', 450 | 'Theo', 451 | 'Sissy', 452 | 'Thor', 453 | 'Sky', 454 | 'Titus', 455 | 'Smokey', 456 | 'TJ', 457 | 'Snickers', 458 | 'Toby', 459 | 'Sophia', 460 | 'Trapper', 461 | 'Sophie', 462 | 'Tripp', 463 | 'Star', 464 | 'Tucker', 465 | 'Stella', 466 | 'Tyler', 467 | 'Sugar', 468 | 'Tyson', 469 | 'Suki', 470 | 'Vince', 471 | 'Summer', 472 | 'Vinnie', 473 | 'Sunny', 474 | 'Wally', 475 | 'Sweetie', 476 | 'Walter', 477 | 'Sydney', 478 | 'Watson', 479 | 'Tasha', 480 | 'Willy', 481 | 'Tessa', 482 | 'Winston', 483 | 'Tilly', 484 | 'Woody', 485 | 'Tootsie', 486 | 'Wrigley', 487 | 'Trixie', 488 | 'Wyatt', 489 | 'Violet', 490 | 'Yogi', 491 | 'Willow', 492 | 'Yoshi', 493 | 'Winnie', 494 | 'Yukon', 495 | 'Xena', 496 | 'Zane', 497 | 'Zelda', 498 | 'Zeus', 499 | 'Zoe', 500 | 'Ziggy' 501 | ] 502 | const NAMES_LEN = NAMES.length 503 | export default function randomName(): string { 504 | return NAMES[Math.floor(Math.random() * NAMES_LEN)] 505 | } 506 | -------------------------------------------------------------------------------- /web-ui/src/pets.name.ts: -------------------------------------------------------------------------------- 1 | 2 | const NAMES = [ 3 | 'Abby', 4 | 'Abe', 5 | 'Addie', 6 | 'Abbott', 7 | 'Alexis', 8 | 'Ace', 9 | 'Alice', 10 | 'Aero', 11 | 'Allie', 12 | 'Aiden', 13 | 'Alyssa', 14 | 'AJ', 15 | 'Amber', 16 | 'Albert', 17 | 'Angel', 18 | 'Alden', 19 | 'Anna', 20 | 'Alex', 21 | 'Annie', 22 | 'Alfie', 23 | 'Ariel', 24 | 'Alvin', 25 | 'Ashley', 26 | 'Amos', 27 | 'Aspen', 28 | 'Andy', 29 | 'Athena', 30 | 'Angus', 31 | 'Autumn', 32 | 'Apollo', 33 | 'Ava', 34 | 'Archie', 35 | 'Avery', 36 | 'Aries', 37 | 'Baby', 38 | 'Artie', 39 | 'Bailey', 40 | 'Ash', 41 | 'Basil', 42 | 'Austin', 43 | 'Bean', 44 | 'Axel', 45 | 'Bella', 46 | 'Bailey', 47 | 'Belle', 48 | 'Bandit', 49 | 'Betsy', 50 | 'Barkley', 51 | 'Betty', 52 | 'Barney', 53 | 'Bianca', 54 | 'Baron', 55 | 'Birdie', 56 | 'Baxter', 57 | 'Biscuit', 58 | 'Bear', 59 | 'Blondie', 60 | 'Beau', 61 | 'Blossom', 62 | 'Benji', 63 | 'Bonnie', 64 | 'Benny', 65 | 'Brandy', 66 | 'Bentley', 67 | 'Brooklyn', 68 | 'Billy', 69 | 'Brownie', 70 | 'Bingo', 71 | 'Buffy', 72 | 'Blake', 73 | 'Callie', 74 | 'Blaze', 75 | 'Camilla', 76 | 'Blue', 77 | 'Candy', 78 | 'Bo', 79 | 'Carla', 80 | 'Boomer', 81 | 'Carly', 82 | 'Brady', 83 | 'Carmela', 84 | 'Brody', 85 | 'Casey', 86 | 'Brownie', 87 | 'Cassie', 88 | 'Bruce', 89 | 'Chance', 90 | 'Bruno', 91 | 'Chanel', 92 | 'Brutus', 93 | 'Chloe', 94 | 'Bubba', 95 | 'Cinnamon', 96 | 'Buck', 97 | 'Cleo', 98 | 'Buddy', 99 | 'Coco', 100 | 'Buster', 101 | 'Cookie', 102 | 'Butch', 103 | 'Cricket', 104 | 'Buzz', 105 | 'Daisy', 106 | 'Cain', 107 | 'Dakota', 108 | 'Captain', 109 | 'Dana', 110 | 'Carter', 111 | 'Daphne', 112 | 'Cash', 113 | 'Darla', 114 | 'Casper', 115 | 'Darlene', 116 | 'Champ', 117 | 'Delia', 118 | 'Chance', 119 | 'Delilah', 120 | 'Charlie', 121 | 'Destiny', 122 | 'Chase', 123 | 'Diamond', 124 | 'Chester', 125 | 'Diva', 126 | 'Chewy', 127 | 'Dixie', 128 | 'Chico', 129 | 'Dolly', 130 | 'Chief', 131 | 'Duchess', 132 | 'Chip', 133 | 'Eden', 134 | 'CJ', 135 | 'Edie', 136 | 'Clifford', 137 | 'Ella', 138 | 'Clyde', 139 | 'Ellie', 140 | 'Coco', 141 | 'Elsa', 142 | 'Cody', 143 | 'Emma', 144 | 'Colby', 145 | 'Emmy', 146 | 'Cooper', 147 | 'Eva', 148 | 'Copper', 149 | 'Faith', 150 | 'Damien', 151 | 'Fanny', 152 | 'Dane', 153 | 'Fern', 154 | 'Dante', 155 | 'Fiona', 156 | 'Denver', 157 | 'Foxy', 158 | 'Dexter', 159 | 'Gabby', 160 | 'Diego', 161 | 'Gemma', 162 | 'Diesel', 163 | 'Georgia', 164 | 'Dodge', 165 | 'Gia', 166 | 'Drew', 167 | 'Gidget', 168 | 'Duke', 169 | 'Gigi', 170 | 'Dylan', 171 | 'Ginger', 172 | 'Eddie', 173 | 'Goldie', 174 | 'Eli', 175 | 'Grace', 176 | 'Elmer', 177 | 'Gracie', 178 | 'Emmett', 179 | 'Greta', 180 | 'Evan', 181 | 'Gypsy', 182 | 'Felix', 183 | 'Hailey', 184 | 'Finn', 185 | 'Hannah', 186 | 'Fisher', 187 | 'Harley', 188 | 'Flash', 189 | 'Harper', 190 | 'Frankie', 191 | 'Hazel', 192 | 'Freddy', 193 | 'Heidi', 194 | 'Fritz', 195 | 'Hershey', 196 | 'Gage', 197 | 'Holly', 198 | 'George', 199 | 'Honey', 200 | 'Gizmo', 201 | 'Hope', 202 | 'Goose', 203 | 'Ibby', 204 | 'Gordie', 205 | 'Inez', 206 | 'Griffin', 207 | 'Isabella', 208 | 'Gunner', 209 | 'Ivy', 210 | 'Gus', 211 | 'Izzy', 212 | 'Hank', 213 | 'Jackie', 214 | 'Harley', 215 | 'Jada', 216 | 'Harvey', 217 | 'Jade', 218 | 'Hawkeye', 219 | 'Jasmine', 220 | 'Henry', 221 | 'Jenna', 222 | 'Hoss', 223 | 'Jersey', 224 | 'Huck', 225 | 'Jessie', 226 | 'Hunter', 227 | 'Jill', 228 | 'Iggy', 229 | 'Josie', 230 | 'Ivan', 231 | 'Julia', 232 | 'Jack', 233 | 'Juliet', 234 | 'Jackson', 235 | 'Juno', 236 | 'Jake', 237 | 'Kali', 238 | 'Jasper', 239 | 'Kallie', 240 | 'Jax', 241 | 'Karma', 242 | 'Jesse', 243 | 'Kate', 244 | 'Joey', 245 | 'Katie', 246 | 'Johnny', 247 | 'Kayla', 248 | 'Judge', 249 | 'Kelsey', 250 | 'Kane', 251 | 'Khloe', 252 | 'King', 253 | 'Kiki', 254 | 'Kobe', 255 | 'Kira', 256 | 'Koda', 257 | 'Koko', 258 | 'Lenny', 259 | 'Kona', 260 | 'Leo', 261 | 'Lacy', 262 | 'Leroy', 263 | 'Lady', 264 | 'Levi', 265 | 'Layla', 266 | 'Lewis', 267 | 'Leia', 268 | 'Logan', 269 | 'Lena', 270 | 'Loki', 271 | 'Lexi', 272 | 'Louie', 273 | 'Libby', 274 | 'Lucky', 275 | 'Liberty', 276 | 'Luke', 277 | 'Lily', 278 | 'Marley', 279 | 'Lizzy', 280 | 'Marty', 281 | 'Lola', 282 | 'Maverick', 283 | 'London', 284 | 'Max', 285 | 'Lucky', 286 | 'Maximus', 287 | 'Lulu', 288 | 'Mickey', 289 | 'Luna', 290 | 'Miles', 291 | 'Mabel', 292 | 'Milo', 293 | 'Mackenzie', 294 | 'Moe', 295 | 'Macy', 296 | 'Moose', 297 | 'Maddie', 298 | 'Morris', 299 | 'Madison', 300 | 'Murphy', 301 | 'Maggie', 302 | 'Ned', 303 | 'Maisy', 304 | 'Nelson', 305 | 'Mandy', 306 | 'Nero', 307 | 'Marley', 308 | 'Nico', 309 | 'Matilda', 310 | 'Noah', 311 | 'Mattie', 312 | 'Norm', 313 | 'Maya', 314 | 'Oakley', 315 | 'Mia', 316 | 'Odie', 317 | 'Mika', 318 | 'Odin', 319 | 'Mila', 320 | 'Oliver', 321 | 'Miley', 322 | 'Ollie', 323 | 'Millie', 324 | 'Oreo', 325 | 'Mimi', 326 | 'Oscar', 327 | 'Minnie', 328 | 'Otis', 329 | 'Missy', 330 | 'Otto', 331 | 'Misty', 332 | 'Ozzy', 333 | 'Mitzi', 334 | 'Pablo', 335 | 'Mocha', 336 | 'Parker', 337 | 'Molly', 338 | 'Peanut', 339 | 'Morgan', 340 | 'Pepper', 341 | 'Moxie', 342 | 'Petey', 343 | 'Muffin', 344 | 'Porter', 345 | 'Mya', 346 | 'Prince', 347 | 'Nala', 348 | 'Quincy', 349 | 'Nell', 350 | 'Radar', 351 | 'Nellie', 352 | 'Ralph', 353 | 'Nikki', 354 | 'Rambo', 355 | 'Nina', 356 | 'Ranger', 357 | 'Noel', 358 | 'Rascal', 359 | 'Nola', 360 | 'Rebel', 361 | 'Nori', 362 | 'Reese', 363 | 'Olive', 364 | 'Reggie', 365 | 'Olivia', 366 | 'Remy', 367 | 'Oreo', 368 | 'Rex', 369 | 'Paisley', 370 | 'Ricky', 371 | 'Pandora', 372 | 'Rider', 373 | 'Paris', 374 | 'Riley', 375 | 'Peaches', 376 | 'Ringo', 377 | 'Peanut', 378 | 'Rocco', 379 | 'Pearl', 380 | 'Rockwell', 381 | 'Pebbles', 382 | 'Rocky', 383 | 'Penny', 384 | 'Romeo', 385 | 'Pepper', 386 | 'Rosco', 387 | 'Phoebe', 388 | 'Rudy', 389 | 'Piper', 390 | 'Rufus', 391 | 'Pippa', 392 | 'Rusty', 393 | 'Pixie', 394 | 'Sam', 395 | 'Polly', 396 | 'Sammy', 397 | 'Poppy', 398 | 'Samson', 399 | 'Precious', 400 | 'Sarge', 401 | 'Princess', 402 | 'Sawyer', 403 | 'Priscilla', 404 | 'Scooby', 405 | 'Raven', 406 | 'Scooter', 407 | 'Reese', 408 | 'Scout', 409 | 'Riley', 410 | 'Scrappy', 411 | 'Rose', 412 | 'Shadow', 413 | 'Rosie', 414 | 'Shamus', 415 | 'Roxy', 416 | 'Shiloh', 417 | 'Ruby', 418 | 'Simba', 419 | 'Sadie', 420 | 'Simon', 421 | 'Sage', 422 | 'Smoky', 423 | 'Sally', 424 | 'Snoopy', 425 | 'Sam', 426 | 'Sparky', 427 | 'Samantha', 428 | 'Spencer', 429 | 'Sammie', 430 | 'Spike', 431 | 'Sandy', 432 | 'Spot', 433 | 'Sasha', 434 | 'Stanley', 435 | 'Sassy', 436 | 'Stewie', 437 | 'Savannah', 438 | 'Storm', 439 | 'Scarlet', 440 | 'Taco', 441 | 'Shadow', 442 | 'Tank', 443 | 'Sheba', 444 | 'Taz', 445 | 'Shelby', 446 | 'Teddy', 447 | 'Shiloh', 448 | 'Tesla', 449 | 'Sierra', 450 | 'Theo', 451 | 'Sissy', 452 | 'Thor', 453 | 'Sky', 454 | 'Titus', 455 | 'Smokey', 456 | 'TJ', 457 | 'Snickers', 458 | 'Toby', 459 | 'Sophia', 460 | 'Trapper', 461 | 'Sophie', 462 | 'Tripp', 463 | 'Star', 464 | 'Tucker', 465 | 'Stella', 466 | 'Tyler', 467 | 'Sugar', 468 | 'Tyson', 469 | 'Suki', 470 | 'Vince', 471 | 'Summer', 472 | 'Vinnie', 473 | 'Sunny', 474 | 'Wally', 475 | 'Sweetie', 476 | 'Walter', 477 | 'Sydney', 478 | 'Watson', 479 | 'Tasha', 480 | 'Willy', 481 | 'Tessa', 482 | 'Winston', 483 | 'Tilly', 484 | 'Woody', 485 | 'Tootsie', 486 | 'Wrigley', 487 | 'Trixie', 488 | 'Wyatt', 489 | 'Violet', 490 | 'Yogi', 491 | 'Willow', 492 | 'Yoshi', 493 | 'Winnie', 494 | 'Yukon', 495 | 'Xena', 496 | 'Zane', 497 | 'Zelda', 498 | 'Zeus', 499 | 'Zoe', 500 | 'Ziggy' 501 | ] 502 | const NAMES_LEN = NAMES.length 503 | export default function randomName(): string { 504 | return NAMES[Math.floor(Math.random() * NAMES_LEN)] 505 | } 506 | -------------------------------------------------------------------------------- /services/adoptions/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const path = require('path') 3 | const cors = require('cors') 4 | const app = express() 5 | const bodyParser = require('body-parser') 6 | const uuid = require('uuid') 7 | const morgan = require('morgan') 8 | const { Kafka, logLevel } = require('kafkajs') 9 | const { KafkaSink, KafkaLogger, KafkaStream, FlatDB: { queryObjToMatchQuery } } = require('../lib') 10 | 11 | // Configs 12 | const KAFKA_HOSTS = (process.env.KAFKA_HOSTS || 'localhost:9092').split(',').map(s => s.trim()) 13 | const DATA_BASEPATH = process.env.DATA_BASEPATH || __dirname 14 | const CLIENT_ID = 'adoptions' 15 | 16 | // --------------------------------------------------------------- 17 | // Kafka 18 | const kafka = new Kafka({ 19 | logLevel: logLevel.INFO, 20 | logCreator: KafkaLogger, 21 | brokers: KAFKA_HOSTS, 22 | clientId: CLIENT_ID, 23 | retry: { 24 | initialRetryTime: 1000, 25 | retries: 16 26 | } 27 | }) 28 | 29 | const consumers = [] 30 | const producer = kafka.producer() 31 | producer.connect() 32 | 33 | 34 | // Consume kafka 35 | const petCache = new KafkaSink({ 36 | kafka, 37 | basePath: DATA_BASEPATH, 38 | name: 'adoptions-pet-status-cache', 39 | topics: ['pets.statusChanged'], 40 | onLog: ({log, sink}) => { 41 | if(!log.status) { 42 | return 43 | } 44 | console.log(`Cacheing pet status to disk: ${log.id} - ${log.status}`) 45 | sink.db.dbPut(log.id, {status: log.status}) 46 | } 47 | }) 48 | 49 | 50 | const adoptionsCache = new KafkaSink({ 51 | kafka, 52 | basePath: DATA_BASEPATH, 53 | name: 'adoptions-adoptions-requested', 54 | topics: ['adoptions.requested', 'adoptions.statusChanged'], 55 | onLog: async ({log, sink, topic}) => { 56 | 57 | if(topic === 'adoptions.requested') { 58 | // Save to DB 59 | console.log(`Adding adoption - ${log.id} - pets = ${JSON.stringify(log.pets)}`) 60 | sink.db.dbPut(log.id, {...log, status: 'pending'}) 61 | } 62 | 63 | if(topic === 'adoptions.statusChanged') { 64 | const adoption = sink.db.dbGet(log.id) 65 | if(!adoption) { 66 | console.error(`Did not find Adoption with id ${log.id}`) 67 | return 68 | } 69 | 70 | console.log(`Saving status - ${log.id} - ${log.status}`) 71 | sink.db.dbMerge(log.id, {status: log.status}) 72 | return 73 | } 74 | 75 | } 76 | }) 77 | 78 | new KafkaStream({ 79 | kafka, 80 | topics: ['adoptions.requested', 'adoptions.statusChanged'], 81 | name: 'adoptions-stream', 82 | onLog: async ({ log, topic }) => { 83 | 84 | if(topic === 'adoptions.requested') { 85 | // Produce: adoptions.statusChanged 86 | producer.send({ 87 | topic: 'adoptions.statusChanged', 88 | messages: [ 89 | { value: JSON.stringify({id: log.id, status: 'requested'}) }, 90 | ], 91 | }) 92 | return 93 | } 94 | 95 | if(topic === 'adoptions.statusChanged') { 96 | const adoption = adoptionsCache.db.dbGet(log.id) 97 | if(!adoption) { 98 | console.error(`Did not find Adoption with id ${log.id}`) 99 | return 100 | } 101 | console.log(`Processing status change - ${log.id} - ${log.status}`) 102 | await processStatusChange(adoption, log.status) 103 | return 104 | } 105 | } 106 | }) 107 | 108 | // Trigger other events based on status change. And update the status 109 | // requested -> rejected | available 110 | // available -> denied | adopted 111 | // adopted -> END 112 | // rejected -> END 113 | // denied -> END 114 | async function processStatusChange(adoption, status) { 115 | 116 | // requested -> rejected | available 117 | if(status === 'requested') { 118 | 119 | // available 120 | // for each pets, hold them 121 | // update adoption status 122 | 123 | // Hold all pets 124 | const reasons = adoption.pets 125 | .map(petId => ({ id: petId, status: petCache.get(petId).status})) 126 | .filter(({status}) => status !== 'available') 127 | .map(({id, status}) => ({petId: id, message: `${status}`})) 128 | 129 | // Denied 130 | if(reasons.length) { 131 | adoptionsCache.db.dbMerge(adoption.id, {reasons}) 132 | await producer.send({ 133 | topic: 'adoptions.statusChanged', 134 | messages: [ 135 | { value: JSON.stringify({id: adoption.id, status: 'rejected', reasons}) }, 136 | ], 137 | }) 138 | // End - Rejected 139 | return 140 | } 141 | 142 | // Available 143 | const petMessages = adoption.pets.map(petId => ({ 144 | value: JSON.stringify({id: petId, status: 'onhold'}), 145 | })) 146 | 147 | await producer.send({ 148 | topic: 'pets.statusChanged', 149 | messages: petMessages 150 | }) 151 | 152 | await producer.send({ 153 | topic: 'adoptions.statusChanged', 154 | messages: [ 155 | { value: JSON.stringify({id: adoption.id, status: 'available'}) }, 156 | ], 157 | }) 158 | 159 | // End - Available 160 | return 161 | } 162 | 163 | // Adopted -> Claim all the Pets 164 | if(status === 'approved') { 165 | const claimPetMessages = adoption.pets 166 | .map(petId => ({ 167 | value: JSON.stringify({ 168 | id: petId, 169 | status: 'adopted' 170 | }) 171 | })) 172 | 173 | await producer.send({ 174 | topic: 'pets.statusChanged', 175 | messages: claimPetMessages 176 | }) 177 | 178 | return 179 | } 180 | 181 | if(status === 'denied') { 182 | const claimPetMessages = adoption.pets 183 | .map(petId => ({ 184 | value: JSON.stringify({ 185 | id: petId, 186 | status: 'available' 187 | }) 188 | })) 189 | 190 | await producer.send({ 191 | topic: 'pets.statusChanged', 192 | messages: claimPetMessages 193 | }) 194 | 195 | return 196 | } 197 | 198 | 199 | } 200 | 201 | // --------------------------------------------------------------- 202 | // Rest 203 | app.use(morgan('short')) 204 | app.use(cors()) 205 | app.use(bodyParser.json()) 206 | 207 | app.get('/api/adoptions', (req, res) => { 208 | const { location, status } = req.query 209 | 210 | if(!location && !status) { 211 | return res.json(adoptionsCache.db.dbGetAll()) 212 | } 213 | 214 | let query = queryObjToMatchQuery({ status, location }) 215 | return res.json(adoptionsCache.db.dbQuery(query)) 216 | }) 217 | 218 | app.post('/api/adoptions', (req, res) => { 219 | const adoption = req.body 220 | adoption.id = adoption.id || uuid.v4() 221 | 222 | // TODO: Some validation of the body 223 | 224 | producer.send({ 225 | topic: 'adoptions.requested', 226 | messages: [ 227 | { value: JSON.stringify(adoption) }, 228 | ], 229 | }) 230 | 231 | res.status(201).send(adoption) 232 | }) 233 | 234 | app.patch('/api/adoptions/:id', (req, res) => { 235 | const adoption = adoptionsCache.db.dbGet(req.params.id) 236 | const { status } = req.body 237 | if(!adoption) { 238 | console.log('Cannot find adoption ${req.params.id} to patch') 239 | return res.status(400).json({ 240 | message: 'Adoption not found, cannot patch.' 241 | }) 242 | } 243 | 244 | const updatedAdoption = {...adoption, status } 245 | console.log(`Patching ${JSON.stringify(updatedAdoption)}`) 246 | 247 | producer.send({ 248 | topic: 'adoptions.statusChanged', 249 | messages: [ 250 | { value: JSON.stringify(updatedAdoption) }, 251 | ], 252 | }) 253 | 254 | return res.status(200).send(updatedAdoption) 255 | }) 256 | 257 | // // SPA 258 | // app.use(express.static(path.resolve(__dirname, process.env.SPA_PATH || '../web-ui/build'))) 259 | 260 | 261 | // --------------------------------------------------------------------------------------- 262 | // Boring stuff follows... 263 | // --------------------------------------------------------------------------------------- 264 | 265 | // Start server and handle logic around graceful exit 266 | const server = app.listen(process.env.NODE_PORT || 3200, () => { 267 | console.log('Server listening on http://' + server.address().address + ':' + server.address().port) 268 | }) 269 | // Keep track of connections to kill 'em off later. 270 | let connections = [] 271 | server.on('connection', connection => { 272 | connections.push(connection); 273 | connection.on('close', () => connections = connections.filter(curr => curr !== connection)); 274 | }); 275 | 276 | // Exit gracefully 277 | const errorTypes = ['unhandledRejection', 'uncaughtException'] 278 | const signalTraps = ['SIGTERM', 'SIGINT', 'SIGUSR2'] 279 | errorTypes.forEach(type => { 280 | process.on(type, async e => { 281 | try { 282 | console.log(`process.on ${type}`) 283 | console.error(e) 284 | await shutdown() 285 | } catch (_) { 286 | process.exit(1) 287 | } 288 | }) 289 | }) 290 | 291 | 292 | signalTraps.forEach(type => { 293 | process.once(type, async () => { 294 | try { 295 | await shutdown() 296 | } finally { 297 | process.kill(process.pid, type) 298 | } 299 | }) 300 | }) 301 | 302 | 303 | async function shutdown() { 304 | await Promise.all(consumers.map(consumer => consumer.disconnect())) 305 | 306 | server.close(() => { 307 | console.log('Closed out remaining connections'); 308 | process.exit(0); 309 | }); 310 | 311 | setTimeout(() => { 312 | console.error('Could not close connections in time, forcefully shutting down'); 313 | process.exit(1); 314 | }, 5000); 315 | 316 | connections.forEach(curr => curr.end()); 317 | setTimeout(() => connections.forEach(curr => curr.destroy()), 5000); 318 | } 319 | 320 | -------------------------------------------------------------------------------- /services/lib/db.test.js: -------------------------------------------------------------------------------- 1 | /// 2 | const { queryFilter, matchesQuery, queryStrToMatchQuery, queryObjToMatchQuery } = require('./db') 3 | 4 | describe('queries', () => { 5 | 6 | describe('queryObjToMatchQuery', () => { 7 | 8 | it('should convert object to $eqi', async () => { 9 | expect(queryObjToMatchQuery({ 10 | one: 'a', 11 | two: 'b', 12 | })).toEqual({ 13 | one: {$eqi: 'a'}, 14 | two: {$eqi: 'b'}, 15 | }) 16 | }) 17 | 18 | it('should convert arrays into $and', async () => { 19 | expect(queryObjToMatchQuery({ 20 | one: 'a', 21 | two: ['b', 'c'] 22 | })).toEqual({ 23 | one: {$eqi: 'a'}, 24 | two: {$and: [{$eqi: 'b'}, {$eqi: 'c'}]}, 25 | }) 26 | }) 27 | 28 | 29 | it('should not include undefined values', async () => { 30 | expect(queryObjToMatchQuery({ 31 | one: 'a', 32 | two: undefined, 33 | })).toEqual({ 34 | one: {$eqi: 'a'}, 35 | }) 36 | }) 37 | 38 | }) 39 | 40 | describe('queryStrToMatchQuery', () => { 41 | 42 | 43 | it('should convert to $eqi', async () => { 44 | expect( 45 | queryStrToMatchQuery('test') 46 | ).toEqual({$eqi: 'test'}) 47 | 48 | }) 49 | 50 | it('should convert bang to $not', async () => { 51 | expect( 52 | queryStrToMatchQuery('!test') 53 | ).toEqual({ 54 | $not: {$eqi: 'test'} 55 | }) 56 | 57 | }) 58 | 59 | it('should convert & into $and', async () => { 60 | expect( 61 | queryStrToMatchQuery('test&!foo') 62 | ).toEqual({ 63 | $and: [{$eqi: 'test'}, {$not: {$eqi: 'foo'}}] 64 | }) 65 | }) 66 | 67 | 68 | }) 69 | 70 | 71 | describe('queryFilter', () => { 72 | 73 | it('should filter values with scalars', async () => { 74 | // Given 75 | const values = [ 76 | {key: 1,}, 77 | {key: 'a'}, 78 | {key: null}, 79 | {key: 0}, 80 | {key: false}, 81 | ] 82 | 83 | // When/Then 84 | expect(queryFilter(values, {key: 1})).toEqual([ {key: 1} ]) 85 | expect(queryFilter(values, {key: 'a'})).toEqual([ {key: 'a'} ]) 86 | expect(queryFilter(values, {key: null})).toEqual([ {key: null} ]) 87 | expect(queryFilter(values, {key: 0})).toEqual([ {key: 0} ]) 88 | expect(queryFilter(values, {key: false})).toEqual([ {key: false} ]) 89 | }) 90 | 91 | 92 | it('should filter all values matching scalar', async () => { 93 | // Given 94 | const values = [ 95 | {key: 'num', value: 1}, 96 | {key: 'num', value: 2}, 97 | {key: 'num', value: 3}, 98 | {key: 'str', value: 'a'}, 99 | {key: 'str', value: 'b'}, 100 | ] 101 | 102 | // When 103 | const res = queryFilter(values, {key: 'num'}) 104 | 105 | // Then 106 | expect(res).toHaveLength(3) 107 | expect(res[1]).toEqual({key: 'num', value: 2}) 108 | }) 109 | 110 | 111 | it('should always return an array', async () => { 112 | // Given 113 | const values = [ 114 | {key: 'num', value: 1}, 115 | {key: 'num', value: 2}, 116 | {key: 'num', value: 3}, 117 | {key: 'str', value: 'a'}, 118 | {key: 'str', value: 'b'}, 119 | ] 120 | 121 | // When 122 | 123 | // Then 124 | expect(queryFilter(values, {key: 'missing'})).toEqual([]) 125 | expect(queryFilter('', {key: 'missing'})).toEqual([]) 126 | }) 127 | 128 | 129 | it('multiple fields should act as logical AND', async () => { 130 | // Given 131 | const values = [ 132 | {name: 'fido', age: 3}, 133 | {name: 'fido', age: 3}, 134 | {name: 'fido', age: 4}, 135 | {name: 'rex', age: 3}, 136 | ] 137 | 138 | // When 139 | 140 | // Then 141 | expect(queryFilter(values, {name: 'fido', age: 3})).toEqual([{name: 'fido', age: 3}, {name: 'fido', age: 3}]) 142 | }) 143 | 144 | 145 | }) 146 | 147 | describe('matchesQuery', () => { 148 | 149 | 150 | it('empty query returns true', async () => { 151 | 152 | expect(matchesQuery({ 153 | one: '1' 154 | }, {})).toEqual(true) 155 | }) 156 | 157 | it('should support scalars', async () => { 158 | expect(matchesQuery('foo', 'foo')).toBe(true) 159 | expect(matchesQuery('foo', 'bar')).toBe(false) 160 | }) 161 | 162 | 163 | it('should support fields + scalars', async () => { 164 | expect(matchesQuery({key: 'a'}, {key: 'a'})).toEqual(true) 165 | }) 166 | 167 | describe('$eq', () => { 168 | 169 | it('should match values including type', async () => { 170 | // Given 171 | const source = { 172 | key: '1' 173 | } 174 | 175 | // When 176 | expect(matchesQuery({ 177 | key: '1' 178 | }, { 179 | key: {$eq: '1'} 180 | })).toEqual(true) 181 | 182 | 183 | expect(matchesQuery({ 184 | key: '1' 185 | }, { 186 | key: {$eq: 1} 187 | })).toEqual(false) 188 | }) 189 | 190 | 191 | it('should reject if test exists but field does not', async () => { 192 | // When 193 | expect(matchesQuery({ 194 | one: '1' 195 | }, { 196 | two: '2' 197 | })).toEqual(false) 198 | 199 | }) 200 | 201 | it('should match strings with case sensitivity', async () => { 202 | 203 | expect(matchesQuery({ 204 | key: 'Foo' 205 | }, { 206 | key: {$eq: 'Foo'} 207 | })).toEqual(true) 208 | 209 | expect(matchesQuery({ 210 | key: 'Foo' 211 | }, { 212 | key: {$eq: 'foo'} 213 | })).toEqual(false) 214 | 215 | }) 216 | 217 | }) 218 | 219 | describe('$not', () => { 220 | 221 | it('should invert the matches', async () => { 222 | // Given 223 | const source = { 224 | key: '1' 225 | } 226 | 227 | // When 228 | expect(matchesQuery({ 229 | key: '1' 230 | }, { 231 | key: { $not: 'foo' } 232 | })).toEqual(true) 233 | 234 | expect(matchesQuery({ 235 | key: '1' 236 | }, { 237 | key: { $not: '1' } 238 | })).toEqual(false) 239 | }) 240 | 241 | }) 242 | 243 | describe('$eqi', () => { 244 | 245 | it('should match strings case-insensitive', async () => { 246 | 247 | expect(matchesQuery({ 248 | key: 'Foo' 249 | }, { 250 | key: { $eqi: 'foo' } 251 | })).toEqual(true) 252 | 253 | }) 254 | 255 | }) 256 | 257 | describe('$or', () => { 258 | 259 | it('should throw if not an array', async () => { 260 | 261 | expect(() => { 262 | matchesQuery({key: 'one'}, {one: {$or: null}}) 263 | }).toThrowError('$or must be an array of queries') 264 | 265 | }) 266 | 267 | it('should match if at least one clause returns true', async () => { 268 | 269 | expect(matchesQuery({ 270 | key: 'Foo' 271 | }, { 272 | key: { $or: ['foo', 'Foo'] } 273 | })).toEqual(true) 274 | 275 | expect(matchesQuery({ 276 | key: 'Foo' 277 | }, { 278 | key: { $or: ['foo', 'Boo'] } 279 | })).toEqual(false) 280 | 281 | }) 282 | 283 | }) 284 | 285 | describe('$and', () => { 286 | 287 | it('should throw if not an array', async () => { 288 | 289 | expect(() => { 290 | matchesQuery({key: 'one'}, {one: {$and: null}}) 291 | }).toThrowError('$and must be an array of queries') 292 | 293 | }) 294 | 295 | it('should return true if-and-only-if all clauses match', async () => { 296 | 297 | expect(matchesQuery({ 298 | key: 'Foo' 299 | }, { 300 | key: { $and: [{$eqi: 'fOo'}, {$eqi: 'fOO'}] } 301 | })).toEqual(true) 302 | 303 | expect(matchesQuery({ 304 | key: 'Foo' 305 | }, { 306 | key: { $and: [{$eqi: 'fOo'}, {$eqi: 'baz'}] } 307 | })).toEqual(false) 308 | 309 | }) 310 | 311 | }) 312 | 313 | describe('all', () => { 314 | 315 | it('should use logical AND for multiple query fields', async () => { 316 | 317 | // All agree 318 | expect(matchesQuery({ 319 | key: 'Foo' 320 | }, { 321 | key: { $eqi: 'foo', $eq: 'Foo', $not: 'bar' } 322 | })).toEqual(true) 323 | 324 | // With $eqi boo 325 | expect(matchesQuery({ 326 | key: 'Foo' 327 | }, { 328 | key: { $eqi: 'boo', $eq: 'Foo', $not: 'bar' } 329 | })).toEqual(false) 330 | 331 | // With $eq boo 332 | expect(matchesQuery({ 333 | key: 'Foo' 334 | }, { 335 | key: { $eqi: 'foo', $eq: 'boo', $not: 'bar' } 336 | })).toEqual(false) 337 | 338 | // With $not Foo 339 | expect(matchesQuery({ 340 | key: 'Foo' 341 | }, { 342 | key: { $eqi: 'foo', $eq: 'Foo', $not: 'Foo' } 343 | })).toEqual(false) 344 | 345 | }) 346 | 347 | 348 | }) 349 | 350 | describe('child fields', () => { 351 | 352 | it('should test nested objects', async () => { 353 | expect(matchesQuery({ 354 | one: { 355 | foo: 'bar', 356 | bar: 'baz' 357 | } 358 | }, { 359 | one: { 360 | foo: 'bar' 361 | } 362 | })).toBeTruthy() 363 | 364 | expect(matchesQuery({ 365 | one: { 366 | foo: 'bar', 367 | bar: 'baz' 368 | } 369 | }, { 370 | one: { 371 | foo: 'nope' 372 | } 373 | })).toBeFalsy() 374 | 375 | }) 376 | 377 | it('should test deeply nested objects', async () => { 378 | expect(matchesQuery({ 379 | one: { 380 | two: { 381 | three: { 382 | four: '4' 383 | } 384 | } 385 | } 386 | }, { 387 | one: { 388 | two: { 389 | three: { 390 | four: {$eq: '4'} 391 | } 392 | } 393 | } 394 | })).toBeTruthy() 395 | 396 | expect(matchesQuery({ 397 | one: { 398 | two: { 399 | three: { 400 | four: '4' 401 | } 402 | } 403 | } 404 | }, { 405 | one: { 406 | two: { 407 | three: { 408 | four: {$eq: 'nope'} 409 | } 410 | } 411 | } 412 | })).toBeFalsy() 413 | 414 | }) 415 | 416 | it('should allow testing keys with a dollar ', async () => { 417 | expect(matchesQuery({ 418 | $test: 'ok' 419 | }, { 420 | $$test: 'ok' 421 | })).toBeTruthy() 422 | 423 | 424 | expect(matchesQuery({ 425 | $test: 'ok' 426 | }, { 427 | $$test: 'nope' 428 | })).toBeFalsy() 429 | 430 | }) 431 | 432 | 433 | }) 434 | 435 | 436 | }) 437 | 438 | }) 439 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /ReadyAPI-tests/Kafka-Petstore-Adding-Pets-readyapi-project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | An API for tracking **Pets** and **Pet Adoptions** across our event and REST based petstore. This OpenAPI definition describes the RESTful APIs offered by the store. 6 | 7 | Links: 8 | - [The code repository housing the implementation](https://github.com/swagger-api/petstore-kafka) 9 | 10 | Other useful information related to this API: 11 | 12 | 13 | 14 | https://api.swaggerhub.com/apis/SwaggerPMTests/Pets-Adoption-API/1.0.0 15 | {"openapi":"3.0.0","info":{"version":"1.0.0","title":"Pets and Adoption API","description":"An API for tracking **Pets** and **Pet Adoptions** across our event and REST based petstore. This OpenAPI definition describes the RESTful APIs offered by the store.\n\nLinks:\n- [The code repository housing the implementation](https://github.com/swagger-api/petstore-kafka)\n\nOther useful information related to this API:\n","termsOfService":"http://swagger.io/terms/","contact":{"email":"apiteam@swagger.io"},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"}},"tags":[{"name":"pets","description":"Everything about your Pets"},{"name":"adoptions","description":"Adoption information for pets in our stores"}],"paths":{"/pets":{"get":{"tags":["pets"],"summary":"Get a list of pets within the store","description":"A list of pets with information on their status and location","operationId":"getPets","parameters":[{"name":"status","in":"query","description":"Status values that need to be considered for filter","required":false,"explode":true,"schema":{"type":"string","description":"the adoption status of the pet","default":"available","enum":["available","pending","onhold","adopted"]}},{"name":"location","in":"query","description":"the store location of the pet","required":false,"schema":{"type":"string","example":"Galway"}}],"responses":{"200":{"description":"Get Pets response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pets"}}}},"default":{"description":"Get Pets response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pets"}}}}}},"post":{"tags":["pets"],"summary":"Add a new pet to the store","description":"Add a new pet to the store","operationId":"postPet","requestBody":{"description":"Create a new pet in the store","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}}},"required":true},"responses":{"201":{"description":"Pet Created Successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}}}}}}},"/pets/{id}":{"patch":{"tags":["pets"],"summary":"Update the status of a pet","description":"Use this operation to update the adoption status of a pet","operationId":"patchPetStatus","parameters":[{"name":"id","in":"path","description":"The identifier for the path","required":true,"schema":{"type":"string","example":"a76b67cb-7976-4b94-af8e-381688c915ad"}}],"requestBody":{"description":"the new status information","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PetStatus"}}},"required":true},"responses":{"200":{"description":"Pet status updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}}}},"404":{"description":"Pet not found"}}}},"/adoptions":{"get":{"tags":["adoptions"],"summary":"Get a list of current pet adoptions","description":"A list of adoptions with information on their status and pet","operationId":"getAdoptions","parameters":[{"name":"status","in":"query","description":"Status values that need to be considered for filter","required":false,"explode":true,"schema":{"type":"string","description":"the adoption status","enum":["requested","pending","available","denied","approved"]}},{"name":"location","in":"query","description":"the store location of the pet adoption","required":false,"schema":{"type":"string","example":"Plett"}}],"responses":{"200":{"description":"Get Adoptions response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Adoptions"}}}},"default":{"description":"Get Adoptions response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Adoptions"}}}}}},"post":{"tags":["adoptions"],"summary":"Add a new adoption","description":"Add a new pet to the store","operationId":"postAdoption","requestBody":{"description":"Create a new adoption request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewAdoption"}}},"required":true},"responses":{"201":{"description":"Pet Created Successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Adoption"}}}}}}},"/adoptions/{id}":{"patch":{"tags":["adoptions"],"summary":"Update the status of an adoption","description":"Use this operation to update the adoption status of an adoption","operationId":"patchAdoptionStatus","parameters":[{"name":"id","in":"path","description":"The identifier for the path","required":true,"schema":{"type":"string","example":"a76b67cb-7976-4b94-af8e-381688c915ad"}}],"requestBody":{"description":"the new status information","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdoptionStatus"}}},"required":true},"responses":{"200":{"description":"Pet status updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Adoption"}}}},"404":{"description":"Adoption not found"}}}}},"components":{"schemas":{"Pet":{"type":"object","properties":{"name":{"type":"string","description":"the name of the pet","example":"Rover"},"location":{"type":"string","description":"the store location housing the pet","example":"plett"},"id":{"type":"string","description":"a _guid_ identifier of the pet","example":"a76b67cb-7976-4b94-af8e-381688c915ad"},"status":{"type":"string","description":"the adoption status of the pet","default":"available","example":"available","enum":["available","pending","onhold","adopted"]}}},"Pets":{"type":"array","items":{"$ref":"#/components/schemas/Pet"}},"PetStatus":{"type":"object","properties":{"status":{"type":"string","description":"the adoption status of the pet","default":"available","example":"available","enum":["available","pending","onhold","adopted"]}}},"AdoptionReason":{"type":"object","properties":{"petId":{"type":"string","description":"the pet id"},"message":{"type":"string","description":"a custom note providing some additional context about the reason for adopting the pet"}}},"Adoption":{"type":"object","properties":{"id":{"type":"string","description":"a _guid_ identifier of the adoption","example":"a76b67cb-7976-4b94-af8e-381688c915ad"},"status":{"type":"string","description":"the status of the adoption","example":"requested","enum":["requested","pending","available","denied","approved"]},"pets":{"type":"array","items":{"type":"string"}},"reasons":{"type":"array","items":{"$ref":"#/components/schemas/AdoptionReason"}}}},"Adoptions":{"type":"array","items":{"$ref":"#/components/schemas/Adoption"}},"NewAdoption":{"type":"object","properties":{"pets":{"type":"array","description":"the pets to be part of this adoption","items":{"type":"string"}},"location":{"type":"string","description":"the location of the pet adoption"}}},"AdoptionStatus":{"type":"object","properties":{"status":{"type":"string","description":"the adoption status of the pet","example":"approved","enum":["requested","pending","available","denied","approved"]}}}}},"servers":[{"description":"SwaggerHub API Auto Mocking","url":"https://virtserver.swaggerhub.com/SwaggerPMTests/Pets-Adoption-API/1.0.0"},{"description":"Local Docker","url":"http://localhost:80/api"},{"url":"https://petstore-kafka.swagger.io/api"}]} 16 | https://swagger.io/openapiv3/specification 17 | 18 | 19 | 20 | http://localhost:80/api 21 | https://petstore-kafka.swagger.io/api 22 | https://virtserver.swaggerhub.com/SwaggerPMTests/Pets-Adoption-API/1.0.0 23 | 24 | 25 | 26 | 27 | 28 | A list of pets with information on their status and location 29 | 30 | 31 | 32 | status 33 | available 34 | QUERY 35 | available 36 | Status values that need to be considered for filter 37 | 38 | 39 | location 40 | QUERY 41 | the store location of the pet 42 | 43 | 44 | 45 | application/json 46 | 200 47 | 48 | Get Pets response 49 | [ { 50 | "name" : "Rover", 51 | "location" : "plett", 52 | "id" : "a76b67cb-7976-4b94-af8e-381688c915ad", 53 | "status" : "available" 54 | } ] 55 | 56 | 57 | 58 | http://localhost:80/api 59 | 60 | No Authorization 61 | No Authorization 62 | 63 | 64 | 65 | 66 | 67 | Add a new pet to the store 68 | 69 | 70 | 71 | application/json 72 | 201 73 | 74 | Pet Created Successfully 75 | { 76 | "name" : "Rover", 77 | "location" : "plett", 78 | "id" : "a76b67cb-7976-4b94-af8e-381688c915ad", 79 | "status" : "available" 80 | } 81 | 82 | 83 | application/json 84 | 85 | { 86 | "name" : "Rover", 87 | "location" : "plett", 88 | "id" : "a76b67cb-7976-4b94-af8e-381688c915ad", 89 | "status" : "available" 90 | } 91 | 92 | 93 | 94 | http://localhost:80/api 95 | {\r 96 | "name" : "Rover",\r 97 | "location" : "plett",\r 98 | "id" : "a76b67cb-7976-4b94-af8e-381688c915ad",\r 99 | "status" : "available"\r 100 | } 101 | 102 | No Authorization 103 | No Authorization 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | Use this operation to update the adoption status of a pet 114 | 115 | 116 | 117 | id 118 | TEMPLATE 119 | The identifier for the path 120 | 121 | 122 | 123 | application/json 124 | 200 125 | 126 | Pet status updated successfully 127 | { 128 | "name" : "Rover", 129 | "location" : "plett", 130 | "id" : "a76b67cb-7976-4b94-af8e-381688c915ad", 131 | "status" : "available" 132 | } 133 | 134 | 135 | application/json 136 | 137 | { 138 | "status" : "available" 139 | } 140 | 141 | 142 | 143 | http://localhost:80/api 144 | {\r 145 | "status" : "available"\r 146 | } 147 | 148 | No Authorization 149 | No Authorization 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | A list of adoptions with information on their status and pet 160 | 161 | 162 | 163 | status 164 | QUERY 165 | Status values that need to be considered for filter 166 | 167 | 168 | location 169 | QUERY 170 | the store location of the pet adoption 171 | 172 | 173 | 174 | application/json 175 | 200 176 | 177 | Get Adoptions response 178 | [ { 179 | "id" : "a76b67cb-7976-4b94-af8e-381688c915ad", 180 | "status" : "requested", 181 | "pets" : [ "string" ], 182 | "reasons" : [ { 183 | "petId" : "string", 184 | "message" : "string" 185 | } ] 186 | } ] 187 | 188 | 189 | 190 | http://localhost:80/api 191 | 192 | No Authorization 193 | No Authorization 194 | 195 | 196 | 197 | 198 | 199 | Add a new pet to the store 200 | 201 | 202 | 203 | application/json 204 | 201 205 | 206 | Pet Created Successfully 207 | { 208 | "id" : "a76b67cb-7976-4b94-af8e-381688c915ad", 209 | "status" : "requested", 210 | "pets" : [ "string" ], 211 | "reasons" : [ { 212 | "petId" : "string", 213 | "message" : "string" 214 | } ] 215 | } 216 | 217 | 218 | application/json 219 | 220 | { 221 | "pets" : [ "string" ], 222 | "location" : "string" 223 | } 224 | 225 | 226 | 227 | http://localhost:80/api 228 | {\r 229 | "pets" : [ "string" ],\r 230 | "location" : "string"\r 231 | } 232 | 233 | No Authorization 234 | No Authorization 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | Use this operation to update the adoption status of an adoption 245 | 246 | 247 | 248 | id 249 | TEMPLATE 250 | The identifier for the path 251 | 252 | 253 | 254 | application/json 255 | 200 256 | 257 | Pet status updated successfully 258 | { 259 | "id" : "a76b67cb-7976-4b94-af8e-381688c915ad", 260 | "status" : "requested", 261 | "pets" : [ "string" ], 262 | "reasons" : [ { 263 | "petId" : "string", 264 | "message" : "string" 265 | } ] 266 | } 267 | 268 | 269 | application/json 270 | 271 | { 272 | "status" : "approved" 273 | } 274 | 275 | 276 | 277 | http://localhost:80/api 278 | {\r 279 | "status" : "approved"\r 280 | } 281 | 282 | No Authorization 283 | No Authorization 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | A simple demo API based on a real implementation. 292 | This is part of a stack. 293 | 294 | 295 | 296 | file:/C:/Users/frank.kilcommins/Downloads/SwaggerPMTests-petstore-kafka-pets-1.0.0-swagger.yaml 297 | asyncapi: '2.4.0' 298 | info: 299 | title: Petstore Kafka - Pets 300 | version: '1.0.0' 301 | description: | 302 | A simple demo API based on a real implementation. 303 | This is part of a stack. 304 | license: 305 | name: Apache 2.0 306 | url: https://www.apache.org/licenses/LICENSE-2.0 307 | 308 | servers: 309 | local: 310 | url: localhost:9092 311 | protocol: kafka 312 | description: When running the dev stack. 313 | compose: 314 | url: kafka:9092 315 | protocol: kafka 316 | description: When running within docker-compose 317 | 318 | defaultContentType: application/json 319 | 320 | channels: 321 | pets.added: 322 | description: Pets that are added 323 | publish: 324 | summary: New Pet added. 325 | operationId: addPet 326 | traits: 327 | - $ref: '#/components/operationTraits/kafka' 328 | message: 329 | description: New Pets 330 | payload: 331 | $ref: "https://api.swaggerhub.com/domains/SwaggerPMTests/petstore-common/1.0.0#/components/schemas/NewPet" 332 | 333 | pets.statusChanged: 334 | description: | 335 | Cache pet statuses to know if an adoption is possible. 336 | subscribe: 337 | summary: Pet's status has changed. 338 | operationId: cachePetStatus 339 | traits: 340 | - $ref: '#/components/operationTraits/kafka' 341 | message: 342 | description: New Pets 343 | payload: 344 | $ref: "https://api.swaggerhub.com/domains/SwaggerPMTests/petstore-common/1.0.0#/components/schemas/PetStatusChange" 345 | 346 | components: 347 | operationTraits: 348 | kafka: 349 | bindings: 350 | kafka: 351 | clientId: 352 | type: string 353 | https://www.asyncapi.com/docs/specifications/2.0.0 354 | 355 | 356 | 357 | kafka.local:9092 358 | localhost:9092 359 | 360 | 361 | Pets that are added 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | Cache pet statuses to know if an adoption is possible. 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | PARALLELL 384 | 385 | 386 | 387 | 388 | 389 | 390 | 5344f0a9-6450-44ca-95c6-6a2f47719827 391 | Subscribe 392 | pets.added 393 | Petstore Kafka - Pets 394 | kafka.local:9092 395 | 396 | 397 | JSON 398 | ANY_CONDITION 399 | 5 400 | 1 401 | 4 402 | 403 | 404 | Received Data 405 | 406 | $['name'] 407 | name 408 | 1 409 | true 410 | TobyDemo 411 | true 412 | false 413 | 1 414 | 415 | 416 | $['location'] 417 | location 418 | 1 419 | true 420 | Galway 421 | true 422 | false 423 | 1 424 | 425 | 426 | $['status'] 427 | status 428 | 1 429 | true 430 | available 431 | true 432 | false 433 | 1 434 | 435 | 436 | $['id'] 437 | id 438 | 9 439 | true 440 | 83c3a968-97a6-45df-86c8-ec9b0e5591fa 441 | true 442 | false 443 | 1 444 | 445 | {"name":"TobyDemo","location":"Galway","status":"available","id":"83c3a968-97a6-45df-86c8-ec9b0e5591fa"} 446 | 447 | 448 | true 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 200 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | <xml-fragment/> 469 | 470 | http://localhost:80/api 471 | { 472 | "name" : "TobyDemo", 473 | "location" : "Galway", 474 | "status" : "available" 475 | } 476 | http://localhost/api/pets 477 | 478 | 479 | 480 | 201 481 | 482 | 483 | 484 | 485 | 200 486 | 487 | 488 | 489 | No Authorization 490 | No Authorization 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | // Sample event script to add custom HTTP header to all outgoing REST, SOAP and HTTP(S) calls 520 | // This code is often used for adding custom authentication to ReadyAPI functional tests 521 | 522 | // If hardcoding the token, uncomment and change line 5 523 | // token = '4567' 524 | 525 | // If your token is parameterized in Project level custom property, uncomment line 8 526 | // token = request.parent.testCase.testSuite.project.getProperty('auth_token').getValue() 527 | 528 | // To modify all outgoing calls, remove comments from lines 11 to 16 529 | // headers = request.requestHeaders 530 | // if (headers.containsKey('auth_token2') == false) { 531 | // headers.put('auth_token2', token) 532 | // request.requestHeaders = headers 533 | // } 534 | 535 | 536 | // Save all test step results into files 537 | // Change the directory path in line 5 to a location where you want to store details 538 | // then uncomment lines 5 to 10 539 | 540 | // filePath = 'C:\\tempOutputDirectory\\' 541 | // fos = new java.io.FileOutputStream(filePath + testStepResult.testStep.label + '.txt', true) 542 | // pw = new java.io.PrintWriter(fos) 543 | // testStepResult.writeTo(pw) 544 | // pw.close() 545 | // fos.close() 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | No Authorization 556 | 557 | ANY_CONDITION 558 | 60 559 | 50 560 | 60 561 | false 562 | 563 | 564 | 565 | -------------------------------------------------------------------------------- /services/pets/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@sindresorhus/is@^0.14.0": 6 | version "0.14.0" 7 | resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz" 8 | integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== 9 | 10 | "@szmarczak/http-timer@^1.1.2": 11 | version "1.1.2" 12 | resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz" 13 | integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== 14 | dependencies: 15 | defer-to-connect "^1.0.1" 16 | 17 | abbrev@1: 18 | version "1.1.1" 19 | resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" 20 | integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== 21 | 22 | accepts@~1.3.8: 23 | version "1.3.8" 24 | resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" 25 | integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== 26 | dependencies: 27 | mime-types "~2.1.34" 28 | negotiator "0.6.3" 29 | 30 | ansi-align@^3.0.0: 31 | version "3.0.1" 32 | resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz" 33 | integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== 34 | dependencies: 35 | string-width "^4.1.0" 36 | 37 | ansi-regex@^5.0.1: 38 | version "5.0.1" 39 | resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" 40 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 41 | 42 | ansi-styles@^4.0.0, ansi-styles@^4.1.0: 43 | version "4.3.0" 44 | resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" 45 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 46 | dependencies: 47 | color-convert "^2.0.1" 48 | 49 | anymatch@~3.1.2: 50 | version "3.1.2" 51 | resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz" 52 | integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== 53 | dependencies: 54 | normalize-path "^3.0.0" 55 | picomatch "^2.0.4" 56 | 57 | array-flatten@1.1.1: 58 | version "1.1.1" 59 | resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" 60 | integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== 61 | 62 | balanced-match@^1.0.0: 63 | version "1.0.2" 64 | resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" 65 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 66 | 67 | basic-auth@~2.0.1: 68 | version "2.0.1" 69 | resolved "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz" 70 | integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== 71 | dependencies: 72 | safe-buffer "5.1.2" 73 | 74 | binary-extensions@^2.0.0: 75 | version "2.2.0" 76 | resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" 77 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 78 | 79 | body-parser@1.20.0, body-parser@^1.20.0: 80 | version "1.20.0" 81 | resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz" 82 | integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== 83 | dependencies: 84 | bytes "3.1.2" 85 | content-type "~1.0.4" 86 | debug "2.6.9" 87 | depd "2.0.0" 88 | destroy "1.2.0" 89 | http-errors "2.0.0" 90 | iconv-lite "0.4.24" 91 | on-finished "2.4.1" 92 | qs "6.10.3" 93 | raw-body "2.5.1" 94 | type-is "~1.6.18" 95 | unpipe "1.0.0" 96 | 97 | boxen@^5.0.0: 98 | version "5.1.2" 99 | resolved "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz" 100 | integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== 101 | dependencies: 102 | ansi-align "^3.0.0" 103 | camelcase "^6.2.0" 104 | chalk "^4.1.0" 105 | cli-boxes "^2.2.1" 106 | string-width "^4.2.2" 107 | type-fest "^0.20.2" 108 | widest-line "^3.1.0" 109 | wrap-ansi "^7.0.0" 110 | 111 | brace-expansion@^1.1.7: 112 | version "1.1.11" 113 | resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" 114 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 115 | dependencies: 116 | balanced-match "^1.0.0" 117 | concat-map "0.0.1" 118 | 119 | braces@~3.0.2: 120 | version "3.0.2" 121 | resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" 122 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 123 | dependencies: 124 | fill-range "^7.0.1" 125 | 126 | bytes@3.1.2: 127 | version "3.1.2" 128 | resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" 129 | integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== 130 | 131 | cacheable-request@^6.0.0: 132 | version "6.1.0" 133 | resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz" 134 | integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== 135 | dependencies: 136 | clone-response "^1.0.2" 137 | get-stream "^5.1.0" 138 | http-cache-semantics "^4.0.0" 139 | keyv "^3.0.0" 140 | lowercase-keys "^2.0.0" 141 | normalize-url "^4.1.0" 142 | responselike "^1.0.2" 143 | 144 | call-bind@^1.0.0: 145 | version "1.0.2" 146 | resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" 147 | integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== 148 | dependencies: 149 | function-bind "^1.1.1" 150 | get-intrinsic "^1.0.2" 151 | 152 | camelcase@^6.2.0: 153 | version "6.3.0" 154 | resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" 155 | integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== 156 | 157 | chalk@^4.1.0: 158 | version "4.1.2" 159 | resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" 160 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 161 | dependencies: 162 | ansi-styles "^4.1.0" 163 | supports-color "^7.1.0" 164 | 165 | chokidar@^3.5.2: 166 | version "3.5.3" 167 | resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" 168 | integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== 169 | dependencies: 170 | anymatch "~3.1.2" 171 | braces "~3.0.2" 172 | glob-parent "~5.1.2" 173 | is-binary-path "~2.1.0" 174 | is-glob "~4.0.1" 175 | normalize-path "~3.0.0" 176 | readdirp "~3.6.0" 177 | optionalDependencies: 178 | fsevents "~2.3.2" 179 | 180 | ci-info@^2.0.0: 181 | version "2.0.0" 182 | resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" 183 | integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== 184 | 185 | cli-boxes@^2.2.1: 186 | version "2.2.1" 187 | resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz" 188 | integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== 189 | 190 | clone-response@^1.0.2: 191 | version "1.0.2" 192 | resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz" 193 | integrity sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q== 194 | dependencies: 195 | mimic-response "^1.0.0" 196 | 197 | color-convert@^2.0.1: 198 | version "2.0.1" 199 | resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" 200 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 201 | dependencies: 202 | color-name "~1.1.4" 203 | 204 | color-name@~1.1.4: 205 | version "1.1.4" 206 | resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" 207 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 208 | 209 | concat-map@0.0.1: 210 | version "0.0.1" 211 | resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" 212 | integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 213 | 214 | configstore@^5.0.1: 215 | version "5.0.1" 216 | resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz" 217 | integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== 218 | dependencies: 219 | dot-prop "^5.2.0" 220 | graceful-fs "^4.1.2" 221 | make-dir "^3.0.0" 222 | unique-string "^2.0.0" 223 | write-file-atomic "^3.0.0" 224 | xdg-basedir "^4.0.0" 225 | 226 | content-disposition@0.5.4: 227 | version "0.5.4" 228 | resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" 229 | integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== 230 | dependencies: 231 | safe-buffer "5.2.1" 232 | 233 | content-type@~1.0.4: 234 | version "1.0.4" 235 | resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" 236 | integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== 237 | 238 | cookie-signature@1.0.6: 239 | version "1.0.6" 240 | resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" 241 | integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== 242 | 243 | cookie@0.5.0: 244 | version "0.5.0" 245 | resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" 246 | integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== 247 | 248 | cors@^2.8.5: 249 | version "2.8.5" 250 | resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" 251 | integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== 252 | dependencies: 253 | object-assign "^4" 254 | vary "^1" 255 | 256 | crypto-random-string@^2.0.0: 257 | version "2.0.0" 258 | resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz" 259 | integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== 260 | 261 | debug@2.6.9: 262 | version "2.6.9" 263 | resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" 264 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 265 | dependencies: 266 | ms "2.0.0" 267 | 268 | debug@^3.2.7: 269 | version "3.2.7" 270 | resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" 271 | integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== 272 | dependencies: 273 | ms "^2.1.1" 274 | 275 | decompress-response@^3.3.0: 276 | version "3.3.0" 277 | resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz" 278 | integrity sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA== 279 | dependencies: 280 | mimic-response "^1.0.0" 281 | 282 | deep-extend@^0.6.0: 283 | version "0.6.0" 284 | resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" 285 | integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== 286 | 287 | defer-to-connect@^1.0.1: 288 | version "1.1.3" 289 | resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz" 290 | integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== 291 | 292 | depd@2.0.0, depd@~2.0.0: 293 | version "2.0.0" 294 | resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" 295 | integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== 296 | 297 | destroy@1.2.0: 298 | version "1.2.0" 299 | resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" 300 | integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== 301 | 302 | dot-prop@^5.2.0: 303 | version "5.3.0" 304 | resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz" 305 | integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== 306 | dependencies: 307 | is-obj "^2.0.0" 308 | 309 | duplexer3@^0.1.4: 310 | version "0.1.4" 311 | resolved "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz" 312 | integrity sha512-CEj8FwwNA4cVH2uFCoHUrmojhYh1vmCdOaneKJXwkeY1i9jnlslVo9dx+hQ5Hl9GnH/Bwy/IjxAyOePyPKYnzA== 313 | 314 | ee-first@1.1.1: 315 | version "1.1.1" 316 | resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" 317 | integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== 318 | 319 | emoji-regex@^8.0.0: 320 | version "8.0.0" 321 | resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" 322 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== 323 | 324 | encodeurl@~1.0.2: 325 | version "1.0.2" 326 | resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" 327 | integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== 328 | 329 | end-of-stream@^1.1.0: 330 | version "1.4.4" 331 | resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" 332 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== 333 | dependencies: 334 | once "^1.4.0" 335 | 336 | escape-goat@^2.0.0: 337 | version "2.1.1" 338 | resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz" 339 | integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== 340 | 341 | escape-html@~1.0.3: 342 | version "1.0.3" 343 | resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" 344 | integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== 345 | 346 | etag@~1.8.1: 347 | version "1.8.1" 348 | resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" 349 | integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== 350 | 351 | express@^4.18.1: 352 | version "4.18.1" 353 | resolved "https://registry.npmjs.org/express/-/express-4.18.1.tgz" 354 | integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== 355 | dependencies: 356 | accepts "~1.3.8" 357 | array-flatten "1.1.1" 358 | body-parser "1.20.0" 359 | content-disposition "0.5.4" 360 | content-type "~1.0.4" 361 | cookie "0.5.0" 362 | cookie-signature "1.0.6" 363 | debug "2.6.9" 364 | depd "2.0.0" 365 | encodeurl "~1.0.2" 366 | escape-html "~1.0.3" 367 | etag "~1.8.1" 368 | finalhandler "1.2.0" 369 | fresh "0.5.2" 370 | http-errors "2.0.0" 371 | merge-descriptors "1.0.1" 372 | methods "~1.1.2" 373 | on-finished "2.4.1" 374 | parseurl "~1.3.3" 375 | path-to-regexp "0.1.7" 376 | proxy-addr "~2.0.7" 377 | qs "6.10.3" 378 | range-parser "~1.2.1" 379 | safe-buffer "5.2.1" 380 | send "0.18.0" 381 | serve-static "1.15.0" 382 | setprototypeof "1.2.0" 383 | statuses "2.0.1" 384 | type-is "~1.6.18" 385 | utils-merge "1.0.1" 386 | vary "~1.1.2" 387 | 388 | fill-range@^7.0.1: 389 | version "7.0.1" 390 | resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" 391 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 392 | dependencies: 393 | to-regex-range "^5.0.1" 394 | 395 | finalhandler@1.2.0: 396 | version "1.2.0" 397 | resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz" 398 | integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== 399 | dependencies: 400 | debug "2.6.9" 401 | encodeurl "~1.0.2" 402 | escape-html "~1.0.3" 403 | on-finished "2.4.1" 404 | parseurl "~1.3.3" 405 | statuses "2.0.1" 406 | unpipe "~1.0.0" 407 | 408 | forwarded@0.2.0: 409 | version "0.2.0" 410 | resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" 411 | integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== 412 | 413 | fresh@0.5.2: 414 | version "0.5.2" 415 | resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" 416 | integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== 417 | 418 | fsevents@~2.3.2: 419 | version "2.3.2" 420 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 421 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 422 | 423 | function-bind@^1.1.1: 424 | version "1.1.1" 425 | resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" 426 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 427 | 428 | get-intrinsic@^1.0.2: 429 | version "1.1.1" 430 | resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz" 431 | integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== 432 | dependencies: 433 | function-bind "^1.1.1" 434 | has "^1.0.3" 435 | has-symbols "^1.0.1" 436 | 437 | get-stream@^4.1.0: 438 | version "4.1.0" 439 | resolved "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz" 440 | integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== 441 | dependencies: 442 | pump "^3.0.0" 443 | 444 | get-stream@^5.1.0: 445 | version "5.2.0" 446 | resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz" 447 | integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== 448 | dependencies: 449 | pump "^3.0.0" 450 | 451 | glob-parent@~5.1.2: 452 | version "5.1.2" 453 | resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" 454 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 455 | dependencies: 456 | is-glob "^4.0.1" 457 | 458 | global-dirs@^3.0.0: 459 | version "3.0.0" 460 | resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz" 461 | integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA== 462 | dependencies: 463 | ini "2.0.0" 464 | 465 | got@^9.6.0: 466 | version "9.6.0" 467 | resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz" 468 | integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== 469 | dependencies: 470 | "@sindresorhus/is" "^0.14.0" 471 | "@szmarczak/http-timer" "^1.1.2" 472 | cacheable-request "^6.0.0" 473 | decompress-response "^3.3.0" 474 | duplexer3 "^0.1.4" 475 | get-stream "^4.1.0" 476 | lowercase-keys "^1.0.1" 477 | mimic-response "^1.0.1" 478 | p-cancelable "^1.0.0" 479 | to-readable-stream "^1.0.0" 480 | url-parse-lax "^3.0.0" 481 | 482 | graceful-fs@^4.1.2: 483 | version "4.2.10" 484 | resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" 485 | integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== 486 | 487 | has-flag@^3.0.0: 488 | version "3.0.0" 489 | resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" 490 | integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== 491 | 492 | has-flag@^4.0.0: 493 | version "4.0.0" 494 | resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" 495 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 496 | 497 | has-symbols@^1.0.1: 498 | version "1.0.3" 499 | resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" 500 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== 501 | 502 | has-yarn@^2.1.0: 503 | version "2.1.0" 504 | resolved "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz" 505 | integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== 506 | 507 | has@^1.0.3: 508 | version "1.0.3" 509 | resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" 510 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 511 | dependencies: 512 | function-bind "^1.1.1" 513 | 514 | http-cache-semantics@^4.0.0: 515 | version "4.1.0" 516 | resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz" 517 | integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== 518 | 519 | http-errors@2.0.0: 520 | version "2.0.0" 521 | resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" 522 | integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== 523 | dependencies: 524 | depd "2.0.0" 525 | inherits "2.0.4" 526 | setprototypeof "1.2.0" 527 | statuses "2.0.1" 528 | toidentifier "1.0.1" 529 | 530 | iconv-lite@0.4.24: 531 | version "0.4.24" 532 | resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" 533 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 534 | dependencies: 535 | safer-buffer ">= 2.1.2 < 3" 536 | 537 | ignore-by-default@^1.0.1: 538 | version "1.0.1" 539 | resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz" 540 | integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== 541 | 542 | import-lazy@^2.1.0: 543 | version "2.1.0" 544 | resolved "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz" 545 | integrity sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A== 546 | 547 | imurmurhash@^0.1.4: 548 | version "0.1.4" 549 | resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" 550 | integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== 551 | 552 | inherits@2.0.4: 553 | version "2.0.4" 554 | resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" 555 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 556 | 557 | ini@2.0.0: 558 | version "2.0.0" 559 | resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" 560 | integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== 561 | 562 | ini@~1.3.0: 563 | version "1.3.8" 564 | resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" 565 | integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== 566 | 567 | ipaddr.js@1.9.1: 568 | version "1.9.1" 569 | resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" 570 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 571 | 572 | is-binary-path@~2.1.0: 573 | version "2.1.0" 574 | resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" 575 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 576 | dependencies: 577 | binary-extensions "^2.0.0" 578 | 579 | is-ci@^2.0.0: 580 | version "2.0.0" 581 | resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz" 582 | integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== 583 | dependencies: 584 | ci-info "^2.0.0" 585 | 586 | is-extglob@^2.1.1: 587 | version "2.1.1" 588 | resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" 589 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 590 | 591 | is-fullwidth-code-point@^3.0.0: 592 | version "3.0.0" 593 | resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" 594 | integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== 595 | 596 | is-glob@^4.0.1, is-glob@~4.0.1: 597 | version "4.0.3" 598 | resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" 599 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 600 | dependencies: 601 | is-extglob "^2.1.1" 602 | 603 | is-installed-globally@^0.4.0: 604 | version "0.4.0" 605 | resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz" 606 | integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== 607 | dependencies: 608 | global-dirs "^3.0.0" 609 | is-path-inside "^3.0.2" 610 | 611 | is-npm@^5.0.0: 612 | version "5.0.0" 613 | resolved "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz" 614 | integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA== 615 | 616 | is-number@^7.0.0: 617 | version "7.0.0" 618 | resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" 619 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 620 | 621 | is-obj@^2.0.0: 622 | version "2.0.0" 623 | resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz" 624 | integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== 625 | 626 | is-path-inside@^3.0.2: 627 | version "3.0.3" 628 | resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" 629 | integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== 630 | 631 | is-typedarray@^1.0.0: 632 | version "1.0.0" 633 | resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" 634 | integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== 635 | 636 | is-yarn-global@^0.3.0: 637 | version "0.3.0" 638 | resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz" 639 | integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== 640 | 641 | json-buffer@3.0.0: 642 | version "3.0.0" 643 | resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz" 644 | integrity sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ== 645 | 646 | kafkajs@^2.0.2: 647 | version "2.0.2" 648 | resolved "https://registry.npmjs.org/kafkajs/-/kafkajs-2.0.2.tgz" 649 | integrity sha512-g6CM3fAenofOjR1bfOAqeZUEaSGhNtBscNokybSdW1rmIKYNwBPC9xQzwulFJm36u/xcxXUiCl/L/qfslapihA== 650 | 651 | keyv@^3.0.0: 652 | version "3.1.0" 653 | resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz" 654 | integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== 655 | dependencies: 656 | json-buffer "3.0.0" 657 | 658 | latest-version@^5.1.0: 659 | version "5.1.0" 660 | resolved "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz" 661 | integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== 662 | dependencies: 663 | package-json "^6.3.0" 664 | 665 | lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: 666 | version "1.0.1" 667 | resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz" 668 | integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== 669 | 670 | lowercase-keys@^2.0.0: 671 | version "2.0.0" 672 | resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz" 673 | integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== 674 | 675 | lru-cache@^6.0.0: 676 | version "6.0.0" 677 | resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" 678 | integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== 679 | dependencies: 680 | yallist "^4.0.0" 681 | 682 | make-dir@^3.0.0: 683 | version "3.1.0" 684 | resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" 685 | integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== 686 | dependencies: 687 | semver "^6.0.0" 688 | 689 | media-typer@0.3.0: 690 | version "0.3.0" 691 | resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" 692 | integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== 693 | 694 | merge-descriptors@1.0.1: 695 | version "1.0.1" 696 | resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" 697 | integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== 698 | 699 | methods@~1.1.2: 700 | version "1.1.2" 701 | resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" 702 | integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== 703 | 704 | mime-db@1.52.0: 705 | version "1.52.0" 706 | resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" 707 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 708 | 709 | mime-types@~2.1.24, mime-types@~2.1.34: 710 | version "2.1.35" 711 | resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" 712 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 713 | dependencies: 714 | mime-db "1.52.0" 715 | 716 | mime@1.6.0: 717 | version "1.6.0" 718 | resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" 719 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 720 | 721 | mimic-response@^1.0.0, mimic-response@^1.0.1: 722 | version "1.0.1" 723 | resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz" 724 | integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== 725 | 726 | minimatch@^3.0.4: 727 | version "3.1.2" 728 | resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" 729 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 730 | dependencies: 731 | brace-expansion "^1.1.7" 732 | 733 | minimist@^1.2.0: 734 | version "1.2.6" 735 | resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz" 736 | integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== 737 | 738 | morgan@^1.10.0: 739 | version "1.10.0" 740 | resolved "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz" 741 | integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== 742 | dependencies: 743 | basic-auth "~2.0.1" 744 | debug "2.6.9" 745 | depd "~2.0.0" 746 | on-finished "~2.3.0" 747 | on-headers "~1.0.2" 748 | 749 | ms@2.0.0: 750 | version "2.0.0" 751 | resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" 752 | integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== 753 | 754 | ms@2.1.3, ms@^2.1.1: 755 | version "2.1.3" 756 | resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" 757 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 758 | 759 | negotiator@0.6.3: 760 | version "0.6.3" 761 | resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" 762 | integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== 763 | 764 | nodemon@^2.0.16: 765 | version "2.0.16" 766 | resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.16.tgz" 767 | integrity sha512-zsrcaOfTWRuUzBn3P44RDliLlp263Z/76FPoHFr3cFFkOz0lTPAcIw8dCzfdVIx/t3AtDYCZRCDkoCojJqaG3w== 768 | dependencies: 769 | chokidar "^3.5.2" 770 | debug "^3.2.7" 771 | ignore-by-default "^1.0.1" 772 | minimatch "^3.0.4" 773 | pstree.remy "^1.1.8" 774 | semver "^5.7.1" 775 | supports-color "^5.5.0" 776 | touch "^3.1.0" 777 | undefsafe "^2.0.5" 778 | update-notifier "^5.1.0" 779 | 780 | nopt@~1.0.10: 781 | version "1.0.10" 782 | resolved "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz" 783 | integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= 784 | dependencies: 785 | abbrev "1" 786 | 787 | normalize-path@^3.0.0, normalize-path@~3.0.0: 788 | version "3.0.0" 789 | resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" 790 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 791 | 792 | normalize-url@^4.1.0: 793 | version "4.5.1" 794 | resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz" 795 | integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== 796 | 797 | object-assign@^4: 798 | version "4.1.1" 799 | resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" 800 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 801 | 802 | object-inspect@^1.9.0: 803 | version "1.12.2" 804 | resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz" 805 | integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== 806 | 807 | on-finished@2.4.1: 808 | version "2.4.1" 809 | resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" 810 | integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== 811 | dependencies: 812 | ee-first "1.1.1" 813 | 814 | on-finished@~2.3.0: 815 | version "2.3.0" 816 | resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" 817 | integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= 818 | dependencies: 819 | ee-first "1.1.1" 820 | 821 | on-headers@~1.0.2: 822 | version "1.0.2" 823 | resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" 824 | integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== 825 | 826 | once@^1.3.1, once@^1.4.0: 827 | version "1.4.0" 828 | resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" 829 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 830 | dependencies: 831 | wrappy "1" 832 | 833 | p-cancelable@^1.0.0: 834 | version "1.1.0" 835 | resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz" 836 | integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== 837 | 838 | package-json@^6.3.0: 839 | version "6.5.0" 840 | resolved "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz" 841 | integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== 842 | dependencies: 843 | got "^9.6.0" 844 | registry-auth-token "^4.0.0" 845 | registry-url "^5.0.0" 846 | semver "^6.2.0" 847 | 848 | parseurl@~1.3.3: 849 | version "1.3.3" 850 | resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" 851 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 852 | 853 | path-to-regexp@0.1.7: 854 | version "0.1.7" 855 | resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" 856 | integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= 857 | 858 | picomatch@^2.0.4, picomatch@^2.2.1: 859 | version "2.3.1" 860 | resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" 861 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 862 | 863 | prepend-http@^2.0.0: 864 | version "2.0.0" 865 | resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz" 866 | integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= 867 | 868 | proxy-addr@~2.0.7: 869 | version "2.0.7" 870 | resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" 871 | integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== 872 | dependencies: 873 | forwarded "0.2.0" 874 | ipaddr.js "1.9.1" 875 | 876 | pstree.remy@^1.1.8: 877 | version "1.1.8" 878 | resolved "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz" 879 | integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== 880 | 881 | pump@^3.0.0: 882 | version "3.0.0" 883 | resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz" 884 | integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== 885 | dependencies: 886 | end-of-stream "^1.1.0" 887 | once "^1.3.1" 888 | 889 | pupa@^2.1.1: 890 | version "2.1.1" 891 | resolved "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz" 892 | integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== 893 | dependencies: 894 | escape-goat "^2.0.0" 895 | 896 | qs@6.10.3: 897 | version "6.10.3" 898 | resolved "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz" 899 | integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== 900 | dependencies: 901 | side-channel "^1.0.4" 902 | 903 | range-parser@~1.2.1: 904 | version "1.2.1" 905 | resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" 906 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 907 | 908 | raw-body@2.5.1: 909 | version "2.5.1" 910 | resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz" 911 | integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== 912 | dependencies: 913 | bytes "3.1.2" 914 | http-errors "2.0.0" 915 | iconv-lite "0.4.24" 916 | unpipe "1.0.0" 917 | 918 | rc@^1.2.8: 919 | version "1.2.8" 920 | resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" 921 | integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== 922 | dependencies: 923 | deep-extend "^0.6.0" 924 | ini "~1.3.0" 925 | minimist "^1.2.0" 926 | strip-json-comments "~2.0.1" 927 | 928 | readdirp@~3.6.0: 929 | version "3.6.0" 930 | resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" 931 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== 932 | dependencies: 933 | picomatch "^2.2.1" 934 | 935 | registry-auth-token@^4.0.0: 936 | version "4.2.1" 937 | resolved "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz" 938 | integrity sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw== 939 | dependencies: 940 | rc "^1.2.8" 941 | 942 | registry-url@^5.0.0: 943 | version "5.1.0" 944 | resolved "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz" 945 | integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== 946 | dependencies: 947 | rc "^1.2.8" 948 | 949 | responselike@^1.0.2: 950 | version "1.0.2" 951 | resolved "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz" 952 | integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= 953 | dependencies: 954 | lowercase-keys "^1.0.0" 955 | 956 | safe-buffer@5.1.2: 957 | version "5.1.2" 958 | resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" 959 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 960 | 961 | safe-buffer@5.2.1: 962 | version "5.2.1" 963 | resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" 964 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 965 | 966 | "safer-buffer@>= 2.1.2 < 3": 967 | version "2.1.2" 968 | resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" 969 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 970 | 971 | semver-diff@^3.1.1: 972 | version "3.1.1" 973 | resolved "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz" 974 | integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== 975 | dependencies: 976 | semver "^6.3.0" 977 | 978 | semver@^5.7.1: 979 | version "5.7.1" 980 | resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" 981 | integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== 982 | 983 | semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: 984 | version "6.3.0" 985 | resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" 986 | integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== 987 | 988 | semver@^7.3.4: 989 | version "7.3.7" 990 | resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz" 991 | integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== 992 | dependencies: 993 | lru-cache "^6.0.0" 994 | 995 | send@0.18.0: 996 | version "0.18.0" 997 | resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" 998 | integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== 999 | dependencies: 1000 | debug "2.6.9" 1001 | depd "2.0.0" 1002 | destroy "1.2.0" 1003 | encodeurl "~1.0.2" 1004 | escape-html "~1.0.3" 1005 | etag "~1.8.1" 1006 | fresh "0.5.2" 1007 | http-errors "2.0.0" 1008 | mime "1.6.0" 1009 | ms "2.1.3" 1010 | on-finished "2.4.1" 1011 | range-parser "~1.2.1" 1012 | statuses "2.0.1" 1013 | 1014 | serve-static@1.15.0: 1015 | version "1.15.0" 1016 | resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz" 1017 | integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== 1018 | dependencies: 1019 | encodeurl "~1.0.2" 1020 | escape-html "~1.0.3" 1021 | parseurl "~1.3.3" 1022 | send "0.18.0" 1023 | 1024 | setprototypeof@1.2.0: 1025 | version "1.2.0" 1026 | resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" 1027 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 1028 | 1029 | side-channel@^1.0.4: 1030 | version "1.0.4" 1031 | resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" 1032 | integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== 1033 | dependencies: 1034 | call-bind "^1.0.0" 1035 | get-intrinsic "^1.0.2" 1036 | object-inspect "^1.9.0" 1037 | 1038 | signal-exit@^3.0.2: 1039 | version "3.0.7" 1040 | resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" 1041 | integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== 1042 | 1043 | statuses@2.0.1: 1044 | version "2.0.1" 1045 | resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" 1046 | integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== 1047 | 1048 | string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.2: 1049 | version "4.2.3" 1050 | resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" 1051 | integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== 1052 | dependencies: 1053 | emoji-regex "^8.0.0" 1054 | is-fullwidth-code-point "^3.0.0" 1055 | strip-ansi "^6.0.1" 1056 | 1057 | strip-ansi@^6.0.0, strip-ansi@^6.0.1: 1058 | version "6.0.1" 1059 | resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" 1060 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 1061 | dependencies: 1062 | ansi-regex "^5.0.1" 1063 | 1064 | strip-json-comments@~2.0.1: 1065 | version "2.0.1" 1066 | resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" 1067 | integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= 1068 | 1069 | supports-color@^5.5.0: 1070 | version "5.5.0" 1071 | resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" 1072 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 1073 | dependencies: 1074 | has-flag "^3.0.0" 1075 | 1076 | supports-color@^7.1.0: 1077 | version "7.2.0" 1078 | resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" 1079 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 1080 | dependencies: 1081 | has-flag "^4.0.0" 1082 | 1083 | to-readable-stream@^1.0.0: 1084 | version "1.0.0" 1085 | resolved "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz" 1086 | integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== 1087 | 1088 | to-regex-range@^5.0.1: 1089 | version "5.0.1" 1090 | resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" 1091 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 1092 | dependencies: 1093 | is-number "^7.0.0" 1094 | 1095 | toidentifier@1.0.1: 1096 | version "1.0.1" 1097 | resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" 1098 | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== 1099 | 1100 | touch@^3.1.0: 1101 | version "3.1.0" 1102 | resolved "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz" 1103 | integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== 1104 | dependencies: 1105 | nopt "~1.0.10" 1106 | 1107 | type-fest@^0.20.2: 1108 | version "0.20.2" 1109 | resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" 1110 | integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== 1111 | 1112 | type-is@~1.6.18: 1113 | version "1.6.18" 1114 | resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" 1115 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 1116 | dependencies: 1117 | media-typer "0.3.0" 1118 | mime-types "~2.1.24" 1119 | 1120 | typedarray-to-buffer@^3.1.5: 1121 | version "3.1.5" 1122 | resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" 1123 | integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== 1124 | dependencies: 1125 | is-typedarray "^1.0.0" 1126 | 1127 | undefsafe@^2.0.5: 1128 | version "2.0.5" 1129 | resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz" 1130 | integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== 1131 | 1132 | unique-string@^2.0.0: 1133 | version "2.0.0" 1134 | resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz" 1135 | integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== 1136 | dependencies: 1137 | crypto-random-string "^2.0.0" 1138 | 1139 | unpipe@1.0.0, unpipe@~1.0.0: 1140 | version "1.0.0" 1141 | resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" 1142 | integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= 1143 | 1144 | update-notifier@^5.1.0: 1145 | version "5.1.0" 1146 | resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz" 1147 | integrity sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw== 1148 | dependencies: 1149 | boxen "^5.0.0" 1150 | chalk "^4.1.0" 1151 | configstore "^5.0.1" 1152 | has-yarn "^2.1.0" 1153 | import-lazy "^2.1.0" 1154 | is-ci "^2.0.0" 1155 | is-installed-globally "^0.4.0" 1156 | is-npm "^5.0.0" 1157 | is-yarn-global "^0.3.0" 1158 | latest-version "^5.1.0" 1159 | pupa "^2.1.1" 1160 | semver "^7.3.4" 1161 | semver-diff "^3.1.1" 1162 | xdg-basedir "^4.0.0" 1163 | 1164 | url-parse-lax@^3.0.0: 1165 | version "3.0.0" 1166 | resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz" 1167 | integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= 1168 | dependencies: 1169 | prepend-http "^2.0.0" 1170 | 1171 | utils-merge@1.0.1: 1172 | version "1.0.1" 1173 | resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" 1174 | integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= 1175 | 1176 | uuid@^8.3.2: 1177 | version "8.3.2" 1178 | resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" 1179 | integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== 1180 | 1181 | vary@^1, vary@~1.1.2: 1182 | version "1.1.2" 1183 | resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" 1184 | integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= 1185 | 1186 | widest-line@^3.1.0: 1187 | version "3.1.0" 1188 | resolved "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz" 1189 | integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== 1190 | dependencies: 1191 | string-width "^4.0.0" 1192 | 1193 | wrap-ansi@^7.0.0: 1194 | version "7.0.0" 1195 | resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" 1196 | integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== 1197 | dependencies: 1198 | ansi-styles "^4.0.0" 1199 | string-width "^4.1.0" 1200 | strip-ansi "^6.0.0" 1201 | 1202 | wrappy@1: 1203 | version "1.0.2" 1204 | resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" 1205 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 1206 | 1207 | write-file-atomic@^3.0.0: 1208 | version "3.0.3" 1209 | resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz" 1210 | integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== 1211 | dependencies: 1212 | imurmurhash "^0.1.4" 1213 | is-typedarray "^1.0.0" 1214 | signal-exit "^3.0.2" 1215 | typedarray-to-buffer "^3.1.5" 1216 | 1217 | xdg-basedir@^4.0.0: 1218 | version "4.0.0" 1219 | resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz" 1220 | integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== 1221 | 1222 | yallist@^4.0.0: 1223 | version "4.0.0" 1224 | resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" 1225 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 1226 | --------------------------------------------------------------------------------