diff --git a/.github/workflows/restaurant.yml b/.github/workflows/restaurant.yml new file mode 100644 index 00000000..5bdb83ef --- /dev/null +++ b/.github/workflows/restaurant.yml @@ -0,0 +1,69 @@ +name: Restaurant Microservice CI/CD + +on: + push: + branches: [ restaurant ] + 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/restaurant/pnpm-lock.yaml + - name: Install dependencies + run: | + cd ./services/restaurant + pnpm install --frozen-lockfile + - name: Build + run: | + cd ./services/restaurant + 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/restaurant/pnpm-lock.yaml + - name: Install dependencies + run: | + cd ./services/restaurant + pnpm install --frozen-lockfile + - name: Execute tests + run: | + cd ./services/restaurant + 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/restaurant/Dockerfile + push: true + tags: floriaaan/goodfood-restaurant:latest \ No newline at end of file diff --git a/README.md b/README.md index b21c638f..66e93439 100644 --- a/README.md +++ b/README.md @@ -16,19 +16,19 @@ a high volume of concurrent users, up to several thousand. ## Microservices ports | Service | Port | Language | Database | Status | Assignee | -|-------------|-------|-------------|------------|--------|-----------------| -| Gateway | 50000 | Go | ❌ | ❌ | @Anatole-Godard | +| ----------- | ----- | ----------- | ---------- | ------ | --------------- | +| 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 | ❌ | @PierreLbg | -| Promotion | 50006 | NodeJS (ts) | PostgreSQL | ❌ | @PierreLbg | -| Order | 50007 | NodeJS (ts) | PostgreSQL | ✅ | @floriaaan | -| Delivery | 50008 | NodeJS (ts) | PostgreSQL | ✅ | @floriaaan | +| 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 | +| Log | 50021 | Go | PostgreSQL | ✅ | @floriaaan | | (...) | (...) | (...) | (...) | (...) | ## File Hierarchy diff --git a/services/proto/restaurant.proto b/services/proto/restaurant.proto new file mode 100644 index 00000000..ecf87eec --- /dev/null +++ b/services/proto/restaurant.proto @@ -0,0 +1,53 @@ +syntax = "proto3"; +import "google/protobuf/empty.proto"; + +package com.goodfood.restaurant; + +message Restaurant { + string id = 1; + + string name = 2; + optional string description = 3; + + repeated float location = 4; + optional string address = 5; + + repeated string openingHours = 6; + optional string phone = 7; + + string createdAt = 8; + string updatedAt = 9; +} + +message RestaurantCreateInput{ + string name = 1; + optional string description = 2; + + repeated float location = 3; + optional string address = 4; + + repeated string openingHours = 5; + optional string phone = 6; +} + +message RestaurantList { + repeated Restaurant restaurants = 1; +} + +message ByLocationInput { + repeated float location = 1; +} + +message RestaurantId { + string id = 1; +} + + +service RestaurantService { + rpc CreateRestaurant (RestaurantCreateInput) returns (Restaurant) {} + rpc GetRestaurant (RestaurantId) returns (Restaurant) {} + rpc UpdateRestaurant (Restaurant) returns (Restaurant) {} + rpc DeleteRestaurant (RestaurantId) returns (google.protobuf.Empty) {} + rpc GetRestaurants (google.protobuf.Empty) returns (RestaurantList) {} + rpc GetRestaurantsByLocation (ByLocationInput) returns (RestaurantList) {} +} diff --git a/services/restaurant/.dockerignore b/services/restaurant/.dockerignore new file mode 100644 index 00000000..b7dab5e9 --- /dev/null +++ b/services/restaurant/.dockerignore @@ -0,0 +1,2 @@ +node_modules +build \ No newline at end of file diff --git a/services/restaurant/.env.example b/services/restaurant/.env.example new file mode 100644 index 00000000..ff61e645 --- /dev/null +++ b/services/restaurant/.env.example @@ -0,0 +1,4 @@ +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres +AMQP_URL=amqp://guest:guest@localhost + +PORT=50005 \ No newline at end of file diff --git a/services/restaurant/.gitignore b/services/restaurant/.gitignore new file mode 100644 index 00000000..bab9187d --- /dev/null +++ b/services/restaurant/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +bin/ +*.env* +!*.env.example \ No newline at end of file diff --git a/services/restaurant/Dockerfile b/services/restaurant/Dockerfile new file mode 100644 index 00000000..c8ec816f --- /dev/null +++ b/services/restaurant/Dockerfile @@ -0,0 +1,38 @@ +FROM node:18-alpine3.17 as builder + +# Set working directory +WORKDIR /app + +# Copy the application code +COPY ./restaurant/ . + +# 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 50005 + +# Start the server +CMD [ "node", "index.js"] diff --git a/services/restaurant/README.md b/services/restaurant/README.md new file mode 100644 index 00000000..e94e1bc8 --- /dev/null +++ b/services/restaurant/README.md @@ -0,0 +1,77 @@ +# Restaurant Microservice + +| Informations | +| ------------------------------------------------------------------ | +| **Port:** 50005 | +| **Developer:** @floriaaan | +| **Status:** In progress | +| **Last update:** 2023-08-19 | +| **Language:** NodeJS | +| **Dependencies:** TypeScript, Prisma, gRPC, Postgres | +| **Models:** (see [`prisma/schema.prisma`](./prisma/schema.prisma)) | + +## gRPC Methods + +TBW + +## 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-restaurant:1.0.0 -f ./restaurant/Dockerfile . +docker tag goodfood-restaurant:1.0.0 pierrelbg/goodfood-restaurant:1.0.0 +docker push floriaaan/goodfood-restaurant:1.0.0 +``` + +### Run + +Create the .env base on the .env.example. Then run the following command: + +``` +docker run --env-file=.env goodfood-restaurant:1.0.0 +``` diff --git a/services/restaurant/k8s/configmap.yml b/services/restaurant/k8s/configmap.yml new file mode 100644 index 00000000..920cfdea --- /dev/null +++ b/services/restaurant/k8s/configmap.yml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: goodfood-restaurant-configmap + labels: + app: goodfood-restaurant +data: + port: "50005" \ No newline at end of file diff --git a/services/restaurant/k8s/deployment.yml b/services/restaurant/k8s/deployment.yml new file mode 100644 index 00000000..435d0787 --- /dev/null +++ b/services/restaurant/k8s/deployment.yml @@ -0,0 +1,31 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: goodfood-restaurant + labels: + app: goodfood-restaurant +spec: + selector: + matchLabels: + app: goodfood-restaurant + template: + metadata: + labels: + app: goodfood-restaurant + spec: + containers: + - name: goodfood-restaurant + image: floriaaan/goodfood-restaurant:1.0.0 + imagePullPolicy: Always + resources: + limits: + memory: "128Mi" + cpu: "200m" + env: + - name: PORT + valueFrom: + configMapKeyRef: + name: goodfood-restaurant-configmap + key: port + ports: + - containerPort: 50005 diff --git a/services/restaurant/k8s/hpa.yml b/services/restaurant/k8s/hpa.yml new file mode 100644 index 00000000..146ad52d --- /dev/null +++ b/services/restaurant/k8s/hpa.yml @@ -0,0 +1,18 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: goodfood-restaurant-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: goodfood-restaurant + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 diff --git a/services/restaurant/k8s/ingress.yml b/services/restaurant/k8s/ingress.yml new file mode 100644 index 00000000..02b3d456 --- /dev/null +++ b/services/restaurant/k8s/ingress.yml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: goodfood-restaurant +spec: + ingressClassName: nginx + rules: + - host: restaurant.localdev.me + http: + paths: + - pathType: Prefix + backend: + service: + name: goodfood-restaurant + port: + number: 50005 + path: / \ No newline at end of file diff --git a/services/restaurant/k8s/service.yml b/services/restaurant/k8s/service.yml new file mode 100644 index 00000000..c617883d --- /dev/null +++ b/services/restaurant/k8s/service.yml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: goodfood-delivery +spec: + selector: + app: goodfood-delivery + ports: + - name: "grpc" + port: 50005 + targetPort: 50005 +status: + loadBalancer: {} diff --git a/services/restaurant/package.json b/services/restaurant/package.json new file mode 100644 index 00000000..6189e4a1 --- /dev/null +++ b/services/restaurant/package.json @@ -0,0 +1,30 @@ +{ + "name": "@goodfood/restaurant", + "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.1.1", + "typescript": "^5.1.6" + }, + "dependencies": { + "@grpc/grpc-js": "^1.8.18", + "@grpc/proto-loader": "^0.7.8", + "@prisma/client": "5.1.1", + "amqplib": "^0.10.3", + "dotenv": "^16.3.1", + "esbuild": "^0.18.15", + "geolib": "^3.3.4", + "nodemon": "^3.0.1", + "path": "^0.12.7", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0" + } +} diff --git a/services/restaurant/pnpm-lock.yaml b/services/restaurant/pnpm-lock.yaml new file mode 100644 index 00000000..0aef035e --- /dev/null +++ b/services/restaurant/pnpm-lock.yaml @@ -0,0 +1,999 @@ +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.1.1 + version: 5.1.1(prisma@5.1.1) + 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 + geolib: + specifier: ^3.3.4 + version: 3.3.4 + 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.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.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.1.1(prisma@5.1.1): + resolution: {integrity: sha512-fxcCeK5pMQGcgCqCrWsi+I2rpIbk0rAhdrN+ke7f34tIrgPwA68ensrpin+9+fZvuV2OtzHmuipwduSY6HswdA==} + engines: {node: '>=16.13'} + requiresBuild: true + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + dependencies: + '@prisma/engines-version': 5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e + prisma: 5.1.1 + dev: false + + /@prisma/engines-version@5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e: + resolution: {integrity: sha512-owZqbY/wucbr65bXJ/ljrHPgQU5xXTSkmcE/JcbqE1kusuAXV/TLN3/exmz21SZ5rJ7WDkyk70J2G/n68iogbQ==} + 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.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 + + /geolib@3.3.4: + resolution: {integrity: sha512-EicrlLLL3S42gE9/wde+11uiaYAaeSVDwCUIv2uMIoRBfNJCn8EsSI+6nS3r4TCKDO6+RQNM9ayLq2at+oZQWQ==} + dev: false + + /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.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.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/restaurant/prisma/migrations/20230819142125_init/migration.sql b/services/restaurant/prisma/migrations/20230819142125_init/migration.sql new file mode 100644 index 00000000..bc0cfc5a --- /dev/null +++ b/services/restaurant/prisma/migrations/20230819142125_init/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "Restaurant" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "location" DOUBLE PRECISION[], + "address" TEXT, + "openingHours" TEXT[], + "phone" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Restaurant_pkey" PRIMARY KEY ("id") +); diff --git a/services/restaurant/prisma/migrations/20230819145035_fix_restaurant_id_type/migration.sql b/services/restaurant/prisma/migrations/20230819145035_fix_restaurant_id_type/migration.sql new file mode 100644 index 00000000..939b88b8 --- /dev/null +++ b/services/restaurant/prisma/migrations/20230819145035_fix_restaurant_id_type/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - The primary key for the `Restaurant` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- AlterTable +ALTER TABLE "Restaurant" DROP CONSTRAINT "Restaurant_pkey", +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ADD CONSTRAINT "Restaurant_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "Restaurant_id_seq"; diff --git a/services/restaurant/prisma/migrations/migration_lock.toml b/services/restaurant/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..fbffa92c --- /dev/null +++ b/services/restaurant/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/restaurant/prisma/schema.prisma b/services/restaurant/prisma/schema.prisma new file mode 100644 index 00000000..538832e8 --- /dev/null +++ b/services/restaurant/prisma/schema.prisma @@ -0,0 +1,23 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Restaurant { + id String @id @default(cuid()) + name String + description String? + + location Float[] + address String? + + openingHours String[] + + phone String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/services/restaurant/proto b/services/restaurant/proto new file mode 120000 index 00000000..5c8d3525 --- /dev/null +++ b/services/restaurant/proto @@ -0,0 +1 @@ +../proto \ No newline at end of file diff --git a/services/restaurant/src/handler/create.ts b/services/restaurant/src/handler/create.ts new file mode 100644 index 00000000..c1f52d85 --- /dev/null +++ b/services/restaurant/src/handler/create.ts @@ -0,0 +1,33 @@ +import { + RestaurantCreateInput, + Restaurant, +} from "@restaurant/types/restaurant"; +import { log } from "@restaurant/lib/log"; +import { Data } from "@restaurant/types"; +import prisma from "@restaurant/lib/prisma"; + +export const CreateRestaurant = async ( + { request }: Data, + callback: (err: any, response: Restaurant | null) => void +) => { + try { + const { location, name, openingHours, address, description, phone } = + request; + + const restaurant = (await prisma.restaurant.create({ + data: { + location, + name, + openingHours, + address, + description, + phone, + }, + })) as unknown as Restaurant; + + callback(null, restaurant); + } catch (error) { + log.error(error); + callback(error, null); + } +}; diff --git a/services/restaurant/src/handler/delete.ts b/services/restaurant/src/handler/delete.ts new file mode 100644 index 00000000..69d356ac --- /dev/null +++ b/services/restaurant/src/handler/delete.ts @@ -0,0 +1,20 @@ +import { RestaurantId } from "@restaurant/types/restaurant"; +import { log } from "@restaurant/lib/log"; +import { Data } from "@restaurant/types"; +import prisma from "@restaurant/lib/prisma"; + +export const DeleteRestaurant = async ( + { request }: Data, + callback: (err: any, response: null) => void +) => { + try { + const { id } = request; + + await prisma.restaurant.delete({ where: { id } }); + + callback(null, null); + } catch (error) { + log.error(error); + callback(error, null); + } +}; diff --git a/services/restaurant/src/handler/get.ts b/services/restaurant/src/handler/get.ts new file mode 100644 index 00000000..97b4f773 --- /dev/null +++ b/services/restaurant/src/handler/get.ts @@ -0,0 +1,22 @@ +import { Restaurant, RestaurantId } from "@restaurant/types/restaurant"; +import { log } from "@restaurant/lib/log"; +import { Data } from "@restaurant/types"; +import prisma from "@restaurant/lib/prisma"; + +export const GetRestaurant = async ( + { request }: Data, + callback: (err: any, response: Restaurant | null) => void +) => { + try { + const { id } = request; + + const restaurant = (await prisma.restaurant.findFirstOrThrow({ + where: { id }, + })) as unknown as Restaurant; + + callback(null, restaurant); + } catch (error) { + log.error(error); + callback(error, null); + } +}; diff --git a/services/restaurant/src/handler/index.ts b/services/restaurant/src/handler/index.ts new file mode 100644 index 00000000..691eab75 --- /dev/null +++ b/services/restaurant/src/handler/index.ts @@ -0,0 +1,15 @@ +import { CreateRestaurant } from "@restaurant/handler/create"; +import { GetRestaurant} from "@restaurant/handler/get"; +import { UpdateRestaurant } from "@restaurant/handler/update"; +import { DeleteRestaurant } from "@restaurant/handler/delete"; +import { GetRestaurants } from "@restaurant/handler/list-all"; +import { GetRestaurantsByLocation } from "@restaurant/handler/list-by-location"; + +export default { + CreateRestaurant, + GetRestaurant, + UpdateRestaurant, + DeleteRestaurant, + GetRestaurants, + GetRestaurantsByLocation, +}; diff --git a/services/restaurant/src/handler/list-all.ts b/services/restaurant/src/handler/list-all.ts new file mode 100644 index 00000000..00c4ee8f --- /dev/null +++ b/services/restaurant/src/handler/list-all.ts @@ -0,0 +1,19 @@ +import { log } from "@restaurant/lib/log"; +import { Data } from "@restaurant/types"; +import prisma from "@restaurant/lib/prisma"; +import { Restaurant, RestaurantList } from "@restaurant/types/restaurant"; + +export const GetRestaurants = async ( + {}: Data, + callback: (err: any, response: RestaurantList | null) => void +) => { + try { + const restaurants = + (await prisma.restaurant.findMany()) as unknown as Restaurant[]; + + callback(null, { restaurants }); + } catch (error) { + log.error(error); + callback(error, null); + } +}; diff --git a/services/restaurant/src/handler/list-by-location.ts b/services/restaurant/src/handler/list-by-location.ts new file mode 100644 index 00000000..be687f4b --- /dev/null +++ b/services/restaurant/src/handler/list-by-location.ts @@ -0,0 +1,45 @@ +import { log } from "@restaurant/lib/log"; +import { Data } from "@restaurant/types"; +import prisma from "@restaurant/lib/prisma"; +import { + ByLocationInput, + Restaurant, + RestaurantList, +} from "@restaurant/types/restaurant"; +import geolib from "geolib"; + +interface RestaurantWithDistance extends Restaurant { + distance: number; +} + +export const sortRestaurantsByDistance = ( + restaurants: Restaurant[], + location: [number, number] +): RestaurantWithDistance[] => { + return restaurants + .map((restaurant) => ({ + ...restaurant, + distance: geolib.getDistance(location, restaurant.location), + })) + .sort((a, b) => a.distance - b.distance); +}; + +export const GetRestaurantsByLocation = async ( + { request }: Data, + callback: (err: any, response: RestaurantList | null) => void +) => { + try { + const { location } = request; + + const all = (await prisma.restaurant.findMany()) as unknown as Restaurant[]; + + const restaurants = sortRestaurantsByDistance(all, location) + .map(({ distance, ...restaurant }) => restaurant as Restaurant) + .filter((_, i) => i < 10); + + callback(null, { restaurants }); + } catch (error) { + log.error(error); + callback(error, null); + } +}; diff --git a/services/restaurant/src/handler/update.ts b/services/restaurant/src/handler/update.ts new file mode 100644 index 00000000..8327c15b --- /dev/null +++ b/services/restaurant/src/handler/update.ts @@ -0,0 +1,31 @@ +import { log } from "@restaurant/lib/log"; +import { Data } from "@restaurant/types"; +import prisma from "@restaurant/lib/prisma"; +import { Restaurant } from "@restaurant/types/restaurant"; + +export const UpdateRestaurant = async ( + { request }: Data, + callback: (err: any, response: Restaurant | null) => void +) => { + try { + const { id, location, name, openingHours, address, description, phone } = + request; + + const restaurant = (await prisma.restaurant.update({ + where: { id }, + data: { + location, + name, + openingHours, + address, + description, + phone, + }, + })) as unknown as Restaurant; + + callback(null, restaurant); + } catch (error) { + log.error(error); + callback(error, null); + } +}; diff --git a/services/restaurant/src/lib/amqp.ts b/services/restaurant/src/lib/amqp.ts new file mode 100644 index 00000000..98872689 --- /dev/null +++ b/services/restaurant/src/lib/amqp.ts @@ -0,0 +1,32 @@ +import amqp from "amqplib"; +import { msg } from "@restaurant/middleware/log"; +import { log } from "@restaurant/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/restaurant/src/lib/log.ts b/services/restaurant/src/lib/log.ts new file mode 100644 index 00000000..2404af54 --- /dev/null +++ b/services/restaurant/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/restaurant/src/lib/prisma.ts b/services/restaurant/src/lib/prisma.ts new file mode 100644 index 00000000..4d495ef0 --- /dev/null +++ b/services/restaurant/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/restaurant/src/lib/proxy.ts b/services/restaurant/src/lib/proxy.ts new file mode 100644 index 00000000..b05d24a6 --- /dev/null +++ b/services/restaurant/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/restaurant/src/middleware/log.ts b/services/restaurant/src/middleware/log.ts new file mode 100644 index 00000000..7235e574 --- /dev/null +++ b/services/restaurant/src/middleware/log.ts @@ -0,0 +1,42 @@ +import { publish } from "@restaurant/lib/amqp"; +import { utils } from "@restaurant/lib/log"; +import { log } from "@restaurant/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/restaurant/src/resources/grpc-credentials.ts b/services/restaurant/src/resources/grpc-credentials.ts new file mode 100644 index 00000000..75a2b5d5 --- /dev/null +++ b/services/restaurant/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/restaurant/src/resources/protoloader-options.ts b/services/restaurant/src/resources/protoloader-options.ts new file mode 100644 index 00000000..5022d96d --- /dev/null +++ b/services/restaurant/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/restaurant/src/server.ts b/services/restaurant/src/server.ts new file mode 100644 index 00000000..76257c6d --- /dev/null +++ b/services/restaurant/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 "@restaurant/lib/log"; +import { options } from "@restaurant/resources/protoloader-options"; +import { serverInsecure } from "@restaurant/resources/grpc-credentials"; + +import promotionHandler from "@restaurant/handler"; +import { createServerProxy } from "@restaurant/lib/proxy"; +import { logGRPC } from "@restaurant/middleware/log"; + +const PORT = process.env.PORT || 50005; +const ADDRESS = `0.0.0.0:${PORT}`; +const PROTO_PATH = resolvePath(__dirname + "/../../proto/restaurant.proto"); + +const packageDefinition = loadSync(PROTO_PATH, options); +const grpc = loadPackageDefinition(packageDefinition) as any; +const { + RestaurantService: { service: rs }, +} = grpc.com.goodfood.restaurant; + +const server = createServerProxy(new Server()); +server.addService(rs, promotionHandler); +server.use(logGRPC); + +server.bindAsync(ADDRESS, serverInsecure, () => { + server.start(); + const message = `---- ${utils.green("good")}${utils.yellow( + "food" + )} Restaurant Service ----\nstarted on: ${utils.bold(ADDRESS)} ${utils.green( + "✓" + )}\n`; + log.debug(message); +}); + +export default server; diff --git a/services/restaurant/src/types/index.d.ts b/services/restaurant/src/types/index.d.ts new file mode 100644 index 00000000..edf2bebf --- /dev/null +++ b/services/restaurant/src/types/index.d.ts @@ -0,0 +1,3 @@ +export type Data = { + request: T; +}; diff --git a/services/restaurant/src/types/restaurant.d.ts b/services/restaurant/src/types/restaurant.d.ts new file mode 100644 index 00000000..8c073d1a --- /dev/null +++ b/services/restaurant/src/types/restaurant.d.ts @@ -0,0 +1,32 @@ +export type Restaurant = { + id: string; + + name: string; + description?: string; + + location: [number, number]; + address?: string; + + openingHours: string[]; + phone?: string; + + createdAt: Date | string; + updatedAt: Date | string; +}; + +export type RestaurantCreateInput = Omit< + Restaurant, + "id" | "createdAt" | "updatedAt" +>; + +export type RestaurantId = { + id: Restaurant["id"]; +}; + +export type ByLocationInput = { + location: Restaurant["location"]; +}; + +export type RestaurantList = { + restaurants: Restaurant[]; +}; diff --git a/services/restaurant/terraform/data.tf b/services/restaurant/terraform/data.tf new file mode 100644 index 00000000..a04c4446 --- /dev/null +++ b/services/restaurant/terraform/data.tf @@ -0,0 +1,13 @@ +data "azurerm_resource_group" "rg-goodfood" { + name = "rg-${var.project_name}${var.environnment_suffix}" +} + +data "azurerm_key_vault_secret" "restaurant-db-login" { + name = "restaurant-db-login" + key_vault_id = azurerm_key_vault.kv-goodfood-restaurant.id +} +data "azurerm_key_vault_secret" "restaurant-db-password" { + name = "restaurant-db-password" + key_vault_id = azurerm_key_vault.kv-goodfood-restaurant.id +} +data "azurerm_client_config" "current" {} diff --git a/services/restaurant/terraform/env/dev-backend.tfvars.example b/services/restaurant/terraform/env/dev-backend.tfvars.example new file mode 100644 index 00000000..9c6c8826 --- /dev/null +++ b/services/restaurant/terraform/env/dev-backend.tfvars.example @@ -0,0 +1,4 @@ +resource_group_name = "rg-goodfood-dev" +storage_account_name = "sa-goodfood-states" +container_name = "tfstate" +key = "restaurant-dev.tfstate" diff --git a/services/restaurant/terraform/env/dev.tfvars.example b/services/restaurant/terraform/env/dev.tfvars.example new file mode 100644 index 00000000..4ac1f44d --- /dev/null +++ b/services/restaurant/terraform/env/dev.tfvars.example @@ -0,0 +1 @@ +environnment_suffix = "-dev" diff --git a/services/restaurant/terraform/main.tf b/services/restaurant/terraform/main.tf new file mode 100644 index 00000000..d7c5d5e7 --- /dev/null +++ b/services/restaurant/terraform/main.tf @@ -0,0 +1,74 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "3.48.0" + } + } + backend "azurerm" {} +} + +provider "azurerm" { + features { + key_vault { + purge_soft_deleted_secrets_on_destroy = true + recover_soft_deleted_secrets = true + } + } +} + +resource "azurerm_key_vault" "kv-goodfood-restaurant" { + name = "kv-goodfood-restaurant" + location = data.azurerm_resource_group.rg-goodfood.location + resource_group_name = data.azurerm_resource_group.rg-goodfood.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_retention_days = 7 + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + key_permissions = [ + "Create", + "Get", + ] + + secret_permissions = [ + "Set", + "Get", + "Delete", + "Purge", + "Recover" + ] + } +} + +resource "azurerm_postgresql_server" "pg-goodfood-restaurant" { + name = "pg-goodfood-restaurant${var.environnment_suffix}" + location = data.azurerm_resource_group.rg-goodfood.location + resource_group_name = data.azurerm_resource_group.rg-goodfood.name + + sku_name = "B_Gen5_2" + + storage_mb = 5120 + backup_retention_days = 7 + geo_redundant_backup_enabled = false + auto_grow_enabled = true + + administrator_login = data.azurerm_key_vault_secret.restaurant-db-login.value + administrator_login_password = data.azurerm_key_vault_secret.restaurant-db-password.value + version = "11" + + ssl_enforcement_enabled = false + ssl_minimal_tls_version_enforced = "TLSEnforcementDisabled" // TODO: change to TLS1_2 + +} + +resource "azurerm_postgresql_firewall_rule" "pgfw-goodfood-restaurant" { + name = "allow-azure-resources" + resource_group_name = data.azurerm_resource_group.rg-goodfood.name + server_name = azurerm_postgresql_server.pg-goodfood-restaurant.name + start_ip_address = "0.0.0.0" + end_ip_address = "0.0.0.0" +} diff --git a/services/restaurant/terraform/outputs.tf b/services/restaurant/terraform/outputs.tf new file mode 100644 index 00000000..a2d60616 --- /dev/null +++ b/services/restaurant/terraform/outputs.tf @@ -0,0 +1,6 @@ +output "main-rg-name" { + value = data.azurerm_resource_group.rg-goodfood.name +} +output "main-rg-id" { + value = data.azurerm_resource_group.rg-goodfood.id +} \ No newline at end of file diff --git a/services/restaurant/terraform/variables.tf b/services/restaurant/terraform/variables.tf new file mode 100644 index 00000000..65fd7547 --- /dev/null +++ b/services/restaurant/terraform/variables.tf @@ -0,0 +1,16 @@ +variable "environnment_suffix" { + type = string + description = "The suffix to append to the environment name" +} + +variable "location" { + type = string + description = "The location/region where all resources in this environment should be created" + default = "West Europe" +} + +variable "project_name" { + type = string + description = "The name of the project" + default = "goodfood" +} \ No newline at end of file diff --git a/services/restaurant/tsconfig.json b/services/restaurant/tsconfig.json new file mode 100644 index 00000000..99c07b41 --- /dev/null +++ b/services/restaurant/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "sourceMap": true, + "outDir": "dist", + "strict": true, + "lib": ["esnext"], + "esModuleInterop": true, + "baseUrl": ".", + "paths": { + "@restaurant/*": ["src/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"], + "ts-node": { + "require": ["tsconfig-paths/register"] + } +}