diff --git a/.github/workflows/promotions.yml b/.github/workflows/promotions.yml new file mode 100644 index 00000000..fb95fee0 --- /dev/null +++ b/.github/workflows/promotions.yml @@ -0,0 +1,69 @@ +name: Promotions Microservice CI/CD + +on: + push: + branches: [ promotions ] + pull_request: + branches: [ dev, main ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: pnpm/action-setup@v2 + with: + version: latest + - uses: actions/setup-node@v3 + with: + node-version: "18.x" + cache: "pnpm" + cache-dependency-path: services/promotions/pnpm-lock.yaml + - name: Install dependencies + run: | + cd ./services/promotions + pnpm install --frozen-lockfile + - name: Build + run: | + cd ./services/promotions + pnpm build + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: pnpm/action-setup@v2 + with: + version: latest + - uses: actions/setup-node@v3 + with: + node-version: "18.x" + cache: "pnpm" + cache-dependency-path: services/promotions/pnpm-lock.yaml + - name: Install dependencies + run: | + cd ./services/promotions + pnpm install --frozen-lockfile + - name: Execute tests + run: | + cd ./services/promotions + pnpm test + + publish: + runs-on: ubuntu-latest + needs: [ build, test ] + steps: + - name: Checkout code + uses: actions/checkout@master + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: ./services/ + file: ./services/promotions/Dockerfile + push: true + tags: pierrelbg/goodfood-promotions:latest \ No newline at end of file diff --git a/services/promotions/.dockerignore b/services/promotions/.dockerignore new file mode 100644 index 00000000..b7dab5e9 --- /dev/null +++ b/services/promotions/.dockerignore @@ -0,0 +1,2 @@ +node_modules +build \ No newline at end of file diff --git a/services/promotions/.env.example b/services/promotions/.env.example new file mode 100644 index 00000000..b1adbc98 --- /dev/null +++ b/services/promotions/.env.example @@ -0,0 +1,4 @@ +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/promotions +AMQP_URL=amqp://guest:guest@localhost + +PORT=50006 \ No newline at end of file diff --git a/services/promotions/.gitignore b/services/promotions/.gitignore new file mode 100644 index 00000000..bab9187d --- /dev/null +++ b/services/promotions/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +bin/ +*.env* +!*.env.example \ No newline at end of file diff --git a/services/promotions/Dockerfile b/services/promotions/Dockerfile new file mode 100644 index 00000000..a1fc70da --- /dev/null +++ b/services/promotions/Dockerfile @@ -0,0 +1,38 @@ +FROM node:18-alpine3.17 as builder + +# Set working directory +WORKDIR /app + +# Copy the application code +COPY ./promotions/ . + +# Install dependencies +RUN npm install + +# Copy the proto files +COPY ./proto ./proto/ + +# Generate Prisma client +RUN npx prisma generate + +# Build the application +RUN npm run build + + +# Create a new image with the application +FROM node:18-alpine3.17 as runner + +# Set working directory +WORKDIR /app + +# Copy the application package +COPY --from=builder /app/dist . +COPY --from=builder /app/prisma/ . +COPY --from=builder /app/proto/ /proto/ +COPY --from=builder /app/node_modules/.prisma /.prisma + +# Expose the gRPC port +EXPOSE 50006 + +# Start the server +CMD [ "node", "index.js"] diff --git a/services/promotions/README.md b/services/promotions/README.md new file mode 100644 index 00000000..8aa04cda --- /dev/null +++ b/services/promotions/README.md @@ -0,0 +1,84 @@ +# Delivery Microservice + +| Informations | +|--------------------------------------------------------------------| +| **Port:** 50006 | +| **Developer:** @PierreLgb | +| **Status:** In progress | +| **Last update:** 2023-07-20 | +| **Language:** NodeJS | +| **Dependencies:** TypeScript, Prisma, gRPC, Postgres | +| **Models:** (see [`prisma/schema.prisma`](./prisma/schema.prisma)) | + +## gRPC Methods + +- Promotions model: + + - `CreatePromotion`: Creates a new promotion in the system. + - `GetPromotion`: Retrieves a promotion by its ID. + - `UpdatePromotion`: Updates an existing promotion. + - `DeletePromotion`: Deletes a promotion by its ID. + - `GetPromotions`: Retrieves all promotions. + - `ListPromotionsByRestaurant`: Retrieves all promotion for a given restaurant. + +## Requirements + +To run this microservice, you will need to have the following installed on your system: + +- NodeJS (v18.12.0 or higher) (dev. with v18.12.0) +- Postgres (v15.2 or higher) (dev. with docker image `postgres:15.2`) + +You can use the following tools to help you with the setup: + +- You can use nvm to set your Node version using: + - `nvm use`. +- You can use docker to run your Postgres database using: + - `docker run --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=password -d postgres` + +## Getting started + +### 1. Clone the repository and install dependencies + +1. Clone the `goodfood` repository to your local machine. +2. Navigate to the service directory (`services/promotions`) in your terminal. +3. Run `npm install` to install the necessary dependencies. +4. Create a `.env` file at the root of the project directory and add the environment variables values ( + see `.env.example`). +5. Run `npm run start` to start the microservice. + +You can now access the microservice at `http://localhost:50006`. + +NB: If you want to run the microservice in development mode, you can run `npm run dev` instead. + +### 2. Create and seed the database + +Run the following command to create your Postgres database structure. This also creates the models tables that are +defined in [`prisma/schema.prisma`](./prisma/schema.prisma): + +``` +npx prisma migrate dev --name init +``` + +When `npx prisma migrate dev` is executed against a newly created database, seeding is also triggered. The seed file +in [`prisma/seed.ts`](./prisma/seed.ts) will be executed and your database will be populated with the sample data. + +## Build and Run with Docker + +### Build + +To build the image you need to be in the **parent folder** of the service you want to build. Then run the following +command: + +``` +docker build -t goodfood-promotions:1.0.0 -f ./promotions/Dockerfile . +docker tag goodfood-promotions:1.0.0 pierrelbg/goodfood-promotions:1.0.0 +docker push pierrelbg/goodfood-promotions:1.0.0 +``` + +### Run + +Create the .env base on the .env.example. Then run the following command: + +``` +docker run --env-file=.env goodfood-delivery:1.0.0 +``` diff --git a/services/promotions/package.json b/services/promotions/package.json new file mode 100644 index 00000000..6506e8a7 --- /dev/null +++ b/services/promotions/package.json @@ -0,0 +1,29 @@ +{ + "name": "@goodfood/delivery", + "bin": "dist/index.js", + "scripts": { + "start": "node dist/index.js", + "dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/server.ts", + "build": "esbuild src/server.ts --bundle --platform=node --target=node18 --outfile=dist/index.js", + "test": "echo 'tests are disabled'", + "prisma:generate": "prisma generate", + "postinstall": "npm run prisma:generate" + }, + "devDependencies": { + "@types/amqplib": "^0.10.1", + "prisma": "^5.0.0", + "typescript": "^5.1.6" + }, + "dependencies": { + "@grpc/grpc-js": "^1.8.18", + "@grpc/proto-loader": "^0.7.8", + "@prisma/client": "5.0.0", + "amqplib": "^0.10.3", + "dotenv": "^16.3.1", + "esbuild": "^0.18.15", + "nodemon": "^3.0.1", + "path": "^0.12.7", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0" + } +} diff --git a/services/promotions/pnpm-lock.yaml b/services/promotions/pnpm-lock.yaml new file mode 100644 index 00000000..22f8d01f --- /dev/null +++ b/services/promotions/pnpm-lock.yaml @@ -0,0 +1,992 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@grpc/grpc-js': + specifier: ^1.8.18 + version: 1.8.18 + '@grpc/proto-loader': + specifier: ^0.7.8 + version: 0.7.8 + '@prisma/client': + specifier: 5.0.0 + version: 5.0.0(prisma@5.0.0) + amqplib: + specifier: ^0.10.3 + version: 0.10.3 + dotenv: + specifier: ^16.3.1 + version: 16.3.1 + esbuild: + specifier: ^0.18.15 + version: 0.18.15 + nodemon: + specifier: ^3.0.1 + version: 3.0.1 + path: + specifier: ^0.12.7 + version: 0.12.7 + ts-node: + specifier: ^10.9.1 + version: 10.9.1(@types/node@20.4.2)(typescript@5.1.6) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 + +devDependencies: + '@types/amqplib': + specifier: ^0.10.1 + version: 0.10.1 + prisma: + specifier: ^5.0.0 + version: 5.0.0 + typescript: + specifier: ^5.1.6 + version: 5.1.6 + +packages: + + /@acuminous/bitsyntax@0.1.2: + resolution: {integrity: sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==} + engines: {node: '>=0.8'} + dependencies: + buffer-more-ints: 1.0.0 + debug: 4.3.4 + safe-buffer: 5.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: false + + /@esbuild/android-arm64@0.18.15: + resolution: {integrity: sha512-NI/gnWcMl2kXt1HJKOn2H69SYn4YNheKo6NZt1hyfKWdMbaGadxjZIkcj4Gjk/WPxnbFXs9/3HjGHaknCqjrww==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@esbuild/android-arm@0.18.15: + resolution: {integrity: sha512-wlkQBWb79/jeEEoRmrxt/yhn5T1lU236OCNpnfRzaCJHZ/5gf82uYx1qmADTBWE0AR/v7FiozE1auk2riyQd3w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@esbuild/android-x64@0.18.15: + resolution: {integrity: sha512-FM9NQamSaEm/IZIhegF76aiLnng1kEsZl2eve/emxDeReVfRuRNmvT28l6hoFD9TsCxpK+i4v8LPpEj74T7yjA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@esbuild/darwin-arm64@0.18.15: + resolution: {integrity: sha512-XmrFwEOYauKte9QjS6hz60FpOCnw4zaPAb7XV7O4lx1r39XjJhTN7ZpXqJh4sN6q60zbP6QwAVVA8N/wUyBH/w==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@esbuild/darwin-x64@0.18.15: + resolution: {integrity: sha512-bMqBmpw1e//7Fh5GLetSZaeo9zSC4/CMtrVFdj+bqKPGJuKyfNJ5Nf2m3LknKZTS+Q4oyPiON+v3eaJ59sLB5A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@esbuild/freebsd-arm64@0.18.15: + resolution: {integrity: sha512-LoTK5N3bOmNI9zVLCeTgnk5Rk0WdUTrr9dyDAQGVMrNTh9EAPuNwSTCgaKOKiDpverOa0htPcO9NwslSE5xuLA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/freebsd-x64@0.18.15: + resolution: {integrity: sha512-62jX5n30VzgrjAjOk5orYeHFq6sqjvsIj1QesXvn5OZtdt5Gdj0vUNJy9NIpjfdNdqr76jjtzBJKf+h2uzYuTQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-arm64@0.18.15: + resolution: {integrity: sha512-BWncQeuWDgYv0jTNzJjaNgleduV4tMbQjmk/zpPh/lUdMcNEAxy+jvneDJ6RJkrqloG7tB9S9rCrtfk/kuplsQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-arm@0.18.15: + resolution: {integrity: sha512-dT4URUv6ir45ZkBqhwZwyFV6cH61k8MttIwhThp2BGiVtagYvCToF+Bggyx2VI57RG4Fbt21f9TmXaYx0DeUJg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-ia32@0.18.15: + resolution: {integrity: sha512-JPXORvgHRHITqfms1dWT/GbEY89u848dC08o0yK3fNskhp0t2TuNUnsrrSgOdH28ceb1hJuwyr8R/1RnyPwocw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-loong64@0.18.15: + resolution: {integrity: sha512-kArPI0DopjJCEplsVj/H+2Qgzz7vdFSacHNsgoAKpPS6W/Ndh8Oe24HRDQ5QCu4jHgN6XOtfFfLpRx3TXv/mEg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-mips64el@0.18.15: + resolution: {integrity: sha512-b/tmngUfO02E00c1XnNTw/0DmloKjb6XQeqxaYuzGwHe0fHVgx5/D6CWi+XH1DvkszjBUkK9BX7n1ARTOst59w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-ppc64@0.18.15: + resolution: {integrity: sha512-KXPY69MWw79QJkyvUYb2ex/OgnN/8N/Aw5UDPlgoRtoEfcBqfeLodPr42UojV3NdkoO4u10NXQdamWm1YEzSKw==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-riscv64@0.18.15: + resolution: {integrity: sha512-komK3NEAeeGRnvFEjX1SfVg6EmkfIi5aKzevdvJqMydYr9N+pRQK0PGJXk+bhoPZwOUgLO4l99FZmLGk/L1jWg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-s390x@0.18.15: + resolution: {integrity: sha512-632T5Ts6gQ2WiMLWRRyeflPAm44u2E/s/TJvn+BP6M5mnHSk93cieaypj3VSMYO2ePTCRqAFXtuYi1yv8uZJNA==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-x64@0.18.15: + resolution: {integrity: sha512-MsHtX0NgvRHsoOtYkuxyk4Vkmvk3PLRWfA4okK7c+6dT0Fu4SUqXAr9y4Q3d8vUf1VWWb6YutpL4XNe400iQ1g==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/netbsd-x64@0.18.15: + resolution: {integrity: sha512-djST6s+jQiwxMIVQ5rlt24JFIAr4uwUnzceuFL7BQT4CbrRtqBPueS4GjXSiIpmwVri1Icj/9pFRJ7/aScvT+A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/openbsd-x64@0.18.15: + resolution: {integrity: sha512-naeRhUIvhsgeounjkF5mvrNAVMGAm6EJWiabskeE5yOeBbLp7T89tAEw0j5Jm/CZAwyLe3c67zyCWH6fsBLCpw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/sunos-x64@0.18.15: + resolution: {integrity: sha512-qkT2+WxyKbNIKV1AEhI8QiSIgTHMcRctzSaa/I3kVgMS5dl3fOeoqkb7pW76KwxHoriImhx7Mg3TwN/auMDsyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: false + optional: true + + /@esbuild/win32-arm64@0.18.15: + resolution: {integrity: sha512-HC4/feP+pB2Vb+cMPUjAnFyERs+HJN7E6KaeBlFdBv799MhD+aPJlfi/yk36SED58J9TPwI8MAcVpJgej4ud0A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@esbuild/win32-ia32@0.18.15: + resolution: {integrity: sha512-ovjwoRXI+gf52EVF60u9sSDj7myPixPxqzD5CmkEUmvs+W9Xd0iqISVBQn8xcx4ciIaIVlWCuTbYDOXOnOL44Q==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@esbuild/win32-x64@0.18.15: + resolution: {integrity: sha512-imUxH9a3WJARyAvrG7srLyiK73XdX83NXQkjKvQ+7vPh3ZxoLrzvPkQKKw2DwZ+RV2ZB6vBfNHP8XScAmQC3aA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@grpc/grpc-js@1.8.18: + resolution: {integrity: sha512-2uWPtxhsXmVgd8WzDhfamSjHpZDXfMjMDciY6VRTq4Sn7rFzazyf0LLDa0oav+61UHIoEZb4KKaAV6S7NuJFbQ==} + engines: {node: ^8.13.0 || >=10.10.0} + dependencies: + '@grpc/proto-loader': 0.7.8 + '@types/node': 20.4.2 + dev: false + + /@grpc/proto-loader@0.7.8: + resolution: {integrity: sha512-GU12e2c8dmdXb7XUlOgYWZ2o2i+z9/VeACkxTA/zzAe2IjclC5PnVL0lpgjhrqfpDYHzM8B1TF6pqWegMYAzlA==} + engines: {node: '>=6'} + hasBin: true + dependencies: + '@types/long': 4.0.2 + lodash.camelcase: 4.3.0 + long: 4.0.0 + protobufjs: 7.2.4 + yargs: 17.7.2 + dev: false + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: false + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: false + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: false + + /@prisma/client@5.0.0(prisma@5.0.0): + resolution: {integrity: sha512-XlO5ELNAQ7rV4cXIDJUNBEgdLwX3pjtt9Q/RHqDpGf43szpNJx2hJnggfFs7TKNx0cOFsl6KJCSfqr5duEU/bQ==} + engines: {node: '>=16.13'} + requiresBuild: true + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + dependencies: + '@prisma/engines-version': 4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584 + prisma: 5.0.0 + dev: false + + /@prisma/engines-version@4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584: + resolution: {integrity: sha512-HHiUF6NixsldsP3JROq07TYBLEjXFKr6PdH8H4gK/XAoTmIplOJBCgrIUMrsRAnEuGyRoRLXKXWUb943+PFoKQ==} + dev: false + + /@prisma/engines@5.0.0: + resolution: {integrity: sha512-kyT/8fd0OpWmhAU5YnY7eP31brW1q1YrTGoblWrhQJDiN/1K+Z8S1kylcmtjqx5wsUGcP1HBWutayA/jtyt+sg==} + requiresBuild: true + + /@protobufjs/aspromise@1.1.2: + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + dev: false + + /@protobufjs/base64@1.1.2: + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + dev: false + + /@protobufjs/codegen@2.0.4: + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + dev: false + + /@protobufjs/eventemitter@1.1.0: + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + dev: false + + /@protobufjs/fetch@1.1.0: + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + dev: false + + /@protobufjs/float@1.0.2: + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + dev: false + + /@protobufjs/inquire@1.1.0: + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + dev: false + + /@protobufjs/path@1.1.2: + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + dev: false + + /@protobufjs/pool@1.1.0: + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + dev: false + + /@protobufjs/utf8@1.1.0: + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + dev: false + + /@tsconfig/node10@1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + dev: false + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: false + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: false + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: false + + /@types/amqplib@0.10.1: + resolution: {integrity: sha512-j6ANKT79ncUDnAs/+9r9eDujxbeJoTjoVu33gHHcaPfmLQaMhvfbH2GqSe8KUM444epAp1Vl3peVOQfZk3UIqA==} + dependencies: + '@types/node': 20.4.2 + dev: true + + /@types/long@4.0.2: + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + dev: false + + /@types/node@20.4.2: + resolution: {integrity: sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==} + + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: false + + /acorn-walk@8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + dev: false + + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: false + + /amqplib@0.10.3: + resolution: {integrity: sha512-UHmuSa7n8vVW/a5HGh2nFPqAEr8+cD4dEZ6u9GjP91nHfr1a54RyAKyra7Sb5NH7NBKOUlyQSMXIp0qAixKexw==} + engines: {node: '>=10'} + dependencies: + '@acuminous/bitsyntax': 0.1.2 + buffer-more-ints: 1.0.0 + readable-stream: 1.1.14 + url-parse: 1.5.10 + transitivePeerDependencies: + - supports-color + dev: false + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: false + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: false + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: false + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: false + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: false + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: false + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: false + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: false + + /buffer-more-ints@1.0.0: + resolution: {integrity: sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==} + dev: false + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: false + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: false + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: false + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: false + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: false + + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: false + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: false + + /debug@3.2.7(supports-color@5.5.0): + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + supports-color: 5.5.0 + dev: false + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: false + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: false + + /dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + dev: false + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: false + + /esbuild@0.18.15: + resolution: {integrity: sha512-3WOOLhrvuTGPRzQPU6waSDWrDTnQriia72McWcn6UCi43GhCHrXH4S59hKMeez+IITmdUuUyvbU9JIp+t3xlPQ==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.18.15 + '@esbuild/android-arm64': 0.18.15 + '@esbuild/android-x64': 0.18.15 + '@esbuild/darwin-arm64': 0.18.15 + '@esbuild/darwin-x64': 0.18.15 + '@esbuild/freebsd-arm64': 0.18.15 + '@esbuild/freebsd-x64': 0.18.15 + '@esbuild/linux-arm': 0.18.15 + '@esbuild/linux-arm64': 0.18.15 + '@esbuild/linux-ia32': 0.18.15 + '@esbuild/linux-loong64': 0.18.15 + '@esbuild/linux-mips64el': 0.18.15 + '@esbuild/linux-ppc64': 0.18.15 + '@esbuild/linux-riscv64': 0.18.15 + '@esbuild/linux-s390x': 0.18.15 + '@esbuild/linux-x64': 0.18.15 + '@esbuild/netbsd-x64': 0.18.15 + '@esbuild/openbsd-x64': 0.18.15 + '@esbuild/sunos-x64': 0.18.15 + '@esbuild/win32-arm64': 0.18.15 + '@esbuild/win32-ia32': 0.18.15 + '@esbuild/win32-x64': 0.18.15 + dev: false + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: false + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: false + + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: false + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: false + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: false + + /ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + dev: false + + /inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + dev: false + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: false + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: false + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: false + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: false + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: false + + /isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + dev: false + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: false + + /lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: false + + /long@4.0.0: + resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + dev: false + + /long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + dev: false + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: false + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: false + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: false + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: false + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /nodemon@3.0.1: + resolution: {integrity: sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + chokidar: 3.5.3 + debug: 3.2.7(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.5.4 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.0 + undefsafe: 2.0.5 + dev: false + + /nopt@1.0.10: + resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: false + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: false + + /path@0.12.7: + resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} + dependencies: + process: 0.11.10 + util: 0.10.4 + dev: false + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: false + + /prisma@5.0.0: + resolution: {integrity: sha512-KYWk83Fhi1FH59jSpavAYTt2eoMVW9YKgu8ci0kuUnt6Dup5Qy47pcB4/TLmiPAbhGrxxSz7gsSnJcCmkyPANA==} + engines: {node: '>=16.13'} + hasBin: true + requiresBuild: true + dependencies: + '@prisma/engines': 5.0.0 + + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + dev: false + + /protobufjs@7.2.4: + resolution: {integrity: sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==} + engines: {node: '>=12.0.0'} + requiresBuild: true + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.4.2 + long: 5.2.3 + dev: false + + /pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + dev: false + + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: false + + /readable-stream@1.1.14: + resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.3 + isarray: 0.0.1 + string_decoder: 0.10.31 + dev: false + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: false + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: false + + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: false + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: false + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: false + + /simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + dev: false + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: false + + /string_decoder@0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} + dev: false + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: false + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: false + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: false + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: false + + /touch@3.1.0: + resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} + hasBin: true + dependencies: + nopt: 1.0.10 + dev: false + + /ts-node@10.9.1(@types/node@20.4.2)(typescript@5.1.6): + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.4.2 + acorn: 8.10.0 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.1.6 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: false + + /tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: false + + /typescript@5.1.6: + resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} + engines: {node: '>=14.17'} + hasBin: true + + /undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + dev: false + + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: false + + /util@0.10.4: + resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} + dependencies: + inherits: 2.0.3 + dev: false + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: false + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: false + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: false + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: false + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: false + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: false diff --git a/services/promotions/prisma/migrations/0_init/migration.sql b/services/promotions/prisma/migrations/0_init/migration.sql new file mode 100644 index 00000000..8816c8d9 --- /dev/null +++ b/services/promotions/prisma/migrations/0_init/migration.sql @@ -0,0 +1,13 @@ +-- CreateEnum +CREATE TYPE "Method" AS ENUM ('PERCENT', 'VALUE'); + +-- CreateTable +CREATE TABLE "Promotions" ( + "id" TEXT NOT NULL, + "code" TEXT NOT NULL, + "reduction" TEXT NOT NULL, + "method" "Method" NOT NULL, + + CONSTRAINT "Promotions_pkey" PRIMARY KEY ("id") +); + diff --git a/services/promotions/prisma/migrations/20230719074150_unique_code/migration.sql b/services/promotions/prisma/migrations/20230719074150_unique_code/migration.sql new file mode 100644 index 00000000..d7807ca3 --- /dev/null +++ b/services/promotions/prisma/migrations/20230719074150_unique_code/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[code]` on the table `Promotions` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "Promotions_code_key" ON "Promotions"("code"); diff --git a/services/promotions/prisma/migrations/20230719074429_rename_table/migration.sql b/services/promotions/prisma/migrations/20230719074429_rename_table/migration.sql new file mode 100644 index 00000000..26553d76 --- /dev/null +++ b/services/promotions/prisma/migrations/20230719074429_rename_table/migration.sql @@ -0,0 +1,21 @@ +/* + Warnings: + + - You are about to drop the `Promotions` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +DROP TABLE "Promotions"; + +-- CreateTable +CREATE TABLE "Promotion" ( + "id" TEXT NOT NULL, + "code" TEXT NOT NULL, + "reduction" TEXT NOT NULL, + "method" "Method" NOT NULL, + + CONSTRAINT "Promotion_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Promotion_code_key" ON "Promotion"("code"); diff --git a/services/promotions/prisma/migrations/20230721123549_add_restaurant_id/migration.sql b/services/promotions/prisma/migrations/20230721123549_add_restaurant_id/migration.sql new file mode 100644 index 00000000..634ce104 --- /dev/null +++ b/services/promotions/prisma/migrations/20230721123549_add_restaurant_id/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `restaurant_id` to the `Promotion` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Promotion" ADD COLUMN "restaurant_id" TEXT NOT NULL; diff --git a/services/promotions/prisma/migrations/migration_lock.toml b/services/promotions/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..fbffa92c --- /dev/null +++ b/services/promotions/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/services/promotions/prisma/schema.prisma b/services/promotions/prisma/schema.prisma new file mode 100644 index 00000000..51eddb16 --- /dev/null +++ b/services/promotions/prisma/schema.prisma @@ -0,0 +1,22 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +enum Method { + PERCENT + VALUE +} + +model Promotion{ + id String @id @default(cuid()) + + code String @unique + reduction String + method Method + restaurant_id String +} \ No newline at end of file diff --git a/services/promotions/src/handler/create.ts b/services/promotions/src/handler/create.ts new file mode 100644 index 00000000..69d08f9a --- /dev/null +++ b/services/promotions/src/handler/create.ts @@ -0,0 +1,27 @@ +import { PromotionCreateInput, Promotion } from "@promotions/types/promotion"; +import { log } from "@promotions/lib/log"; +import { Data } from "@promotions/types"; +import prisma from "@promotions/lib/prisma"; + +export const CreatePromotion = async ( + { request }: Data, + callback: (err: any, response: Promotion | null) => void +) => { + try { + const { code, reduction, method, restaurant_id } = request; + + const promotion = await prisma.promotion.create({ + data: { + code, + reduction, + method, + restaurant_id + }, + }) as unknown as Promotion; + + callback(null, promotion); + } catch (error) { + log.error(error); + callback(error, null); + } +}; \ No newline at end of file diff --git a/services/promotions/src/handler/delete.ts b/services/promotions/src/handler/delete.ts new file mode 100644 index 00000000..3e1edd13 --- /dev/null +++ b/services/promotions/src/handler/delete.ts @@ -0,0 +1,20 @@ +import { PromotionId } from "@promotions/types/promotion"; +import { log } from "@promotions/lib/log"; +import { Data } from "@promotions/types"; +import prisma from "@promotions/lib/prisma"; + +export const DeletePromotion = async ( + { request }: Data, + callback: (err: any, response: null) => void +) => { + try { + const { id } = request; + + await prisma.promotion.delete({ where : { id } }); + + callback(null, null); + } catch (error) { + log.error(error); + callback(error, null); + } +}; \ No newline at end of file diff --git a/services/promotions/src/handler/get.ts b/services/promotions/src/handler/get.ts new file mode 100644 index 00000000..b92b5cd0 --- /dev/null +++ b/services/promotions/src/handler/get.ts @@ -0,0 +1,20 @@ +import { PromotionCode, Promotion } from "@promotions/types/promotion"; +import { log } from "@promotions/lib/log"; +import { Data } from "@promotions/types"; +import prisma from "@promotions/lib/prisma"; + +export const GetPromotion = async ( + { request }: Data, + callback: (err: any, response: Promotion | null) => void +) => { + try { + const { code } = request; + + const promotion = await prisma.promotion.findFirstOrThrow({ where: { code } }) as unknown as Promotion; + + callback(null, promotion); + } catch (error) { + log.error(error); + callback(error, null); + } +}; \ No newline at end of file diff --git a/services/promotions/src/handler/index.ts b/services/promotions/src/handler/index.ts new file mode 100644 index 00000000..57c52450 --- /dev/null +++ b/services/promotions/src/handler/index.ts @@ -0,0 +1,15 @@ +import { CreatePromotion } from "@promotions/handler/create"; +import { GetPromotion } from "@promotions/handler/get"; +import { UpdatePromotion } from "@promotions/handler/update"; +import { DeletePromotion } from "@promotions/handler/delete"; +import { GetPromotions } from "@promotions/handler/list-all"; +import { GetPromotionsByRestaurant } from "@promotions/handler/list-by-restaurant"; + +export default { + CreatePromotion, + GetPromotion, + UpdatePromotion, + DeletePromotion, + GetPromotions, + GetPromotionsByRestaurant, +}; diff --git a/services/promotions/src/handler/list-all.ts b/services/promotions/src/handler/list-all.ts new file mode 100644 index 00000000..b9df0f52 --- /dev/null +++ b/services/promotions/src/handler/list-all.ts @@ -0,0 +1,21 @@ +import { PromotionId, PromotionList, Promotion } from "@promotions/types/promotion"; +import { log } from "@promotions/lib/log"; +import { Data } from "@promotions/types"; +import prisma from "@promotions/lib/prisma"; + +export const GetPromotions = async ( + { request }: Data, + callback: (err: any, response: PromotionList | null) => void +) => { + try { + const { id } = request; + + const promotions = await prisma.promotion.findMany() as unknown as Promotion[]; + const promotionList = {promotions} as PromotionList; + + callback(null, promotionList); + } catch (error) { + log.error(error); + callback(error, null); + } +}; \ No newline at end of file diff --git a/services/promotions/src/handler/list-by-restaurant.ts b/services/promotions/src/handler/list-by-restaurant.ts new file mode 100644 index 00000000..c59d669c --- /dev/null +++ b/services/promotions/src/handler/list-by-restaurant.ts @@ -0,0 +1,21 @@ +import { RestaurantId, PromotionList, Promotion } from "@promotions/types/promotion"; +import { log } from "@promotions/lib/log"; +import { Data } from "@promotions/types"; +import prisma from "@promotions/lib/prisma"; + +export const GetPromotionsByRestaurant = async ( + { request }: Data, + callback: (err: any, response: PromotionList | null) => void +) => { + try { + const { id } = request; + + const promotions = await prisma.promotion.findMany({where : { restaurant_id : id }}) as unknown as Promotion[]; + const promotionList = { promotions } as PromotionList; + + callback(null, promotionList); + } catch (error) { + log.error(error); + callback(error, null); + } +}; \ No newline at end of file diff --git a/services/promotions/src/handler/update.ts b/services/promotions/src/handler/update.ts new file mode 100644 index 00000000..ae9e777d --- /dev/null +++ b/services/promotions/src/handler/update.ts @@ -0,0 +1,27 @@ +import { Promotion } from "@promotions/types/promotion"; +import { log } from "@promotions/lib/log"; +import { Data } from "@promotions/types"; +import prisma from "@promotions/lib/prisma"; + +export const UpdatePromotion = async ( + { request }: Data, + callback: (err: any, response: Promotion | null) => void +) => { + try { + const { id, code, method, reduction } = request; + + const promotion = await prisma.promotion.update({ + where: { id }, + data: { + code, + method, + reduction, + }, + }); + + callback(null, promotion); + } catch (error) { + log.error(error); + callback(error, null); + } +}; \ No newline at end of file diff --git a/services/promotions/src/lib/amqp.ts b/services/promotions/src/lib/amqp.ts new file mode 100644 index 00000000..0f2edcde --- /dev/null +++ b/services/promotions/src/lib/amqp.ts @@ -0,0 +1,32 @@ +import amqp from "amqplib"; +import { msg } from "@promotions/middleware/log"; +import { log } from "@promotions/lib/log"; + +const DEFAULT_QUEUE = "log"; + +async function connectQueue(queue = DEFAULT_QUEUE) { + try { + const connection = await amqp.connect(process.env.AMQP_URL || ""); + const channel = await connection.createChannel(); + await channel.assertExchange(queue, "fanout", { durable: true }); + return channel; + } catch (error) { + return error as Error; + } +} + +const toLog = (message: any) => { + const { request, path } = message; + return { event_message: path, metadata: { request } }; +}; + +export const publish = async (message: any, queue = DEFAULT_QUEUE) => { + const channel = await connectQueue(queue); + if (channel instanceof Error) + return console.log("Error to connect to queue: ", channel); + channel.sendToQueue(queue, Buffer.from(JSON.stringify(toLog(message)))); + log.debug( + msg("AMQP", `${queue} (queue)`, new Date(), new Date()), + JSON.stringify(message) + ); +}; diff --git a/services/promotions/src/lib/log.ts b/services/promotions/src/lib/log.ts new file mode 100644 index 00000000..2404af54 --- /dev/null +++ b/services/promotions/src/lib/log.ts @@ -0,0 +1,74 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-console */ + +function getStackTrace() { + let stack; + + try { + throw new Error(""); + } catch (error) { + stack = (error as Error).stack || ""; + } + + stack = stack.split("\n").map((line) => line.trim()); + return stack.splice(stack[0] === "Error" ? 2 : 1); +} + +const getInitiator = () => { + // _getInitiatorLine, _ObjectInfoLine, caller + const [, , caller] = getStackTrace(); + const file = caller.split("/").at(-1) || ""; + return file.replace(")", ""); +}; + +const warn = (...args: any[]) => { + console.group( + `\x1b[33m${new Date().toISOString()} - WARN - ${getInitiator()}\x1b[0m` + ); + console.warn(...args); + console.groupEnd(); +}; +const error = (...args: any[]) => { + console.group( + `\x1b[31m${new Date().toISOString()} - ERROR - ${getInitiator()}\x1b[0m` + ); + console.error(...args); + console.groupEnd(); +}; +const info = (...args: any[]) => { + console.group( + `\x1b[34m${new Date().toISOString()} - INFO - ${getInitiator()}\x1b[0m` + ); + console.log(...args); + console.groupEnd(); +}; +const debug = (...args: any[]) => { + console.log(...args); +}; + +/** + * @description a simple interface logger for logging to the console and into a log drain in the future + * @example + * log.warn('this is a warning'); + * log.error('this is an error'); + * log.info('this is an info'); + * @exports log + */ +export const log = { + warn, + error, + info, + debug, +}; + +export const utils = { + bold: (text: string) => `\x1b[1m${text}\x1b[0m`, + red: (text: string) => `\x1b[31m${text}\x1b[0m`, + green: (text: string) => `\x1b[32m${text}\x1b[0m`, + yellow: (text: string) => `\x1b[33m${text}\x1b[0m`, + blue: (text: string) => `\x1b[34m${text}\x1b[0m`, + magenta: (text: string) => `\x1b[35m${text}\x1b[0m`, + cyan: (text: string) => `\x1b[36m${text}\x1b[0m`, + white: (text: string) => `\x1b[37m${text}\x1b[0m`, + gray: (text: string) => `\x1b[90m${text}\x1b[0m`, +}; diff --git a/services/promotions/src/lib/prisma.ts b/services/promotions/src/lib/prisma.ts new file mode 100644 index 00000000..4d495ef0 --- /dev/null +++ b/services/promotions/src/lib/prisma.ts @@ -0,0 +1,29 @@ +import { Prisma, PrismaClient } from "@prisma/client"; + +let prisma: PrismaClient; +type GlobalWithPrisma = typeof globalThis & { + prisma: PrismaClient; +}; + +const OPTIONS: Prisma.PrismaClientOptions = { + log: + process.env.NEXT_PUBLIC_APP_ENV !== "production" + ? ["info", "warn"] + : undefined, +}; + +if (process.env.NODE_ENV === "production") { + prisma = new PrismaClient(OPTIONS); +} else { + // Ensure the prisma instance is re-used during hot-reloading + // Otherwise, a new client will be created on every reload + if (!("prisma" in global)) { + (global as GlobalWithPrisma).prisma = new PrismaClient(OPTIONS); + } + + prisma = (global as GlobalWithPrisma).prisma; +} + +export default prisma; + +export { prisma }; diff --git a/services/promotions/src/lib/proxy.ts b/services/promotions/src/lib/proxy.ts new file mode 100644 index 00000000..b05d24a6 --- /dev/null +++ b/services/promotions/src/lib/proxy.ts @@ -0,0 +1,132 @@ +import { Server, status } from "@grpc/grpc-js"; + +const getType = (method: { + requestStream: boolean; + responseStream: boolean; +}) => { + if (method.requestStream === false && method.responseStream === false) { + return "unary"; + } + return "unknown"; +}; + + +const lookupServiceMetadata = (service: { [x: string]: any }) => { + const serviceKeys = Object.keys(service); + const intersectingMethods = serviceKeys.reduce((acc, k) => { + const method = service[k]; + if (!method) { + throw new Error(`cannot find method ${k} on service`); + } + const components = method.path.split("/"); + acc[k] = { + name: components[1], + method: components[2], + type: getType(method), + path: method.path, + responseType: method.responseType, + requestType: method.requestType, + }; + return acc; + }, {} as any); + + return (key: string) => intersectingMethods[key]; +}; + +export const handler = { + get( + target: { + [x: string]: any; + intercept: () => any; + addService: (arg0: any, arg1: {}) => any; + }, + propKey: string + ) { + if (propKey !== "addService") { + return target[propKey]; + } + return (service: any, implementation: { [x: string]: any }) => { + const newImplementation = {} as any; + const lookup = lookupServiceMetadata(service); + for (const k in implementation) { + const name = k; + const fn = implementation[k]; + newImplementation[name] = (call: any, callback: any) => { + const ctx = { + call, + name, + service: lookup(name), + status: { + code: status.UNKNOWN, + details: "", + }, + }; + const newCallback = (callback: (arg0: any) => void) => { + return (...args: any[]) => { + ctx.status = { + code: status.OK, + details: "", + }; + const err = args[0]; + if (err) { + ctx.status = { + code: status.UNKNOWN, + details: err, + }; + } + // @ts-ignore + callback(...args); + }; + }; + + const interceptors = target.intercept(); + const first = interceptors.next(); + if (!first.value) { + // if we don't have any interceptors + return new Promise((resolve) => { + return resolve(fn(call, newCallback(callback))); + }); + } + first.value(ctx, function next() { + return new Promise((resolve) => { + const i = interceptors.next(); + if (i.done) { + return resolve(fn(call, newCallback(callback))); + } + return resolve(i.value(ctx, next)); + }); + }); + }; + } + return target.addService(service, newImplementation); + }; + }, +}; + +export const createServerProxy = ( + server: Server +): Server & { + intercept: Generator; + interceptors: any[]; + use: (fn: any) => void; +} => { + // @ts-ignore + server.interceptors = []; + // @ts-ignore + server.use = (fn: any) => { + // @ts-ignore + server.interceptors.push(fn); + }; + // @ts-ignore + server.intercept = function* intercept() { + let i = 0; + // @ts-ignore + while (i < server.interceptors.length) { + // @ts-ignore + yield server.interceptors[i]; + i++; + } + }; + // @ts-ignore + return new Proxy(server, handler); +}; \ No newline at end of file diff --git a/services/promotions/src/middleware/log.ts b/services/promotions/src/middleware/log.ts new file mode 100644 index 00000000..093a4a4c --- /dev/null +++ b/services/promotions/src/middleware/log.ts @@ -0,0 +1,42 @@ +import { publish } from "@promotions/lib/amqp"; +import { utils } from "@promotions/lib/log"; +import { log } from "@promotions/lib/log"; + +type Context = { + call: { + request: Map; + }; + service: { + path: string; + }; +}; + +const parseContext = (ctx: Context) => { + const { + call: { request }, + service: { path }, + } = ctx; + return { request, path }; +}; + +export const msg = ( + type: "GRPC" | "AMQP", + path: string | undefined, + requestAt: Date, + responseAt: Date +) => + `${utils.magenta(requestAt.toISOString())} | ${utils[ + type === "GRPC" ? "cyan" : "red" + ](type)}${path ? ` | ${utils.yellow(path)}` : ""} | ${utils.green( + responseAt.getTime() - requestAt.getTime() + "ms" + )} |`; + +export const logGRPC = async (ctx: Context, next: () => Promise) => { + const requestAt = new Date(); + await next(); + const responseAt = new Date(); + let { request, path } = parseContext(ctx); + path = path?.split(".").at(-1) || ""; + publish({ request, path }, "log"); + log.debug(msg("GRPC", path, requestAt, responseAt), JSON.stringify(request)); +}; diff --git a/services/promotions/src/resources/grpc-credentials.ts b/services/promotions/src/resources/grpc-credentials.ts new file mode 100644 index 00000000..75a2b5d5 --- /dev/null +++ b/services/promotions/src/resources/grpc-credentials.ts @@ -0,0 +1,5 @@ +import { credentials, ServerCredentials } from "@grpc/grpc-js"; + +export const insecure = credentials.createInsecure(); + +export const serverInsecure = ServerCredentials.createInsecure(); diff --git a/services/promotions/src/resources/protoloader-options.ts b/services/promotions/src/resources/protoloader-options.ts new file mode 100644 index 00000000..5022d96d --- /dev/null +++ b/services/promotions/src/resources/protoloader-options.ts @@ -0,0 +1,7 @@ +export const options = { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, +}; diff --git a/services/promotions/src/server.ts b/services/promotions/src/server.ts new file mode 100644 index 00000000..e0627e0b --- /dev/null +++ b/services/promotions/src/server.ts @@ -0,0 +1,39 @@ +import "dotenv/config"; +import { resolve as resolvePath } from "path"; + +import { loadSync } from "@grpc/proto-loader"; +import { loadPackageDefinition, Server } from "@grpc/grpc-js"; + +import { log, utils } from "@promotions/lib/log"; +import { options } from "@promotions/resources/protoloader-options"; +import { serverInsecure } from "@promotions/resources/grpc-credentials"; + +import promotionHandler from "@promotions/handler"; +import { createServerProxy } from "@promotions/lib/proxy"; +import { logGRPC } from "@promotions/middleware/log"; + +const PORT = process.env.PORT || 50006; +const ADDRESS = `0.0.0.0:${PORT}`; +const PROTO_PATH = resolvePath(__dirname + "/../../proto/promotions.proto"); + +const packageDefinition = loadSync(PROTO_PATH, options); +const grpc = loadPackageDefinition(packageDefinition) as any; +const { + PromotionService: { service: ds }, +} = grpc.com.goodfood.promotions; + +const server = createServerProxy(new Server()); +server.addService(ds, promotionHandler); +server.use(logGRPC); + +server.bindAsync(ADDRESS, serverInsecure, () => { + server.start(); + const message = `---- ${utils.green("good")}${utils.yellow( + "food" + )} Promotions Service ----\nstarted on: ${utils.bold(ADDRESS)} ${utils.green( + "✓" + )}\n`; + log.debug(message); +}); + +export default server; diff --git a/services/promotions/src/types/index.d.ts b/services/promotions/src/types/index.d.ts new file mode 100644 index 00000000..edf2bebf --- /dev/null +++ b/services/promotions/src/types/index.d.ts @@ -0,0 +1,3 @@ +export type Data = { + request: T; +}; diff --git a/services/promotions/src/types/promotion.d.ts b/services/promotions/src/types/promotion.d.ts new file mode 100644 index 00000000..4c401b41 --- /dev/null +++ b/services/promotions/src/types/promotion.d.ts @@ -0,0 +1,36 @@ +import { Method } from "@prisma/client"; + +export type Promotion = { + id: string; + + code: string; + reduction: string; + method: Method; + + restaurant_id: string; +} + + +export type PromotionCreateInput = { + code: string; + reduction: string; + method: Method; + + restaurant_id: string; +} + +export type PromotionId = { + id: Promotion["id"]; +} + +export type RestaurantId = { + id: string; +} + +export type PromotionCode = { + code: Promotion["code"]; +} + +export type PromotionList = { + promotions: Promotion[]; +} \ No newline at end of file diff --git a/services/promotions/tsconfig.json b/services/promotions/tsconfig.json new file mode 100644 index 00000000..4eb46577 --- /dev/null +++ b/services/promotions/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "sourceMap": true, + "outDir": "dist", + "strict": true, + "lib": ["esnext"], + "esModuleInterop": true, + "baseUrl": ".", + "paths": { + "@promotions/*": ["src/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"], + "ts-node": { + "require": ["tsconfig-paths/register"] + } +} diff --git a/services/proto/promotions.proto b/services/proto/promotions.proto new file mode 100644 index 00000000..43333e43 --- /dev/null +++ b/services/proto/promotions.proto @@ -0,0 +1,53 @@ +syntax = "proto3"; +import "google/protobuf/empty.proto"; + +package com.goodfood.promotions; + +message Promotion { + string id = 1; + + string code = 2; + string reduction = 3; + Method method = 4; + + string restaurant_id = 5; +} + +message PromotionCreateInput{ + string code = 1; + string reduction = 2; + Method method = 3; + + string restaurant_id = 4; +} + +message PromotionId{ + string id = 1; +} + +message RestaurantId{ + string id = 1; +} + +message PromotionCode{ + string code = 1; +} + +message PromotionList{ + repeated Promotion promotions = 1; +} + +enum Method { + PERCENT = 0; + VALUE = 1; +} + +service PromotionService { + rpc CreatePromotion (PromotionCreateInput) returns (Promotion) {} + rpc GetPromotion (PromotionCode) returns (Promotion) {} + rpc UpdatePromotion (Promotion) returns (Promotion) {} + rpc DeletePromotion (PromotionId) returns (google.protobuf.Empty) {} + rpc GetPromotions (google.protobuf.Empty) returns (PromotionList) {} + rpc GetPromotionsByRestaurant (RestaurantId) returns (PromotionList) {} + +}