KaufmanBot에 Prisma ORM 추가(NestJS 텔레그램 봇 애플리케이션)
연결
https://github.com/EndyKaufman/kaufman-bot - 봇의 소스 코드
https://telegram.me/DevelopKaufmanBot - 전보의 현재 봇
https://www.prisma.io - 차세대 Node.js 및 TypeScript ORM
종속성 설치
npm install prisma --save-dev
endy@endy-virtual-machine:~/Projects/current/kaufman-bot$ npm install prisma --save-dev
added 2 packages, and audited 940 packages in 10s
115 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
prod 모드에서 postgres를 올바르게 사용하려면 pg deps를 설치해야 합니다.
npm install pg pg-promise --save
endy@endy-virtual-machine:~/Projects/current/kaufman-bot$ npm install pg pg-promise --save
up to date, audited 942 packages in 3s
115 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
초기화 프리즈마
npx prisma init
endy@endy-virtual-machine:~/Projects/current/kaufman-bot$ npx prisma init
✔ Your Prisma schema was created at prisma/schema.prisma
You can now open it in your favorite editor.
warn You already have a .gitignore. Don't forget to exclude .env to not commit any secret.
Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver or mongodb (Preview).
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.
More information in our documentation:
https://pris.ly/d/getting-started
생성된 .env 파일 제거
rm -rf .env
프리즈마 스키마 업데이트
프리즈마/스키마.프리즈마
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
기존 데이터베이스에서 prisma 스키마 채우기
package.json에 추가 스크립트 추가
패키지.json
...
"prisma": "prisma",
"prisma:pull": "npm run -- prisma db pull && npm run prisma:generate",
"prisma:pull:local": "export $(xargs < ./.env.local) > /dev/null 2>&1 && export DATABASE_URL=$SERVER_POSTGRES_URL && npm run -- prisma db pull && npm run prisma:generate",
"prisma:generate": "npm run -- prisma generate",
"postinstall": "npm run generate"
...
package.json에 스크립트가 있음 업데이트
패키지.json
...
"generate": "npm run prisma:generate && npm run rucken -- prepare --locales=en,ru && npm run lint:fix"
...
데이터베이스에 대한 검사 실행
npm run prisma:pull:local
endy@endy-virtual-machine:~/Projects/current/kaufman-bot$ npm run prisma:pull:local
> [email protected] prisma:pull:local
> export $(xargs < ./.env.local) > /dev/null 2>&1 && export DATABASE_URL=$SERVER_POSTGRES_URL && npm run -- prisma db pull && npm run prisma:generate
> [email protected] prisma
> prisma "db" "pull"
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "kaufman_bot_develop", schema "public" at "localhost:5432"
Introspecting based on datasource defined in prisma/schema.prisma …
✔ Introspected 2 models and wrote them into prisma/schema.prisma in 121ms
Run prisma generate to generate Prisma Client.
> [email protected] prisma:generate
> npm run -- prisma generate
> [email protected] prisma
> prisma "generate"
Prisma schema loaded from prisma/schema.prisma
✔ Generated Prisma Client (3.11.1 | library) to ./node_modules/@prisma/client in 200ms
You can now start using Prisma Client in your code. Reference: https://pris.ly/d/client
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
pull 명령 이후의 스키마
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id(map: "PK_USERS") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
telegramId String @unique(map: "UQ_USERS__TELEGRAM_ID") @db.VarChar(64)
langCode String @default("en") @db.VarChar(64)
}
model migrations {
installed_rank Int @id(map: "__migrations_pk")
version String? @db.VarChar(50)
description String @db.VarChar(200)
type String @db.VarChar(20)
script String @db.VarChar(1000)
checksum Int?
installed_by String @db.VarChar(100)
installed_on DateTime @default(now()) @db.Timestamp(6)
execution_time Int
success Boolean
@@index([success], map: "__migrations_s_idx")
@@map("__migrations")
}
prisma 작업을 위한 NestJS 모듈 추가
구성 파일 추가
libs/core/server/src/lib/prisma-client/prisma-client.config.ts
export const PRISMA_CLIENT_CONFIG = Symbol('PRISMA_CLIENT_CONFIG');
export interface PrismaClientConfig {
databaseUrl: string;
logging: 'all_queries' | 'long_queries';
maxQueryExecutionTime: number;
}
서비스 추가
libs/core/server/src/lib/prisma-client/prisma-client.service.ts
import {
Injectable,
Logger,
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { CustomInject, CustomInjectorService } from 'nestjs-custom-injector';
import {
PrismaClientConfig,
PRISMA_CLIENT_CONFIG,
} from './prisma-client.config';
@Injectable()
export class PrismaClientService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
private logger = new Logger(PrismaClientService.name);
@CustomInject(PRISMA_CLIENT_CONFIG)
private readonly prismaClientConfig!: PrismaClientConfig;
constructor(customInjectorService: CustomInjectorService) {
super({
datasources: {
db: {
url: customInjectorService.getProviders<PrismaClientConfig>(
PRISMA_CLIENT_CONFIG
).databaseUrl,
},
},
rejectOnNotFound: true,
log: [
{
emit: 'event',
level: 'query',
},
{
emit: 'event',
level: 'error',
},
],
});
}
async onModuleInit(): Promise<void> {
this.logger.log('onModuleInit');
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this as any).$on('query', (e) => {
if (this.prismaClientConfig.logging === 'all_queries') {
if (e.query !== 'SELECT 1') {
this.logger.log(
`query: ${e.query}, params: ${e.params}, duration: ${e.duration}`
);
}
}
if (this.prismaClientConfig.logging === 'long_queries') {
if (e.duration >= this.prismaClientConfig.maxQueryExecutionTime) {
this.logger.warn(
`query is slow: ${e.query}, params: ${e.params}, execution time: ${e.duration}`
);
}
}
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this as any).$on('error', (e) => {
this.logger.error(`target: ${e.target}, message: ${e.message}`);
});
await this.$connect();
setInterval(
() =>
this.$queryRaw`SELECT 1`.catch((err) =>
this.logger.error(err, err.stack)
),
5 * 60000
);
} catch (err) {
this.logger.error(err, err.stack);
}
}
async onModuleDestroy(): Promise<void> {
this.logger.log('onModuleDestroy');
await this.$disconnect();
}
}
모듈 추가
libs/core/server/src/lib/prisma-client/prisma-client.module.ts
import { DynamicModule, Module } from '@nestjs/common';
import env from 'env-var';
import { CustomInjectorModule } from 'nestjs-custom-injector';
import {
PrismaClientConfig,
PRISMA_CLIENT_CONFIG,
} from './prisma-client.config';
import { PrismaClientService } from './prisma-client.service';
@Module({
imports: [CustomInjectorModule],
providers: [PrismaClientService],
exports: [PrismaClientService],
})
class PrismaClientModuleCore {}
@Module({
imports: [PrismaClientModuleCore],
exports: [PrismaClientModuleCore],
})
export class PrismaClientModule {
static forRoot(config: PrismaClientConfig): DynamicModule {
return {
module: PrismaClientModule,
providers: [
{
provide: PRISMA_CLIENT_CONFIG,
useValue: {
...config,
databaseUrl: config.databaseUrl
.replace(
'${POSTGRES_HOST}',
env.get('POSTGRES_HOST').default('').asString()
)
.replace(
'localhost',
env.get('POSTGRES_HOST').default('').asString()
),
},
},
],
};
}
}
libs의 index.ts 파일 업데이트
npm run generate
애플리케이션에 PrismaClientModule 추가
AppModule 업데이트
앱/서버/src/app/app.module.ts
...
@Module({
imports: [
...
PrismaClientModule.forRoot({
databaseUrl: env.get('SERVER_POSTGRES_URL').required().asString(),
logging: 'long_queries',
maxQueryExecutionTime: 5000,
}),
...
]
...
})
...
언어SwitherModule 업데이트
libs/언어-swither/서버/src/lib/언어-swither.module.ts
import {
...
PrismaClientModule,
} from '@kaufman-bot/core/server';
...
@Module({
imports: [TranslatesModule, PrismaClientModule, BotCommandsModule],
providers: [LanguageSwitherStorage],
exports: [
TranslatesModule,
PrismaClientModule,
BotCommandsModule,
LanguageSwitherStorage,
],
})
export class LanguageSwitherModule {
...
}
언어 만들기SwitherStorage
libs/language-swither/server/src/lib/language-swither-services/language-swither.storage.ts
import { PrismaClientService } from '@kaufman-bot/core/server';
import { Injectable } from '@nestjs/common';
import { DEFAULT_LANGUAGE } from '../language-swither-config/language-swither.config';
@Injectable()
export class LanguageSwitherStorage {
private readonly languageOfUsers: Record<number, string> = {};
constructor(private readonly prismaClientService: PrismaClientService) {}
async getLanguageOfUser(
userId: number,
defaultLangCode?: string
): Promise<string> {
const currentLanguageCode = this.languageOfUsers[userId];
if (currentLanguageCode) {
return currentLanguageCode;
}
try {
const currentLanguageCodeFromDatabase =
await this.prismaClientService.user.findFirst({
where: { telegramId: userId.toString() },
rejectOnNotFound: true,
});
this.languageOfUsers[userId] = currentLanguageCodeFromDatabase.langCode;
return this.languageOfUsers[userId];
} catch (error) {
return defaultLangCode || DEFAULT_LANGUAGE;
}
}
async setLanguageOfUser(userId: number, langCode: string): Promise<void> {
await this.prismaClientService.user.upsert({
create: { telegramId: userId.toString(), langCode },
update: { langCode },
where: { telegramId: userId.toString() },
});
this.languageOfUsers[userId] = langCode;
}
}
언어SwitherService 업데이트
libs/language-swither/server/src/lib/language-swither-services/language-swither.service.ts
import {
BotCommandsEnum,
BotCommandsProvider,
BotCommandsProviderActionMsg,
BotCommandsProviderActionResultType,
BotСommandsToolsService,
OnBeforeBotCommands,
} from '@kaufman-bot/core/server';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { getText } from 'class-validator-multi-lang';
import { TranslatesService, TranslatesStorage } from 'nestjs-translates';
import {
LanguageSwitherConfig,
LANGUAGE_SWITHER_CONFIG,
} from '../language-swither-config/language-swither.config';
import { LanguageSwitherCommandsEnum } from '../language-swither-types/language-swither-commands';
import { LanguageSwitherStorage } from './language-swither.storage';
@Injectable()
export class LanguageSwitherService
implements BotCommandsProvider, OnBeforeBotCommands
{
private readonly logger = new Logger(LanguageSwitherService.name);
constructor(
@Inject(LANGUAGE_SWITHER_CONFIG)
private readonly languageSwitherConfig: LanguageSwitherConfig,
private readonly translatesService: TranslatesService,
private readonly translatesStorage: TranslatesStorage,
private readonly languageSwitherStorage: LanguageSwitherStorage,
private readonly commandToolsService: BotСommandsToolsService
) {}
async onBeforeBotCommands<
TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg
>(msg: TMsg): Promise<TMsg> {
const locale = await this.languageSwitherStorage.getLanguageOfUser(
msg.from?.id
);
const detectedLocale = await this.languageSwitherStorage.getLanguageOfUser(
msg.from?.id,
msg.from?.language_code
);
if (msg.from?.id && !locale) {
await this.languageSwitherStorage.setLanguageOfUser(
msg.from?.id,
detectedLocale
);
} else {
if (detectedLocale) {
msg.from.language_code = detectedLocale;
}
}
return msg;
}
async onHelp<
TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg
>(msg: TMsg): Promise<BotCommandsProviderActionResultType<TMsg>> {
return await this.onMessage({
...msg,
text: `${this.languageSwitherConfig.name} ${BotCommandsEnum.help}`,
});
}
async onMessage<
TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg
>(msg: TMsg): Promise<BotCommandsProviderActionResultType<TMsg>> {
const locale = await this.languageSwitherStorage.getLanguageOfUser(
msg.from?.id
);
const spyWord = this.languageSwitherConfig.spyWords.find((spyWord) =>
this.commandToolsService.checkCommands(msg.text, [spyWord], locale)
);
if (spyWord) {
if (
this.commandToolsService.checkCommands(
msg.text,
[BotCommandsEnum.help],
locale
)
) {
return {
type: 'markdown',
markdown: this.commandToolsService.generateHelpMessage(
locale,
this.languageSwitherConfig.name,
this.languageSwitherConfig.descriptions,
this.languageSwitherConfig.usage
),
};
}
const preparedText = this.commandToolsService.clearCommands(
msg.text,
[
spyWord,
...Object.keys(LanguageSwitherCommandsEnum),
...(this.languageSwitherConfig.removeWords || []),
],
locale
);
const processedMsg = await this.process(msg, locale, preparedText);
if (typeof processedMsg === 'string') {
return {
type: 'text',
text: processedMsg,
};
}
if (processedMsg) {
return { type: 'message', message: processedMsg };
}
this.logger.warn(`Unhandled commands for text: "${msg.text}"`);
this.logger.debug(msg);
}
return null;
}
private async process<
TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg
>(msg: TMsg, locale: string, text: string) {
if (
this.commandToolsService.checkCommands(
msg.text,
[
LanguageSwitherCommandsEnum.set,
LanguageSwitherCommandsEnum.change,
LanguageSwitherCommandsEnum['quick change'],
],
locale
)
) {
if (
!Object.keys(this.translatesStorage.translates)
.map((key) => key.toLowerCase())
.includes(text.trim().toLowerCase())
) {
const currentLocale =
await this.languageSwitherStorage.getLanguageOfUser(msg.from?.id);
return this.translatesService.translate(
getText(
`locale "{{locale}}" not founded, current locale: "{{currentLocale}}"`
),
currentLocale,
{
locale: text.trim().toLowerCase(),
currentLocale,
}
);
}
const inputLocale =
Object.keys(this.translatesStorage.translates).find((lang) =>
text
.split(' ')
.find((key) => key.toLowerCase() === lang.toLowerCase())
) || locale;
locale = inputLocale || locale;
msg.from.language_code = inputLocale || locale;
await this.languageSwitherStorage.setLanguageOfUser(
msg.from?.id,
inputLocale || locale
);
return this.translatesService.translate(
getText(`locale changed, current locale: "{{locale}}"`),
locale,
{
locale,
}
);
}
if (
this.commandToolsService.checkCommands(
msg.text,
[LanguageSwitherCommandsEnum.my, LanguageSwitherCommandsEnum.current],
locale
)
) {
return this.translatesService.translate(
getText(`you locale: {{locale}}`),
locale,
{ locale }
);
}
return msg;
}
}
개발 인프라 업데이트
docker-compose 업데이트
도커/dev/docker-compose.yml
...
kaufman-bot-server:
image: node:16-alpine
user: ${CURRENT_UID}
container_name: 'kaufman-bot-server'
environment:
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
- SERVER_POSTGRES_URL=${SERVER_POSTGRES_URL}
- POSTGRES_HOST=${POSTGRES_HOST}
- PORT=3000
ports:
- '3000:3000'
- '9229:9229'
working_dir: '/app'
volumes:
- ./../../:/app
networks:
- kaufman-bot-network
command: 'npm run serve'
tty: true
depends_on:
- kaufman-bot-postgres
...
업데이트 스크립트
도커/dev/docker-compose-up.sh
#!/bin/bash
#export UID=$(id -u)
#export GID=$(id -g)
export CURRENT_UID=$(id -u):$(id -g)
docker volume create --name=kaufman-bot-postgres-volume --label=kaufman-bot-postgres-volume
# Start only database
docker-compose -f ./docker/dev/docker-compose.yml --compatibility up -d kaufman-bot-postgres
# Wait ready datatbase
until docker exec -it $(docker ps -aqf "name=kaufman-bot-postgres") pg_isready -U postgres; do
echo "Waiting for postgres..."
sleep 2
done
# Create all need application databases by exists match evn key and nx app name
# for app: "server" - env: SERVER_POSTGRES_URL
# for app: "core-server" - env: CORE_SERVER_POSTGRES_URL
npm run rucken -- postgres
# Run migrate database for specific database
export DATABASE_URL=$SERVER_POSTGRES_URL && npm run migrate
# Change database host for applications
export POSTGRES_HOST=kaufman-bot-postgres
# Update all egnerated files
npm run generate
# Start all services
docker-compose -f ./docker/dev/docker-compose.yml --compatibility up -d
개발 인프라 재시작
npm run docker:dev:clean-restart
새로운 로직 확인
텔레그램 봇에서 로케일 변경 명령 보내기
데이터베이스 확인
데이터베이스로 컨테이너에 연결
docker exec -it $(docker ps -aqf "name=kaufman-bot-postgres") sh
애플리케이션 데이터베이스에 psql로 연결
set PGPASSWORD=password_develop&& psql -d kaufman_bot_develop -U admin_develop
텔레그램 사용자 선택
select * from "User";
endy@endy-virtual-machine:~/Projects/current/kaufman-bot$ docker exec -it $(docker ps -aqf "name=kaufman-bot-postgres") sh
# set PGPASSWORD=password_develop&& psql -d kaufman_bot_develop -U admin_develop
psql (13.3 (Debian 13.3-1.pgdg100+1))
Type "help" for help.
kaufman_bot_develop=> select * from "User";
id | telegramId | langCode
--------------------------------------+------------+----------
b659808e-35a8-4c93-a40a-96858b352779 | testId | en
25e4a306-a977-4536-bf05-73ce96a94b73 | 102375526 | en
(2 rows)
github 배포 구성 업데이트
.github/workflows/develop.deploy.yml
name: 'deploy'
# yamllint disable-line rule:truthy
on:
push:
branches:
- feature/73
jobs:
migrate:
runs-on: [self-hosted, develop-vps]
environment: dev
steps:
- name: Cloning repo
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Apply migrations
run: |
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
. ~/.nvm/nvm.sh
nvm --version
nvm install v16.13.2
nvm use v16.13.2
npm i --force
export POSTGRES_HOST=$(dokku postgres:info global-postgres --internal-ip)
export ROOT_POSTGRES_URL=postgres://postgres:${{secrets.ROOT_POSTGRES_PASSWORD}}@${POSTGRES_HOST}:5432/postgres?schema=public
export SERVER_POSTGRES_URL=${{secrets.SERVER_POSTGRES_URL}}
npm run rucken -- postgres
export DATABASE_URL=$SERVER_POSTGRES_URL && npm run migrate
dokku config:set --no-restart kaufman-bot SERVER_POSTGRES_URL=$SERVER_POSTGRES_URL
dokku config:set --no-restart --global POSTGRES_HOST=global-postgres
deploy:
needs: [migrate]
runs-on: ubuntu-latest
environment: dev
steps:
- name: Cloning repo
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Push to dokku
uses: dokku/github-action@master
with:
branch: 'feature/73'
git_remote_url: 'ssh://dokku@${{secrets.HOST}}:22/kaufman-bot'
ssh_private_key: ${{secrets.SSH_PRIVATE_KEY}}
vps 서버에서 데이터베이스 확인
데이터베이스에 연결
데이터베이스 전환
\connect kaufman_bot_develop
텔레그램 사용자 선택
select * from "User";
root@vps17825:~# dokku postgres:connect global-postgres
psql (13.3 (Debian 13.3-1.pgdg100+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.
global_postgres=# \connect kaufman_bot_develop
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
You are now connected to database "kaufman_bot_develop" as user "postgres".
kaufman_bot_develop=# select * from "User";
id | telegramId | langCode
--------------------------------------+------------+----------
7fa21a25-60a9-4d69-86d9-13770bd467fd | testId | en
99ea4d90-04a9-4d69-9a74-0eff06823f3a | 102375526 | ru
(2 rows)
다음 포스팅에서는 관리자 모드와 사용자 모드에서 메시지 디버깅을 위한 모듈을 추가하도록 하겠습니다...
Reference
이 문제에 관하여(KaufmanBot에 Prisma ORM 추가(NestJS 텔레그램 봇 애플리케이션)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/endykaufman/add-prisma-orm-to-kaufmanbot-nestjs-telegram-bot-application-3e8b텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)