diff --git a/backend.Dockerfile b/backend.Dockerfile new file mode 100644 index 0000000000..89a8c64f10 --- /dev/null +++ b/backend.Dockerfile @@ -0,0 +1,48 @@ +FROM node:20 AS base +WORKDIR /app + +RUN set -eux; \ + apt-get update -y; \ + apt-get install -y --no-install-recommends openssl; \ + rm -rf /var/lib/apt/list/* + +FROM base AS builder + +COPY packages/server/package.json ./ +COPY yarn.lock ./ +RUN yarn --immutable + +COPY tsconfig.json ./base.tsconfig.json +COPY packages/server/tsconfig.json ./ +RUN sed -i 's/"extends":.*/"extends": ".\/base.tsconfig.json",/' tsconfig.json + +COPY packages/server/prisma ./ +COPY packages/server/codegen.ts ./ +COPY packages/server/src ./src +RUN yarn build:local + +RUN rm -rf node_modules +ENV NODE_ENV=production +RUN yarn --prod --immutable +RUN yarn prisma generate + +FROM base + +USER www-data + +COPY --from=builder --chown=www-data /app/dist ./dist +COPY --from=builder --chown=www-data /app/node_modules ./node_modules +COPY packages/server/prisma ./prisma +COPY packages/server/docker/prod/entrypoint.sh /entrypoint.sh +COPY packages/server/docker/prod/notify.sh /usr/bin/notify + +ENV QUERY_NODE_ENDPOINT "https://query.joystream.org/graphql" +ENV PIONEER_URL "https://pioneerapp.xyz" +ENV STARTING_BLOCK 1 +ENV NODE_ENV=production +ENV APP_LOG_LEVEL "verbose" +ENV PORT 3000 +EXPOSE 3000/tcp + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["api"] diff --git a/backend.dev.Dockerfile b/backend.dev.Dockerfile new file mode 100644 index 0000000000..aaed75ed3a --- /dev/null +++ b/backend.dev.Dockerfile @@ -0,0 +1,42 @@ +FROM node:18-alpine3.18 AS builder +WORKDIR /app + +COPY packages/server/package.json ./ +COPY yarn.lock ./ +RUN yarn --immutable + +COPY tsconfig.json ./base.tsconfig.json +COPY packages/server/tsconfig.json ./ +RUN sed -i 's/"extends":.*/"extends": ".\/base.tsconfig.json",/' tsconfig.json + +COPY packages/server/prisma ./ +COPY packages/server/codegen.ts ./ +COPY packages/server/src ./src +RUN yarn build:local + +RUN rm -rf node_modules +ENV NODE_ENV=production +RUN yarn --prod --immutable +RUN yarn prisma generate + +FROM postgres:16.0-alpine3.18 +WORKDIR /app +RUN apk add --no-cache nodejs + +COPY --from=builder --chown=postgres /app/dist ./dist +COPY --from=builder --chown=postgres /app/node_modules ./node_modules +COPY packages/server/prisma ./prisma +COPY packages/server/docker/dev/entrypoint.sh /entrypoint.sh +COPY packages/server/docker/dev/notify.sh /usr/bin/notify +COPY packages/server/docker/dev/env.sh ./ +COPY packages/server/docker/dev/prisma-deploy.sh /docker-entrypoint-initdb.d/ + +ENV QUERY_NODE_ENDPOINT "https://query.joystream.org/graphql" +ENV PIONEER_URL "https://pioneerapp.xyz" +ENV STARTING_BLOCK 1 +ENV APP_LOG_LEVEL "verbose" +ENV PORT 3000 +EXPOSE 3000/tcp + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["api"] diff --git a/packages/server/.env.dev b/packages/server/.env.dev index c55228518b..7fca14d26f 100644 --- a/packages/server/.env.dev +++ b/packages/server/.env.dev @@ -6,27 +6,43 @@ POSTGRES_DB: "pioneer" POSTGRES_USER: "pioneer" POSTGRES_PASSWORD: "pioneer" -# Use a non standard port to not interfere with the Joystream mono-repo `db` service +# Use a non standard port to not interfere with the local Joystream mono-repo `db` service. DB_PORT: 5433 +# URL Prisma uses to connect to the database (this value is not used when runing the api from the docker compose file). DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${DB_PORT}/${POSTGRES_DB}" # # Joystream environment # +# Query node to fetch from: +# - http://localhost:8081/graphql to connect to a query node running locally. +# - http://graphql-server:8081/graphql to connect to the Joystream mono-repo docker service from its network. +# - https://query.joystream.org/graphql to connect to the Joystream's mainnet query node. QUERY_NODE_ENDPOINT: "https://query.joystream.org/graphql" + +# Pioneer instance to link to in the notification (Use "http://localhost:8080" for the local instance). PIONEER_URL: "https://pioneerapp.xyz" + +# Block to start fetching the events from. STARTING_BLOCK: 1 # # General # +# Port to run the api on. PORT: 3000 + +# The key used to to sign JSON Web Token with. APP_SECRET_KEY: "SECRET_1234" + +# Log level. APP_LOG_LEVEL: "verbose" +NODE_ENV: "development" + # # Email # @@ -41,7 +57,8 @@ APP_LOG_LEVEL: "verbose" # MAILGUN_API_URL: "https://api.eu.mailgun.net" # this is needed for EU domains # -# Useful on personal deployment +# For development only: # +# Create default members (their tokens will be displayed when the server start). INITIAL_MEMBERSHIPS: '[{ "id": 1, "name": "Alice", "email": "alice@example.com" }, { "id": 2, "name": "Bob", "email": "bob@example.com" }]' diff --git a/packages/server/README.md b/packages/server/README.md index 3adab960c0..d1b47a6913 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -30,7 +30,26 @@ It is composed of 3 parts: ## Quick Start -[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/Joystream/pioneer/tree/feature/backend-poc) +### render.com deployments + +### Run with docker + +```shell +yarn workspace server docker:up +``` + +This runs the api on: http://localhost:3000 + +Configurations are available in `packages/server/.env`. + +To run the notification script: +```shell +yarn workspace server docker:notify +``` + +### Demo deployment + +[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/Joystream/pioneer/tree/backend-dev-blueprint) > **Warning** > @@ -51,6 +70,10 @@ Mapping existing Joystream memberships id to a name and an email address in the In order to customize the default notification behavior with the GraphQL API, an authorization token can be found for each membership in the "Logs" section. +### Production + +[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/Joystream/pioneer/tree/main) + ## Production CLI usage > **Note** @@ -59,7 +82,7 @@ In order to customize the default notification behavior with the GraphQL API, an - `yarn start:api`: starts the API server. - `yarn notify`: run the notify job. -- `yarn start:all`: for environments where cron is not available, starts both the API server, and schedule the notify job every 30 minutes via [`node-cron`](https://www.npmjs.com/package/node-cron). +- `yarn start:all`: for environments where cron is not available, starts both the API server, and schedule the notify job every 10 minutes via [`node-cron`](https://www.npmjs.com/package/node-cron). ## API usage @@ -210,7 +233,7 @@ query { To run the API to develop locally: -1. `yarn --frozen-lockfile`: Install the dependencies. +1. `yarn --immutable`: Install the dependencies. 2. Create and configure a `packages/server/.env`. 3. Prepare the database and generate the code by running either: - `yarn workspace server dev:db:build`: To use docker for the db. diff --git a/packages/server/docker-compose.yml b/packages/server/docker-compose.yml index 37beb4997f..d492a39dfc 100644 --- a/packages/server/docker-compose.yml +++ b/packages/server/docker-compose.yml @@ -1,14 +1,33 @@ version: '3.4' volumes: - db-data: + pioneer-db-data: + +networks: + default: + name: joystream_default + external: true services: - db: - image: postgres:12 + pioneer-api: + image: thesan/pioneer-backend # TODO change this to joystream/... + build: + context: ../.. + dockerfile: backend.Dockerfile + ports: + - '${PORT}:${PORT}' + depends_on: + - pioneer-db + env_file: + - .env # ensure `.env` exist + environment: + DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@pioneer-db:5432/${POSTGRES_DB} + + pioneer-db: + image: postgres:16 ports: - '127.0.0.1:${DB_PORT}:5432' volumes: - - db-data:/var/lib/postgresql/data + - pioneer-db-data:/var/lib/postgresql/data env_file: - - .env + - .env # ensure `.env` exist diff --git a/packages/server/docker/dev/entrypoint.sh b/packages/server/docker/dev/entrypoint.sh new file mode 100755 index 0000000000..73c59f9446 --- /dev/null +++ b/packages/server/docker/dev/entrypoint.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +export PATH=$PATH:/app/node_modules/.bin +. /app/env.sh + +case "$1" in + api) + docker-entrypoint.sh postgres & + + prismaClient=/app/node_modules/.prisma/client/index.js + while grep -oh "@prisma/client did not initialize yet." $prismaClient ; do + sleep 1 + done + + node /app/dist/common/scripts/startApi + ;; + + postgres) + docker-entrypoint.sh postgres + ;; + + + *) + exec "$@" + ;; +esac \ No newline at end of file diff --git a/packages/server/docker/dev/env.sh b/packages/server/docker/dev/env.sh new file mode 100644 index 0000000000..b557f49656 --- /dev/null +++ b/packages/server/docker/dev/env.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +export POSTGRES_USER=${POSTGRES_USER:="postgres"} +export POSTGRES_DB=${POSTGRES_DB:=POSTGRES_USER} +if [ -z "$POSTGRES_PASSWORD" ]; then + export DATABASE_URL="postgresql://${POSTGRES_USER}@localhost/${POSTGRES_DB}?host=/var/run/postgresql/" +else + export DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost/${POSTGRES_DB}?host=/var/run/postgresql/" +fi + +export APP_SECRET_KEY=${APP_SECRET_KEY:=${POSTGRES_PASSWORD:="pioneer"}} \ No newline at end of file diff --git a/packages/server/docker/dev/notify.sh b/packages/server/docker/dev/notify.sh new file mode 100755 index 0000000000..c0c54dfab7 --- /dev/null +++ b/packages/server/docker/dev/notify.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +. /app/env.sh +node /app/dist/notifier/scripts/notify \ No newline at end of file diff --git a/packages/server/docker/dev/prisma-deploy.sh b/packages/server/docker/dev/prisma-deploy.sh new file mode 100755 index 0000000000..591e6e5595 --- /dev/null +++ b/packages/server/docker/dev/prisma-deploy.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +cd /app +prisma migrate deploy \ No newline at end of file diff --git a/packages/server/docker/prod/entrypoint.sh b/packages/server/docker/prod/entrypoint.sh new file mode 100755 index 0000000000..862524da8c --- /dev/null +++ b/packages/server/docker/prod/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +set -e + +export PATH=$PATH:/app/node_modules/.bin + +case "$1" in + api) + cd /app + until prisma migrate deploy; do + echo "Waiting for the db to be ready..." + sleep 1 + done + node ./dist/common/scripts/startApi + ;; + + *) + exec "$@" + ;; +esac diff --git a/packages/server/docker/prod/notify.sh b/packages/server/docker/prod/notify.sh new file mode 100755 index 0000000000..da0bcaa244 --- /dev/null +++ b/packages/server/docker/prod/notify.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +node /app/dist/notifier/scripts/notify \ No newline at end of file diff --git a/packages/server/docker/up.sh b/packages/server/docker/up.sh new file mode 100755 index 0000000000..f8dff40954 --- /dev/null +++ b/packages/server/docker/up.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -x + +if [ ! -f .env ]; then + sed 's/^QUERY_NODE_ENDPOINT: .*/QUERY_NODE_ENDPOINT: "http:\/\/graphql-server:8081\/graphql"/' .env.dev > .env +fi +docker network create joystream_default 2> /dev/null +yarn docker down +yarn docker up diff --git a/packages/server/package.json b/packages/server/package.json index 81ff1aec3d..6ee0e89a1d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -6,12 +6,14 @@ "prisma": "prisma", "codegen": "graphql-codegen", "docker": "docker-compose", + "docker:up": "./docker/up.sh", + "docker:notify": "yarn docker run pioneer-api notify", "test": "jest --runInBand", "tsc": "tsc", "ts-node": "ts-node --transpile-only -r tsconfig-paths/register", "nodemon": "nodemon --transpile-only -r tsconfig-paths/register", "dev:build": "yarn codegen && yarn prisma db push", - "dev:db": "yarn docker up -d", + "dev:db": "yarn docker up pioneer-db -d", "dev:db:reset": "yarn docker down -v && yarn dev:db && yarn prisma db push", "dev:db:build": "yarn dev:db && yarn dev:build", "dev:api": "yarn nodemon ./src/common/scripts/startApi.ts", @@ -37,6 +39,7 @@ "@react-email/render": "^0.0.7", "@sendgrid/mail": "^7.7.0", "apollo-server": "^3.11.1", + "dotenv": "^16.0.3", "graphql": "^16.6.0", "graphql-request": "^5.1.0", "jsonwebtoken": "^9.0.0", @@ -66,11 +69,12 @@ "@types/lodash": "^4", "@types/node": "^18.14.0", "@types/node-cron": "^3.0.7", + "@types/npmlog": "^4.1.4", "@types/prettier": "^2", + "@types/react": "^18.2.31", "@typescript-eslint/eslint-plugin": "^5.52.0", "@typescript-eslint/parser": "^5.52.0", "babel": "^6.23.0", - "dotenv": "^16.0.3", "eslint": "^8.33.0", "jest": "^29.4.3", "nodemon": "^2.0.21", diff --git a/packages/server/src/common/config.ts b/packages/server/src/common/config.ts index 362c10aad3..507bb0bbe4 100644 --- a/packages/server/src/common/config.ts +++ b/packages/server/src/common/config.ts @@ -5,7 +5,7 @@ config() const { PORT = 3000, - APP_SECRET_KEY, // TODO check this is defined when running the api + APP_SECRET_KEY, APP_LOG_LEVEL = 'info', QUERY_NODE_ENDPOINT = 'https://query.joystream.org/graphql', PIONEER_URL = 'https://pioneerapp.xyz', diff --git a/packages/server/src/common/email-templates/pioneer-email.tsx b/packages/server/src/common/email-templates/pioneer-email.tsx index 92ad674569..a84c3cf498 100644 --- a/packages/server/src/common/email-templates/pioneer-email.tsx +++ b/packages/server/src/common/email-templates/pioneer-email.tsx @@ -24,6 +24,8 @@ interface PioneerEmailTemplateProps { } } +const APP_LOGOS = 'https://eu-central-1.linodeobjects.com/atlas-assets/email/logos/pioneer' + const PioneerEmailTemplate = ({ memberHandle = 'bob', text = 'New council election has just started. Follow the link below to announce your candidacy.', @@ -40,11 +42,7 @@ const PioneerEmailTemplate = ({
- Pioneer's Logo + Pioneer's Logo
Hi {memberHandle}, @@ -56,11 +54,7 @@ const PioneerEmailTemplate = ({ )}
- Small Pioneer logo + Small Pioneer logo pioneerapp.xyz diff --git a/packages/server/src/common/scripts/startApi.ts b/packages/server/src/common/scripts/startApi.ts index 332365d7b0..22dc2c7880 100644 --- a/packages/server/src/common/scripts/startApi.ts +++ b/packages/server/src/common/scripts/startApi.ts @@ -1,5 +1,9 @@ import { server } from '@/common/api' -import { PORT } from '@/common/config' +import { APP_SECRET_KEY, PORT } from '@/common/config' + +if (!APP_SECRET_KEY) { + throw Error('APP_SECRET_KEY should be defined') +} server.listen(PORT).then(({ url }) => { process.stdout.write(`🚀 Server ready at ${url}\n`) diff --git a/packages/server/src/common/utils/email.ts b/packages/server/src/common/utils/email.ts index 24b2b2d7e9..3263d54f25 100644 --- a/packages/server/src/common/utils/email.ts +++ b/packages/server/src/common/utils/email.ts @@ -2,9 +2,12 @@ import sgMail from '@sendgrid/mail' import formData from 'form-data' import Mailgun from 'mailgun.js' import type MailgunClient from 'mailgun.js/client' +import { warn } from 'npmlog' import { EMAIL_SENDER, SENDGRID_CONFIG, MAILGUN_CONFIG } from '@/common/config' +import { errorMessage } from '.' + const createMissingEnvError = (name: string) => Error(`${name} should be defined in environment`) export type EmailBody = { text: string } | { html: string } @@ -17,6 +20,12 @@ export interface EmailProvider { sendEmail: (email: Email) => Promise } +const LogOnlyEmailProvider: EmailProvider = { + sendEmail: async (email) => { + warn('Email notifications', `Email not send to ${email.to}: ${email.subject}`) + }, +} + class MailgunEmailProvider implements EmailProvider { private mailgun: MailgunClient private mailgunDomain: string @@ -46,16 +55,22 @@ class SendgridEmailProvider implements EmailProvider { } export const createEmailProvider = (): EmailProvider => { - if (!EMAIL_SENDER) { - throw createMissingEnvError('EMAIL_SENDER') - } + try { + if (!EMAIL_SENDER) { + throw createMissingEnvError('EMAIL_SENDER') + } - if (!SENDGRID_CONFIG && !MAILGUN_CONFIG) { - throw Error('The email provider is not defined correctly') - } + if (!SENDGRID_CONFIG && !MAILGUN_CONFIG) { + throw Error('The email provider is not defined correctly') + } - if (SENDGRID_CONFIG && MAILGUN_CONFIG) { - throw Error('Multiple email providers are defined') + if (SENDGRID_CONFIG && MAILGUN_CONFIG) { + throw Error('Multiple email providers are defined') + } + } catch (err) { + warn('Email notifications', 'Failed to configure email provider with error:', errorMessage(err)) + if (process.env['NODE_ENV'] === 'production') throw err + return LogOnlyEmailProvider } return SENDGRID_CONFIG diff --git a/packages/server/src/notifier/processNotifications.ts b/packages/server/src/notifier/processNotifications.ts index 639dec43a2..ed9f76e8f3 100644 --- a/packages/server/src/notifier/processNotifications.ts +++ b/packages/server/src/notifier/processNotifications.ts @@ -3,29 +3,21 @@ import { error, info, warn } from 'npmlog' import { EMAIL_MAX_RETRY_COUNT } from '@/common/config' import { prisma } from '@/common/prisma' -import { EmailProvider, createEmailProvider, errorMessage } from '@/common/utils' +import { createEmailProvider, EmailProvider, errorMessage } from '@/common/utils' import { createEmailNotifier } from './model/email' export const processNotifications = async (): Promise => { - let emailProvider: EmailProvider - try { - emailProvider = createEmailProvider() - } catch (err) { - warn('Email notifications', 'Failed to configure email provider with error:', errorMessage(err)) - return - } - const notifications = await prisma.notification.findMany({ where: { emailStatus: 'PENDING' }, include: { member: true }, }) - await sendNotifications(notifications, emailProvider) + await sendNotifications(notifications, createEmailProvider()) } type NotificationWithMember = Notification & { member: Member } -export const sendNotifications = async ( +const sendNotifications = async ( notifications: NotificationWithMember[], emailProvider: EmailProvider ): Promise => { diff --git a/packages/server/src/notifier/scripts/notifyNodeCron.ts b/packages/server/src/notifier/scripts/notifyNodeCron.ts index e2bb138486..4216d16eca 100644 --- a/packages/server/src/notifier/scripts/notifyNodeCron.ts +++ b/packages/server/src/notifier/scripts/notifyNodeCron.ts @@ -2,4 +2,4 @@ import cron from 'node-cron' import { run } from '@/notifier' -cron.schedule('*/30 * * * *', run) +cron.schedule('*/10 * * * *', run) diff --git a/render.yaml b/render.yaml index 8bd9110c09..cf29f9deec 100644 --- a/render.yaml +++ b/render.yaml @@ -1,15 +1,11 @@ services: - type: web name: api - env: node region: frankfurt - plan: free - - repo: https://github.com/Joystream/pioneer - branch: feature/backend-poc + plan: starter - buildCommand: yarn --frozen-lockfile && yarn workspace server build - startCommand: yarn workspace server start:api + runtime: image + image: { url: docker.io/thesan/pioneer-backend:prod } envVars: - key: DATABASE_URL @@ -18,47 +14,29 @@ services: property: connectionString - key: QUERY_NODE_ENDPOINT - fromService: - type: cron - name: notifier - envVarKey: QUERY_NODE_ENDPOINT + sync: false - key: PIONEER_URL - fromService: - type: cron - name: notifier - envVarKey: PIONEER_URL + sync: false - key: EMAIL_SENDER - fromService: - type: cron - name: notifier - envVarKey: EMAIL_SENDER + sync: false - key: SENDGRID_API_KEY - fromService: - type: cron - name: notifier - envVarKey: SENDGRID_API_KEY + sync: false - key: APP_SECRET_KEY generateValue: true - - key: INITIAL_MEMBERSHIPS - sync: false - - type: cron name: notifier - env: node region: frankfurt plan: starter - repo: https://github.com/Joystream/pioneer - branch: feature/backend-poc - - buildCommand: yarn --frozen-lockfile && yarn workspace server build - startCommand: yarn workspace server notify - schedule: "*/30 * * * *" + runtime: image + image: { url: docker.io/thesan/pioneer-backend:prod } + dockerCommand: notify + schedule: "*/10 * * * *" envVars: - key: DATABASE_URL @@ -67,16 +45,28 @@ services: property: connectionString - key: QUERY_NODE_ENDPOINT - value: https://query.joystream.org/graphql + fromService: + type: web + name: api + envVarKey: QUERY_NODE_ENDPOINT - key: PIONEER_URL - value: https://pioneerapp.xyz + fromService: + type: web + name: api + envVarKey: PIONEER_URL - key: EMAIL_SENDER - sync: false + fromService: + type: web + name: api + envVarKey: EMAIL_SENDER - key: SENDGRID_API_KEY - sync: false + fromService: + type: web + name: api + envVarKey: SENDGRID_API_KEY - key: STARTING_BLOCK sync: false @@ -87,4 +77,4 @@ services: databases: - name: db region: frankfurt - plan: free + plan: starter diff --git a/yarn.lock b/yarn.lock index ecd7290148..d10482f377 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9413,6 +9413,13 @@ __metadata: languageName: node linkType: hard +"@types/npmlog@npm:^4.1.4": + version: 4.1.4 + resolution: "@types/npmlog@npm:4.1.4" + checksum: 740f7431ccfc0e127aa8d162fe05c6ce8aa71290be020d179b2824806d19bd2c706c7e0c9a3c9963cefcdf2ceacb1dec6988c394c3694451387759dafe0aa927 + languageName: node + linkType: hard + "@types/parse-json@npm:^4.0.0": version: 4.0.0 resolution: "@types/parse-json@npm:4.0.0" @@ -9557,6 +9564,17 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^18.2.31": + version: 18.2.31 + resolution: "@types/react@npm:18.2.31" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: b11be8e39174d3303e308461400889e353e422d22b01d09795b2c35b7b99d5351716503d9ec5c58e4c2c871249603fa52840d45a34fb5901dd7a26e06129c716 + languageName: node + linkType: hard + "@types/responselike@npm:^1.0.0": version: 1.0.0 resolution: "@types/responselike@npm:1.0.0" @@ -29477,7 +29495,9 @@ __metadata: "@types/lodash": ^4 "@types/node": ^18.14.0 "@types/node-cron": ^3.0.7 + "@types/npmlog": ^4.1.4 "@types/prettier": ^2 + "@types/react": ^18.2.31 "@typescript-eslint/eslint-plugin": ^5.52.0 "@typescript-eslint/parser": ^5.52.0 apollo-server: ^3.11.1