diff --git a/.github/workflows/notifications.yml b/.github/workflows/notifications.yml new file mode 100644 index 00000000..0596cd95 --- /dev/null +++ b/.github/workflows/notifications.yml @@ -0,0 +1,69 @@ +name: Notification Microservice CI/CD + +on: + push: + branches: [ notification ] + 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/notification/pnpm-lock.yaml + - name: Install dependencies + run: | + cd ./services/notification + pnpm install --frozen-lockfile + - name: Build + run: | + cd ./services/notification + 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/notification/pnpm-lock.yaml + - name: Install dependencies + run: | + cd ./services/notification + pnpm install --frozen-lockfile + - name: Execute tests + run: | + cd ./services/notification + 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/notification/Dockerfile + push: true + tags: pierrelbg/goodfood-notification:latest \ No newline at end of file diff --git a/README.md b/README.md index 66e93439..155f8b10 100644 --- a/README.md +++ b/README.md @@ -15,21 +15,22 @@ a high volume of concurrent users, up to several thousand. ## Microservices ports -| Service | Port | Language | Database | Status | Assignee | -| ----------- | ----- | ----------- | ---------- | ------ | --------------- | -| Gateway | 50000 | Go | ❌ | ❌ | @Anatole-Godard | -| User (auth) | 50001 | Go | PostgreSQL | ⚠️ | @Anatole-Godard | -| Basket | 50002 | NodeJS (ts) | Redis | ⚠️ | @Anatole-Godard | -| Payment | 50003 | NodeJS (ts) | PostgreSQL | ✅ | @floriaaan | -| Product | 50004 | NodeJS (ts) | PostgreSQL | ❌ | @PierreLbg | -| Restaurant | 50005 | NodeJS (ts) | PostgreSQL | ❌ | @floriaaan | -| Promotion | 50006 | NodeJS (ts) | PostgreSQL | ❌ | @PierreLbg | -| Order | 50007 | NodeJS (ts) | PostgreSQL | ✅ | @floriaaan | -| Delivery | 50008 | NodeJS (ts) | PostgreSQL | ✅ | @floriaaan | -| Stock | 50009 | NodeJS (ts) | PostgreSQL | ⚠️ | @floriaaan | -| Reporting | 50020 | C# (dotnet) | PostgreSQL | ⚠️ | @floriaaan | -| Log | 50021 | Go | PostgreSQL | ✅ | @floriaaan | -| (...) | (...) | (...) | (...) | (...) | +| Service | Port | Language | Database | Status | Assignee | +| ------------ | ----- | ----------- | ---------- | ------ | --------------- | +| Gateway | 50000 | Go | ❌ | ❌ | @Anatole-Godard | +| User (auth) | 50001 | Go | PostgreSQL | ⚠️ | @Anatole-Godard | +| Basket | 50002 | NodeJS (ts) | Redis | ⚠️ | @Anatole-Godard | +| Payment | 50003 | NodeJS (ts) | PostgreSQL | ✅ | @floriaaan | +| Product | 50004 | NodeJS (ts) | PostgreSQL | ❌ | @PierreLbg | +| Restaurant | 50005 | NodeJS (ts) | PostgreSQL | ❌ | @floriaaan | +| Promotion | 50006 | NodeJS (ts) | PostgreSQL | ❌ | @PierreLbg | +| Order | 50007 | NodeJS (ts) | PostgreSQL | ✅ | @floriaaan | +| Delivery | 50008 | NodeJS (ts) | PostgreSQL | ✅ | @floriaaan | +| Stock | 50009 | NodeJS (ts) | PostgreSQL | ⚠️ | @floriaaan | +| Reporting | 50020 | C# (dotnet) | PostgreSQL | ⚠️ | @floriaaan | +| Log | 50021 | Go | PostgreSQL | ✅ | @floriaaan | +| Notification | 50022 | NodeJS (ts) | PostgreSQL | ✅ | @PierreLbg | +| (...) | (...) | (...) | (...) | (...) | ## File Hierarchy diff --git a/services/notification/.dockerignore b/services/notification/.dockerignore new file mode 100644 index 00000000..b7dab5e9 --- /dev/null +++ b/services/notification/.dockerignore @@ -0,0 +1,2 @@ +node_modules +build \ No newline at end of file diff --git a/services/notification/.env.example b/services/notification/.env.example new file mode 100644 index 00000000..c8470406 --- /dev/null +++ b/services/notification/.env.example @@ -0,0 +1,6 @@ +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/notifications +AMQP_URL=amqp://guest:guest@localhost +PLUNK_PUBLIC_KEY="exemple" +PLUNk_PROJECT_NAME=goodfood + +PORT=50022 \ No newline at end of file diff --git a/services/notification/.gitignore b/services/notification/.gitignore new file mode 100644 index 00000000..5240516d --- /dev/null +++ b/services/notification/.gitignore @@ -0,0 +1,4 @@ +node_modules +# Keep environment variables out of version control +.env +dist/ diff --git a/services/notification/Dockerfile b/services/notification/Dockerfile new file mode 100644 index 00000000..e8685fd5 --- /dev/null +++ b/services/notification/Dockerfile @@ -0,0 +1,38 @@ +FROM node:18-alpine3.17 as builder + +# Set working directory +WORKDIR /app + +# Copy the application code +COPY ./notification/ . + +# 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 50022 + +# Start the server +CMD [ "node", "index.js"] diff --git a/services/notification/README.md b/services/notification/README.md new file mode 100644 index 00000000..2394966c --- /dev/null +++ b/services/notification/README.md @@ -0,0 +1,83 @@ +# Notification Microservice + +| Informations | +|--------------------------------------------------------------------| +| **Port:** 50022 | +| **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 + +- Notifications model: + + - `CreateNotification`: Creates a new notification in the system. + - `GetNotification`: Retrieves a notification by its ID. + - `UpdateNotification`: Updates an existing notification. + - `DeleteNotification`: Deletes a notification by its ID. + - `GetNotifications`: Retrieves notifications for a given message type. + +## 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/notification`) 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:50022`. + +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-notification:1.0.0 -f ./notification/Dockerfile . +docker tag goodfood-notification:1.0.0 pierrelbg/goodfood-notification:1.0.0 +docker push pierrelbg/goodfood-notification:1.0.0 +``` + +### Run + +Create the .env base on the .env.example. Then run the following command: + +``` +docker run --env-file=.env goodfood-notification:1.0.0 +``` diff --git a/services/notification/package.json b/services/notification/package.json new file mode 100644 index 00000000..61f2828b --- /dev/null +++ b/services/notification/package.json @@ -0,0 +1,30 @@ +{ + "name": "@goodfood/notification", + "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 --minify", + "test": "echo 'tests are disabled'", + "prisma:generate": "prisma generate", + "postinstall": "npm run prisma:generate" + }, + "devDependencies": { + "prisma": "^5.1.1", + "typescript": "^5.1.6" + }, + "dependencies": { + "@grpc/grpc-js": "^1.9.0", + "@grpc/proto-loader": "^0.7.8", + "@plunk/node": "^2.0.0", + "@prisma/client": "5.1.0", + "@types/amqplib": "^0.10.1", + "amqplib": "^0.10.3", + "dotenv": "^16.3.1", + "esbuild": "^0.19.2", + "nodemon": "^3.0.1", + "path": "^0.12.7", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0" + } +} diff --git a/services/notification/pnpm-lock.yaml b/services/notification/pnpm-lock.yaml new file mode 100644 index 00000000..7043033b --- /dev/null +++ b/services/notification/pnpm-lock.yaml @@ -0,0 +1,1039 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@grpc/grpc-js': + specifier: ^1.9.0 + version: 1.9.0 + '@grpc/proto-loader': + specifier: ^0.7.8 + version: 0.7.8 + '@plunk/node': + specifier: ^2.0.0 + version: 2.0.0 + '@prisma/client': + specifier: 5.1.0 + version: 5.1.0(prisma@5.1.1) + '@types/amqplib': + specifier: ^0.10.1 + version: 0.10.1 + amqplib: + specifier: ^0.10.3 + version: 0.10.3 + dotenv: + specifier: ^16.3.1 + version: 16.3.1 + esbuild: + specifier: ^0.19.2 + version: 0.19.2 + 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.5.1)(typescript@5.1.6) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 + +devDependencies: + prisma: + specifier: ^5.1.1 + version: 5.1.1 + 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.19.2: + resolution: {integrity: sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@esbuild/android-arm@0.19.2: + resolution: {integrity: sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@esbuild/android-x64@0.19.2: + resolution: {integrity: sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@esbuild/darwin-arm64@0.19.2: + resolution: {integrity: sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@esbuild/darwin-x64@0.19.2: + resolution: {integrity: sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@esbuild/freebsd-arm64@0.19.2: + resolution: {integrity: sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/freebsd-x64@0.19.2: + resolution: {integrity: sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-arm64@0.19.2: + resolution: {integrity: sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-arm@0.19.2: + resolution: {integrity: sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-ia32@0.19.2: + resolution: {integrity: sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-loong64@0.19.2: + resolution: {integrity: sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-mips64el@0.19.2: + resolution: {integrity: sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-ppc64@0.19.2: + resolution: {integrity: sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-riscv64@0.19.2: + resolution: {integrity: sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-s390x@0.19.2: + resolution: {integrity: sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-x64@0.19.2: + resolution: {integrity: sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/netbsd-x64@0.19.2: + resolution: {integrity: sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/openbsd-x64@0.19.2: + resolution: {integrity: sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/sunos-x64@0.19.2: + resolution: {integrity: sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: false + optional: true + + /@esbuild/win32-arm64@0.19.2: + resolution: {integrity: sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@esbuild/win32-ia32@0.19.2: + resolution: {integrity: sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@esbuild/win32-x64@0.19.2: + resolution: {integrity: sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@grpc/grpc-js@1.9.0: + resolution: {integrity: sha512-H8+iZh+kCE6VR/Krj6W28Y/ZlxoZ1fOzsNt77nrdE3knkbSelW1Uus192xOFCxHyeszLj8i4APQkSIXjAoOxXg==} + engines: {node: ^8.13.0 || >=10.10.0} + dependencies: + '@grpc/proto-loader': 0.7.8 + '@types/node': 20.5.1 + 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 + + /@plunk/node@2.0.0: + resolution: {integrity: sha512-53lgots3fWGAo1QdS18BdEpJl7A29O1F9rYVn/7DfJ07SpJ1ZlzUeeWGVrWGL7PRRZb4a9Tw7Tt8Wnw0Xorhjg==} + dependencies: + native-fetch: 4.0.2(undici@5.23.0) + tslib: 2.6.2 + undici: 5.23.0 + dev: false + + /@prisma/client@5.1.0(prisma@5.1.1): + resolution: {integrity: sha512-aIxuXlH3p3Vy91buodQhYgAD9m0yuxJzpXMhc1ejQ/WN3pNNWzBA0iDcBObLq8DMdszcXmoLAk3hiRq/ZwBsOw==} + engines: {node: '>=16.13'} + requiresBuild: true + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + dependencies: + '@prisma/engines-version': 5.1.0-28.a9b7003df90aa623086e4d6f4e43c72468e6339b + prisma: 5.1.1 + dev: false + + /@prisma/engines-version@5.1.0-28.a9b7003df90aa623086e4d6f4e43c72468e6339b: + resolution: {integrity: sha512-jTwE2oy1yjICmTfnCR0ASIpuMZXZ18sUzQXB7V0RMbrM9OlcmbUwXPuYhnxXuWN8XwRmujeIhsXs/Zeh+fjPOQ==} + dev: false + + /@prisma/engines@5.1.1: + resolution: {integrity: sha512-NV/4nVNWFZSJCCIA3HIFJbbDKO/NARc9ej0tX5S9k2EVbkrFJC4Xt9b0u4rNZWL4V+F5LAjvta8vzEUw0rw+HA==} + 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.5.1 + dev: false + + /@types/long@4.0.2: + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + dev: false + + /@types/node@20.5.1: + resolution: {integrity: sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==} + dev: false + + /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 + + /busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + 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.19.2: + resolution: {integrity: sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.19.2 + '@esbuild/android-arm64': 0.19.2 + '@esbuild/android-x64': 0.19.2 + '@esbuild/darwin-arm64': 0.19.2 + '@esbuild/darwin-x64': 0.19.2 + '@esbuild/freebsd-arm64': 0.19.2 + '@esbuild/freebsd-x64': 0.19.2 + '@esbuild/linux-arm': 0.19.2 + '@esbuild/linux-arm64': 0.19.2 + '@esbuild/linux-ia32': 0.19.2 + '@esbuild/linux-loong64': 0.19.2 + '@esbuild/linux-mips64el': 0.19.2 + '@esbuild/linux-ppc64': 0.19.2 + '@esbuild/linux-riscv64': 0.19.2 + '@esbuild/linux-s390x': 0.19.2 + '@esbuild/linux-x64': 0.19.2 + '@esbuild/netbsd-x64': 0.19.2 + '@esbuild/openbsd-x64': 0.19.2 + '@esbuild/sunos-x64': 0.19.2 + '@esbuild/win32-arm64': 0.19.2 + '@esbuild/win32-ia32': 0.19.2 + '@esbuild/win32-x64': 0.19.2 + 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 + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + 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 + + /native-fetch@4.0.2(undici@5.23.0): + resolution: {integrity: sha512-4QcVlKFtv2EYVS5MBgsGX5+NWKtbDbIECdUXDBGDMAZXq3Jkv9zf+y8iS7Ub8fEdga3GpYeazp9gauNqXHJOCg==} + peerDependencies: + undici: '*' + dependencies: + undici: 5.23.0 + 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.1.1: + resolution: {integrity: sha512-WJFG/U7sMmcc6TjJTTifTfpI6Wjoh55xl4AzopVwAdyK68L9/ogNo8QQ2cxuUjJf/Wa82z/uhyh3wMzvRIBphg==} + engines: {node: '>=16.13'} + hasBin: true + requiresBuild: true + dependencies: + '@prisma/engines': 5.1.1 + + /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.5.1 + 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.4 + 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 + + /streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + 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.5.1)(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.5.1 + 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 + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + 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 + + /undici@5.23.0: + resolution: {integrity: sha512-1D7w+fvRsqlQ9GscLBwcAJinqcZGHUKjbOmXdlE/v8BvEGXjeWAax+341q44EuTcHXXnfyKNbKRq4Lg7OzhMmg==} + engines: {node: '>=14.0'} + dependencies: + busboy: 1.6.0 + 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/notification/prisma/migrations/20230819164921_init/migration.sql b/services/notification/prisma/migrations/20230819164921_init/migration.sql new file mode 100644 index 00000000..2c63ed12 --- /dev/null +++ b/services/notification/prisma/migrations/20230819164921_init/migration.sql @@ -0,0 +1,12 @@ +-- CreateEnum +CREATE TYPE "MessageType" AS ENUM ('USER_REQUEST', 'OUTPUT'); + +-- CreateTable +CREATE TABLE "Notification" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "message" TEXT NOT NULL, + "message_type" "MessageType" NOT NULL, + + CONSTRAINT "Notification_pkey" PRIMARY KEY ("id") +); diff --git a/services/notification/prisma/migrations/migration_lock.toml b/services/notification/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..fbffa92c --- /dev/null +++ b/services/notification/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/notification/prisma/schema.prisma b/services/notification/prisma/schema.prisma new file mode 100644 index 00000000..c6a0806b --- /dev/null +++ b/services/notification/prisma/schema.prisma @@ -0,0 +1,23 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Notification { + id String @id @default(cuid()) + title String + message String + message_type MessageType +} + +enum MessageType { + USER_REQUEST + OUTPUT +} diff --git a/services/notification/src/handler/create.ts b/services/notification/src/handler/create.ts new file mode 100644 index 00000000..75c088c6 --- /dev/null +++ b/services/notification/src/handler/create.ts @@ -0,0 +1,27 @@ +import { NotificationCreateInput, Notification } from "@notifications/types/notification"; +import { log } from "@notifications/lib/log"; +import { Data } from "@notifications/types"; +import prisma from "@notifications/lib/prisma"; +import { MessageType } from "@prisma/client"; + +export const CreateNotification = async ( + { request }: Data, + callback: (err: any, response: Notification | null) => void +) => { + try { + const { message, message_type, title } = request; + + const notifications = await prisma.notification.create({ + data: { + title, + message, + message_type: message_type as unknown as MessageType + }, + }) as unknown as Notification; + + callback(null, notifications); + } catch (error) { + log.error(error); + callback(error, null); + } +}; \ No newline at end of file diff --git a/services/notification/src/handler/delete.ts b/services/notification/src/handler/delete.ts new file mode 100644 index 00000000..123ad128 --- /dev/null +++ b/services/notification/src/handler/delete.ts @@ -0,0 +1,20 @@ +import { NotificationId } from "@notifications/types/notification"; +import { log } from "@notifications/lib/log"; +import { Data } from "@notifications/types"; +import prisma from "@notifications/lib/prisma"; + +export const DeleteNotification = async ( + { request }: Data, + callback: (err: any, response: null) => void +) => { + try { + const { id } = request; + + await prisma.notification.delete({ where : { id } }); + + callback(null, null); + } catch (error) { + log.error(error); + callback(error, null); + } +}; \ No newline at end of file diff --git a/services/notification/src/handler/get.ts b/services/notification/src/handler/get.ts new file mode 100644 index 00000000..7b9ed42f --- /dev/null +++ b/services/notification/src/handler/get.ts @@ -0,0 +1,20 @@ +import { NotificationId, Notification } from "@notifications/types/notification"; +import { log } from "@notifications/lib/log"; +import { Data } from "@notifications/types"; +import prisma from "@notifications/lib/prisma"; + +export const GetNotification = async ( + { request }: Data, + callback: (err: any, response: Notification | null) => void +) => { + try { + const { id } = request; + + const notification = await prisma.notification.findFirstOrThrow({ where: { id } }) as unknown as Notification; + + callback(null, notification); + } catch (error) { + log.error(error); + callback(error, null); + } +}; \ No newline at end of file diff --git a/services/notification/src/handler/index.ts b/services/notification/src/handler/index.ts new file mode 100644 index 00000000..7c4672a8 --- /dev/null +++ b/services/notification/src/handler/index.ts @@ -0,0 +1,13 @@ +import { CreateNotification } from "@notifications/handler/create"; +import { GetNotification } from "@notifications/handler/get"; +import { UpdateNotification } from "@notifications/handler/update"; +import { DeleteNotification } from "@notifications/handler/delete"; +import { GetNotifications } from "@notifications/handler/listByType"; + +export default { + CreateNotification, + GetNotification, + UpdateNotification, + DeleteNotification, + GetNotifications, +}; diff --git a/services/notification/src/handler/listByType.ts b/services/notification/src/handler/listByType.ts new file mode 100644 index 00000000..d8e7cf03 --- /dev/null +++ b/services/notification/src/handler/listByType.ts @@ -0,0 +1,24 @@ +import { MessageType, MessageTypeInput, NotificationList } from "@notifications/types/notification"; +import { log } from "@notifications/lib/log"; +import { Data } from "@notifications/types"; +import prisma from "@notifications/lib/prisma"; + +export const GetNotifications = async ( + { request }: Data, + callback: (err: any, response: NotificationList | null) => void +) => { + try { + const { message_type } = request; + + log.debug(message_type); + + const notifications = { + notifications: await prisma.notification.findMany({ where: { message_type: message_type as MessageType } }) + } as unknown as NotificationList; + + callback(null, notifications); + } catch (error) { + log.error(error); + callback(error, null); + } +}; \ No newline at end of file diff --git a/services/notification/src/handler/update.ts b/services/notification/src/handler/update.ts new file mode 100644 index 00000000..eaff4212 --- /dev/null +++ b/services/notification/src/handler/update.ts @@ -0,0 +1,28 @@ +import { Notification } from "@notifications/types/notification"; +import { log } from "@notifications/lib/log"; +import { Data } from "@notifications/types"; +import prisma from "@notifications/lib/prisma"; +import { MessageType } from "@prisma/client"; + +export const UpdateNotification = async ( + { request }: Data, + callback: (err: any, response: Notification | null) => void +) => { + try { + const { id, title, message, message_type } = request; + + const notification = await prisma.notification.update({ + where: { id }, + data: { + title, + message, + message_type: message_type as unknown as MessageType + }, + }) as unknown as Notification; + + callback(null, notification); + } catch (error) { + log.error(error); + callback(error, null); + } +}; \ No newline at end of file diff --git a/services/notification/src/lib/amqp.ts b/services/notification/src/lib/amqp.ts new file mode 100644 index 00000000..22333df5 --- /dev/null +++ b/services/notification/src/lib/amqp.ts @@ -0,0 +1,84 @@ +import amqp from "amqplib"; +import plunk from "@notifications/lib/plunk"; +import { msg } from "@notifications/middleware/log"; +import { log } from "@notifications/lib/log"; +import { Data } from "@notifications/types"; +import { NotificationCreateInput, MessageType } from "@notifications/types/notification"; +import { CreateNotification } from "@notifications/handler/create"; + +const DEFAULT_QUEUE = "log"; +const DEFAULT_SUB_QUEUE = "notification"; + +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) + ); +}; + +const toNotification = (message: any) => { + const { email, title, messageTexte } = JSON.parse(message.content.toString()); + return { email: email, title: title, messageTexte: messageTexte, messageType: MessageType.OUTPUT }; +} + +export const subscribe = async (queue = DEFAULT_SUB_QUEUE) => { + const channel = await connectQueue(queue); + if (channel instanceof Error) + return console.log("Error to connect to queue: ", channel); + channel.consume( + queue, + async function(message) { + const { email, title, messageTexte, messageType } = toNotification(message); + const notification = { + request: + { + title: title, + message: messageTexte, + message_type: messageType as unknown as MessageType + } + } as Data + + CreateNotification(notification, () => {}); + + const { success } = await plunk.emails.send({ + to: email, + body: messageTexte, + subject: title + }); + + if(success) + log.debug( + msg("AMQP", `${queue} (queue)`, new Date(), new Date()), + "Email send" + ); + else + log.debug( + msg("AMQP", `${queue} (queue)`, new Date(), new Date()), + "Email not send" + ); + }, + { + noAck: true + } + ) +} diff --git a/services/notification/src/lib/log.ts b/services/notification/src/lib/log.ts new file mode 100644 index 00000000..2404af54 --- /dev/null +++ b/services/notification/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/notification/src/lib/plunk.ts b/services/notification/src/lib/plunk.ts new file mode 100644 index 00000000..85ff1929 --- /dev/null +++ b/services/notification/src/lib/plunk.ts @@ -0,0 +1,6 @@ +import Plunk from "@plunk/node"; +const plunkKey = process.env.PLUNK_PUBLIC_KEY; +if (!plunkKey) throw new Error("Plunk API key not found"); + +const plunk = new Plunk(plunkKey); +export default plunk; \ No newline at end of file diff --git a/services/notification/src/lib/prisma.ts b/services/notification/src/lib/prisma.ts new file mode 100644 index 00000000..4d495ef0 --- /dev/null +++ b/services/notification/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/notification/src/lib/proxy.ts b/services/notification/src/lib/proxy.ts new file mode 100644 index 00000000..b05d24a6 --- /dev/null +++ b/services/notification/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/notification/src/middleware/log.ts b/services/notification/src/middleware/log.ts new file mode 100644 index 00000000..be264f6b --- /dev/null +++ b/services/notification/src/middleware/log.ts @@ -0,0 +1,42 @@ +import { publish } from "@notifications/lib/amqp"; +import { utils } from "@notifications/lib/log"; +import { log } from "@notifications/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/notification/src/resources/grpc-credentials.ts b/services/notification/src/resources/grpc-credentials.ts new file mode 100644 index 00000000..75a2b5d5 --- /dev/null +++ b/services/notification/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/notification/src/resources/protoloader-options.ts b/services/notification/src/resources/protoloader-options.ts new file mode 100644 index 00000000..5022d96d --- /dev/null +++ b/services/notification/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/notification/src/server.ts b/services/notification/src/server.ts new file mode 100644 index 00000000..71c9e75a --- /dev/null +++ b/services/notification/src/server.ts @@ -0,0 +1,41 @@ +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 "@notifications/lib/log"; +import { options } from "@notifications/resources/protoloader-options"; +import { serverInsecure } from "@notifications/resources/grpc-credentials"; + +import notificationHandler from "@notifications/handler"; +import { createServerProxy } from "@notifications/lib/proxy"; +import { logGRPC } from "@notifications/middleware/log"; +import { subscribe } from "@notifications/lib/amqp"; + +const PORT = process.env.PORT || 50022; +const ADDRESS = `0.0.0.0:${PORT}`; +const PROTO_PATH = resolvePath(__dirname + "/../../proto/notification.proto"); + +const packageDefinition = loadSync(PROTO_PATH, options); +const grpc = loadPackageDefinition(packageDefinition) as any; +const { + NotificationService: { service: ns }, +} = grpc.com.goodfood.notification; +subscribe(); + +const server = createServerProxy(new Server()); +server.addService(ns, notificationHandler); +server.use(logGRPC); + +server.bindAsync(ADDRESS, serverInsecure, () => { + server.start(); + const message = `---- ${utils.green("good")}${utils.yellow( + "food" + )} Notification Service ----\nstarted on: ${utils.bold(ADDRESS)} ${utils.green( + "✓" + )}\n`; + log.debug(message); +}); + +export default server; diff --git a/services/notification/src/types/index.d.ts b/services/notification/src/types/index.d.ts new file mode 100644 index 00000000..edf2bebf --- /dev/null +++ b/services/notification/src/types/index.d.ts @@ -0,0 +1,3 @@ +export type Data = { + request: T; +}; diff --git a/services/notification/src/types/notification.ts b/services/notification/src/types/notification.ts new file mode 100644 index 00000000..0c5e4d85 --- /dev/null +++ b/services/notification/src/types/notification.ts @@ -0,0 +1,30 @@ +export interface Notification { + id: string; + + title: string; + message: string; + message_type: MessageType; +} + +export interface NotificationCreateInput { + title: string; + message: string; + message_type: MessageType; +} + +export interface NotificationId { + id: string; +} + +export interface NotificationList { + notifications: Notification[]; +} + +export interface MessageTypeInput { + message_type: string; +} + +export const enum MessageType { + USER_REQUEST = "USER_REQUEST", + OUTPUT = "OUTPUT" +} diff --git a/services/notification/tsconfig.json b/services/notification/tsconfig.json new file mode 100644 index 00000000..bf927d60 --- /dev/null +++ b/services/notification/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "sourceMap": true, + "outDir": "dist", + "strict": true, + "lib": ["esnext"], + "esModuleInterop": true, + "baseUrl": ".", + "paths": { + "@notifications/*": ["src/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"], + "ts-node": { + "require": ["tsconfig-paths/register"] + } +} diff --git a/services/proto/notification.proto b/services/proto/notification.proto new file mode 100644 index 00000000..2406bea9 --- /dev/null +++ b/services/proto/notification.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; +import "google/protobuf/empty.proto"; + +package com.goodfood.notification; + +enum MessageType { + USER_REQUEST = 1; + OUTPUT = 2; +} + +message MessageTypeInput { + MessageType message_type = 1; +} + +message Notification { + string id = 1; + + string title = 2; + string message = 3; + + MessageType message_type = 4; +} + +message NotificationCreateInput { + string title = 2; + string message = 3; + MessageType message_type = 4; +} + +message NotificationId { + string id = 1; +} + +message NotificationList { + repeated Notification notifications = 1; +} + +service NotificationService { + rpc CreateNotification (NotificationCreateInput) returns (Notification) {} + rpc GetNotification (NotificationId) returns (Notification) {} + rpc UpdateNotification (Notification) returns (Notification) {} + rpc DeleteNotification (NotificationId) returns (google.protobuf.Empty) {} + rpc GetNotifications (MessageTypeInput) returns (NotificationList) {} +}