add redis, graphql
This commit is contained in:
parent
c5936cf05e
commit
b1a7097d02
@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"useTabs": true,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": false,
|
||||
"arrowParens": "avoid",
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true,
|
||||
"importOrderCaseInsensitive": true,
|
||||
"importOrderParserPlugin": [
|
||||
"classProperties",
|
||||
"decorators-legacy",
|
||||
"typescript"
|
||||
],
|
||||
"importOrder": ["<THIRD_PARTY_MODULES>", "^@/(.*)$", "^../(.*)", "^./(.*)"],
|
||||
"plugins": ["@trivago/prettier-plugin-sort-imports"]
|
||||
}
|
||||
@ -13,9 +13,22 @@ services:
|
||||
networks:
|
||||
- teastream-backend
|
||||
|
||||
redis:
|
||||
container_name: teastream-redis
|
||||
image: redis:5.0
|
||||
restart: always
|
||||
ports:
|
||||
- 6379:6379
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- teastream-backend
|
||||
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
|
||||
networks:
|
||||
teastream-backend:
|
||||
|
||||
@ -1,16 +1,19 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import stylistic from '@stylistic/eslint-plugin'
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['eslint.config.mjs'],
|
||||
ignores: [
|
||||
'node_modules',
|
||||
'dist'
|
||||
],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
eslintPluginPrettierRecommended,
|
||||
stylistic.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
@ -23,6 +26,9 @@ export default tseslint.config(
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@stylistic': stylistic,
|
||||
}
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
@ -23,11 +22,22 @@
|
||||
"db:generate": "prisma generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/server": "^4.12.2",
|
||||
"@nestjs/apollo": "^13.1.0",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/graphql": "^13.1.0",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@prisma/client": "^6.10.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"connect-redis": "^9.0.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"express-session": "^1.18.1",
|
||||
"graphql": "^16.11.0",
|
||||
"ioredis": "^5.6.1",
|
||||
"prisma": "^6.10.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
@ -38,19 +48,18 @@
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"@swc/cli": "^0.6.0",
|
||||
"@swc/core": "^1.10.7",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"@types/cookie-parser": "^1.4.9",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/express-session": "^1.18.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
"globals": "^16.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
|
||||
20
backend/src/core/config/graphql.config.ts
Normal file
20
backend/src/core/config/graphql.config.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { isDev } from '@/src/shared/util/is-dev.util'
|
||||
import { ApolloDriverConfig } from '@nestjs/apollo'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
import { Request, Response } from 'express'
|
||||
import * as path from 'node:path'
|
||||
|
||||
type ContextType = {
|
||||
req: Request
|
||||
res: Response
|
||||
}
|
||||
|
||||
export function getGraphQLConfig(configService: ConfigService): ApolloDriverConfig {
|
||||
return {
|
||||
playground: isDev(configService),
|
||||
path: configService.getOrThrow('GRAPHQL_PREFIX'),
|
||||
autoSchemaFile: path.join(process.cwd(), 'src', 'core', 'graphql', 'schema.gql'),
|
||||
sortSchema: true,
|
||||
context: ({ req, res }: ContextType) => ({ req, res }),
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,28 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { getGraphQLConfig } from '@/src/core/config/graphql.config'
|
||||
import { AccountModule } from '@/src/module/auth/account/account.module'
|
||||
import { IS_DEV } from '@/src/shared/util/is-dev.util'
|
||||
import { ApolloDriver } from '@nestjs/apollo'
|
||||
import { Module } from '@nestjs/common'
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config'
|
||||
import { GraphQLModule } from '@nestjs/graphql'
|
||||
import { PrismaModule } from './prisma/prisma.module'
|
||||
import { RedisModule } from './redis/redis.module'
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
ignoreEnvFile: !IS_DEV,
|
||||
isGlobal: true,
|
||||
}),
|
||||
GraphQLModule.forRootAsync({
|
||||
driver: ApolloDriver,
|
||||
imports: [ConfigModule],
|
||||
useFactory: getGraphQLConfig,
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
PrismaModule,
|
||||
RedisModule,
|
||||
AccountModule,
|
||||
],
|
||||
})
|
||||
export class CoreModule {}
|
||||
|
||||
24
backend/src/core/graphql/schema.gql
Normal file
24
backend/src/core/graphql/schema.gql
Normal file
@ -0,0 +1,24 @@
|
||||
# ------------------------------------------------------
|
||||
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
|
||||
# ------------------------------------------------------
|
||||
|
||||
"""
|
||||
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
|
||||
"""
|
||||
scalar DateTime
|
||||
|
||||
type Query {
|
||||
findAllUsers: [UserModel!]!
|
||||
}
|
||||
|
||||
type UserModel {
|
||||
avatar: String
|
||||
bio: String
|
||||
createdAt: DateTime!
|
||||
displayName: String!
|
||||
email: String!
|
||||
id: ID!
|
||||
name: String!
|
||||
password: String!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma.service';
|
||||
import { Global, Module } from '@nestjs/common'
|
||||
import { PrismaService } from './prisma.service'
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
exports: [PrismaService]
|
||||
exports: [PrismaService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { PrismaClient } from '@/prisma/generated';
|
||||
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { PrismaClient } from '@/prisma/generated'
|
||||
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
await this.$connect()
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.$disconnect();
|
||||
await this.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
9
backend/src/core/redis/redis.module.ts
Normal file
9
backend/src/core/redis/redis.module.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common'
|
||||
import { RedisService } from './redis.service'
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [RedisService],
|
||||
exports: [RedisService],
|
||||
})
|
||||
export class RedisModule {}
|
||||
12
backend/src/core/redis/redis.service.ts
Normal file
12
backend/src/core/redis/redis.service.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
import Redis from 'ioredis'
|
||||
|
||||
@Injectable()
|
||||
export class RedisService extends Redis {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
super(configService.getOrThrow('REDIS_URI'))
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,50 @@
|
||||
import { CoreModule } from '@/src/core/core.module';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { RedisService } from '@/src/core/redis/redis.service'
|
||||
import { ms, StringValue } from '@/src/shared/util/ms.util'
|
||||
import { parseBoolean } from '@/src/shared/util/parse-boolean.util'
|
||||
import { ValidationPipe } from '@nestjs/common'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
import { NestFactory } from '@nestjs/core'
|
||||
import { RedisStore } from 'connect-redis'
|
||||
import * as cookieParser from 'cookie-parser'
|
||||
import { CoreModule } from '@/src/core/core.module'
|
||||
import * as session from 'express-session'
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(CoreModule);
|
||||
await app.listen(process.env.PORT ?? 3000);
|
||||
const app = await NestFactory.create(CoreModule)
|
||||
|
||||
const config = app.get(ConfigService)
|
||||
const redis = app.get(RedisService)
|
||||
|
||||
app.use(cookieParser(config.getOrThrow<string>('COOKIE_SECRET')))
|
||||
|
||||
app.useGlobalPipes(new ValidationPipe({
|
||||
transform: true,
|
||||
}))
|
||||
|
||||
app.use(session({
|
||||
secret: config.getOrThrow<string>('SESSION_SECRET'),
|
||||
name: config.getOrThrow<string>('SESSION_NAME'),
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
domain: config.getOrThrow<string>('SESSION_DOMAIN'),
|
||||
maxAge: ms(config.getOrThrow<StringValue>('SESSION_MAX_AGE')),
|
||||
httpOnly: parseBoolean(config.getOrThrow('SESSION_HTTP_ONLY')),
|
||||
secure: parseBoolean(config.getOrThrow('SESSION_SECURE')),
|
||||
sameSite: 'lax',
|
||||
},
|
||||
store: new RedisStore({
|
||||
client: redis,
|
||||
prefix: config.getOrThrow('SESSION_FOLDER'),
|
||||
}),
|
||||
}))
|
||||
|
||||
app.enableCors({
|
||||
origin: config.getOrThrow<string>('ALLOWED_ORIGIN'),
|
||||
credential: true,
|
||||
exposedHeaders: ['set-cookie'],
|
||||
})
|
||||
|
||||
await app.listen(config.getOrThrow('APPLICATION_PORT'))
|
||||
}
|
||||
bootstrap();
|
||||
void bootstrap()
|
||||
|
||||
8
backend/src/module/auth/account/account.module.ts
Normal file
8
backend/src/module/auth/account/account.module.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { AccountService } from './account.service'
|
||||
import { AccountResolver } from './account.resolver'
|
||||
|
||||
@Module({
|
||||
providers: [AccountResolver, AccountService],
|
||||
})
|
||||
export class AccountModule {}
|
||||
13
backend/src/module/auth/account/account.resolver.ts
Normal file
13
backend/src/module/auth/account/account.resolver.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Query, Resolver } from '@nestjs/graphql'
|
||||
import { AccountService } from './account.service'
|
||||
import { UserModel } from './models/user.model'
|
||||
|
||||
@Resolver('Account')
|
||||
export class AccountResolver {
|
||||
constructor(private readonly accountService: AccountService) {}
|
||||
|
||||
@Query(() => [UserModel], { name: 'findAllUsers' })
|
||||
public async findAll() {
|
||||
return await this.accountService.findAll()
|
||||
}
|
||||
}
|
||||
15
backend/src/module/auth/account/account.service.ts
Normal file
15
backend/src/module/auth/account/account.service.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
||||
import { Injectable } from '@nestjs/common'
|
||||
|
||||
@Injectable()
|
||||
export class AccountService {
|
||||
constructor(
|
||||
private readonly prismaService: PrismaService,
|
||||
) {
|
||||
}
|
||||
|
||||
public async findAll() {
|
||||
const users = await this.prismaService.user.findMany({})
|
||||
return users
|
||||
}
|
||||
}
|
||||
31
backend/src/module/auth/account/models/user.model.ts
Normal file
31
backend/src/module/auth/account/models/user.model.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class UserModel {
|
||||
@Field(() => ID)
|
||||
id: string
|
||||
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => String)
|
||||
password: string
|
||||
|
||||
@Field(() => String)
|
||||
name: string
|
||||
|
||||
@Field(() => String)
|
||||
displayName: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
avatar: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
bio: string
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date
|
||||
|
||||
@Field(() => Date)
|
||||
updatedAt: Date
|
||||
}
|
||||
11
backend/src/shared/util/is-dev.util.ts
Normal file
11
backend/src/shared/util/is-dev.util.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
import * as dotenv from 'dotenv'
|
||||
import * as process from 'node:process'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
export function isDev(configService: ConfigService) {
|
||||
return configService.getOrThrow('NODE_ENV') === 'development'
|
||||
}
|
||||
|
||||
export const IS_DEV = process.env.NODE_ENV === 'development'
|
||||
112
backend/src/shared/util/ms.util.ts
Normal file
112
backend/src/shared/util/ms.util.ts
Normal file
@ -0,0 +1,112 @@
|
||||
const s = 1000
|
||||
const m = s * 60
|
||||
const h = m * 60
|
||||
const d = h * 24
|
||||
const w = d * 7
|
||||
const y = d * 365.25
|
||||
|
||||
type Unit
|
||||
= | 'Years'
|
||||
| 'Year'
|
||||
| 'Yrs'
|
||||
| 'Yr'
|
||||
| 'Y'
|
||||
| 'Weeks'
|
||||
| 'Week'
|
||||
| 'W'
|
||||
| 'Days'
|
||||
| 'Day'
|
||||
| 'D'
|
||||
| 'Hours'
|
||||
| 'Hour'
|
||||
| 'Hrs'
|
||||
| 'Hr'
|
||||
| 'H'
|
||||
| 'Minutes'
|
||||
| 'Minute'
|
||||
| 'Mins'
|
||||
| 'Min'
|
||||
| 'M'
|
||||
| 'Seconds'
|
||||
| 'Second'
|
||||
| 'Secs'
|
||||
| 'Sec'
|
||||
| 's'
|
||||
| 'Milliseconds'
|
||||
| 'Millisecond'
|
||||
| 'Msecs'
|
||||
| 'Msec'
|
||||
| 'Ms'
|
||||
|
||||
type UnitAnyCase = Unit | Uppercase<Unit> | Lowercase<Unit>
|
||||
|
||||
export type StringValue
|
||||
= | `${number}`
|
||||
| `${number}${UnitAnyCase}`
|
||||
| `${number} ${UnitAnyCase}`
|
||||
|
||||
export function ms(str: StringValue): number {
|
||||
if (typeof str !== 'string' || str.length === 0 || str.length > 100) {
|
||||
throw new Error(
|
||||
'Value provided to ms() must be a string with length between 1 and 99.',
|
||||
)
|
||||
}
|
||||
|
||||
const match
|
||||
= /^(?<value>-?(?:\d+)?\.?\d+) *(?<type>milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(
|
||||
str,
|
||||
)
|
||||
|
||||
const groups = match?.groups as { value: string, type?: string } | undefined
|
||||
if (!groups) {
|
||||
return NaN
|
||||
}
|
||||
const n = parseFloat(groups.value)
|
||||
const type = (groups.type || 'ms').toLowerCase() as Lowercase<Unit>
|
||||
|
||||
switch (type) {
|
||||
case 'years':
|
||||
case 'year':
|
||||
case 'yrs':
|
||||
case 'yr':
|
||||
case 'y':
|
||||
return n * y
|
||||
case 'weeks':
|
||||
case 'week':
|
||||
case 'w':
|
||||
return n * w
|
||||
case 'days':
|
||||
case 'day':
|
||||
case 'd':
|
||||
return n * d
|
||||
case 'hours':
|
||||
case 'hour':
|
||||
case 'hrs':
|
||||
case 'hr':
|
||||
case 'h':
|
||||
return n * h
|
||||
case 'minutes':
|
||||
case 'minute':
|
||||
case 'mins':
|
||||
case 'min':
|
||||
case 'm':
|
||||
return n * m
|
||||
case 'seconds':
|
||||
case 'second':
|
||||
case 'secs':
|
||||
case 'sec':
|
||||
case 's':
|
||||
return n * s
|
||||
case 'milliseconds':
|
||||
case 'millisecond':
|
||||
case 'msecs':
|
||||
case 'msec':
|
||||
case 'ms':
|
||||
return n
|
||||
default:
|
||||
throw new Error(
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
`Ошибка: единица времени ${type} была распознана, но не существует соответствующего случая. Пожалуйста, проверьте введенные данные.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
19
backend/src/shared/util/parse-boolean.util.ts
Normal file
19
backend/src/shared/util/parse-boolean.util.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export function parseBoolean(value: string): boolean {
|
||||
if (typeof value === 'boolean') {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const lowerValue = value.trim().toLowerCase()
|
||||
if (lowerValue === 'true') {
|
||||
return true
|
||||
}
|
||||
if (lowerValue === 'false') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Не удалось преобразовать значение "${value}" в логическое значение.`,
|
||||
)
|
||||
}
|
||||
1028
backend/yarn.lock
1028
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user