entities, init schema migration

This commit is contained in:
Pravdin Egor 2023-09-19 20:37:42 +07:00
parent 51344d2018
commit 968d83e58b
16 changed files with 347 additions and 17 deletions

View File

@ -0,0 +1,51 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class InitSchema1695130227420 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE users (
id serial4 NOT NULL,
"name" varchar NOT NULL,
"password" varchar NOT NULL,
email varchar NOT NULL,
CONSTRAINT "users_pk" PRIMARY KEY (id),
CONSTRAINT "users_email_unique" UNIQUE (email)
);
CREATE TABLE swipes (
id serial4 NOT NULL,
liked bool NOT NULL,
"createdAt" timestamp NOT NULL DEFAULT now(),
"sourceUserId" int4 NULL,
"targetUserId" int4 NULL,
CONSTRAINT "PK_bb38af5831e2c084a78e3622ff6" PRIMARY KEY (id)
);
ALTER TABLE swipes ADD CONSTRAINT "swipes_target_user_fk" FOREIGN KEY ("targetUserId") REFERENCES users(id);
ALTER TABLE swipes ADD CONSTRAINT "swipes_source_user_fk" FOREIGN KEY ("sourceUserId") REFERENCES users(id);
CREATE TABLE pairs (
id serial4 NOT NULL,
CONSTRAINT "pairs_pk" PRIMARY KEY (id)
);
CREATE TABLE "pair-members" (
"pairId" int4 NOT NULL,
"userId" int4 NOT NULL,
CONSTRAINT "pair_members_pk" PRIMARY KEY ("pairId", "userId")
);
ALTER TABLE "pair-members" ADD CONSTRAINT "pair_members_user_fk" FOREIGN KEY ("userId") REFERENCES users(id);
ALTER TABLE "pair-members" ADD CONSTRAINT "pair_members_pair_fk" FOREIGN KEY ("pairId") REFERENCES pairs(id);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DROP TABLE users;
DROP TABLE swipes;
DROP TABLE pairs;
DROP TABLE "pair-members";
`);
}
}

View File

@ -17,7 +17,9 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json",
"typeorm": "ts-node ./node_modules/typeorm/cli",
"migrate:run": "npm run typeorm migration:run -- -d ./typeorm.config.ts"
},
"dependencies": {
"@nestjs/common": "^9.0.0",
@ -51,6 +53,7 @@
"@types/express": "^4.17.13",
"@types/jest": "29.5.1",
"@types/lodash": "^4.14.198",
"@types/multer": "^1.4.7",
"@types/node": "18.16.12",
"@types/passport-jwt": "^3.0.9",
"@types/passport-local": "^1.0.35",
@ -87,4 +90,4 @@
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
}

View File

@ -2,6 +2,8 @@ import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SwipesModule } from './swipes/swipes.module';
import { PairsModule } from './pairs/pairs.module';
@Module({
imports: [
@ -15,8 +17,11 @@ import { TypeOrmModule } from '@nestjs/typeorm';
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: ['dist/**/*.entity.js'],
// synchronize: true,
// logging: ['query']
}),
SwipesModule,
PairsModule,
],
})
export class AppModule {}

View File

@ -1,5 +1,5 @@
import { NestFactory } from '@nestjs/core';
import cookieParser from 'cookie-parser';
import * as cookieParser from 'cookie-parser';
import helmet from 'helmet';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

View File

@ -0,0 +1,14 @@
import { Entity, ManyToOne, PrimaryColumn } from 'typeorm';
import { Pair } from './pair.entity';
import { User } from 'src/users/entities/user.entity';
@Entity('pair-members')
export class PairMember {
@PrimaryColumn('int', { name: 'pairId' })
@ManyToOne(() => Pair, (pair) => pair.pairMembers)
pair: Pair;
@PrimaryColumn('int', { name: 'userId' })
@ManyToOne(() => User)
user: User;
}

View File

@ -0,0 +1,11 @@
import { Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { PairMember } from './pair-member.entity';
@Entity('pairs')
export class Pair {
@PrimaryGeneratedColumn()
id!: number;
@OneToMany(() => PairMember, (pairMember) => pairMember.pair)
pairMembers: PairMember[];
}

14
src/pairs/pairs.module.ts Normal file
View File

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { PairsService } from './pairs.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Pair } from './entities/pair.entity';
import { PairMember } from './entities/pair-member.entity';
import { UsersModule } from 'src/users/users.module';
import { Swipe } from 'src/swipes/entities/swipe.entity';
@Module({
imports: [TypeOrmModule.forFeature([Pair, PairMember, Swipe]), UsersModule],
providers: [PairsService],
exports: [PairsService],
})
export class PairsModule {}

View File

@ -0,0 +1,77 @@
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { Pair } from './entities/pair.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { PairMember } from './entities/pair-member.entity';
import { UsersService } from 'src/users/users.service';
import {
UserIdNotExistsException,
UserNotBelongsToPair,
} from 'src/utils/errors';
import { Swipe } from 'src/swipes/entities/swipe.entity';
@Injectable()
export class PairsService {
constructor(
@InjectRepository(Pair)
private readonly pairsRepository: Repository<Pair>,
@InjectRepository(PairMember)
private readonly pairMembersRepository: Repository<PairMember>,
private readonly usersService: UsersService,
) {}
async createPair(userIds: [number, number]) {
const users = await this.usersService.getByIds(userIds);
if (users.length !== 2) {
throw new UserIdNotExistsException();
}
const pair = await this.pairsRepository.manager.transaction(
async (manager) => {
const pair = this.pairsRepository.create();
await manager.save(pair);
const pairMembers = this.pairMembersRepository.create([
{
pair,
user: { id: userIds[0] },
},
{
pair,
user: { id: userIds[1] },
},
]);
await manager.save(pairMembers);
return pair;
},
);
return pair;
}
async destroyPair(userId: number, pairId: number) {
const pair = await this.pairsRepository.findOne({
where: { id: pairId },
relations: { pairMembers: { user: true } },
});
const members = pair.pairMembers;
if (!members.some((member: PairMember) => member.user.id === userId)) {
throw new UserNotBelongsToPair();
}
const anotherMember = members.find(
(member: PairMember) => member.user.id !== userId,
);
await this.pairsRepository.manager.transaction(async (manager) => {
await manager.delete(PairMember, { pair });
await manager.delete(Pair, { id: pairId });
await manager.update(
Swipe,
{
sourceUser: { id: userId },
targetUser: { id: anotherMember.user.id },
},
{ liked: false },
);
});
}
}

View File

@ -0,0 +1,26 @@
import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { User } from 'src/users/entities/user.entity';
@Entity('swipes')
export class Swipe {
@PrimaryGeneratedColumn()
id!: number;
@ManyToOne(() => User, (user) => user.swipes)
sourceUser: User;
@ManyToOne(() => User, (user) => user.swiped)
targetUser: User;
@Column()
liked: boolean;
@CreateDateColumn({ type: 'timestamp' })
createdAt: Date;
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Swipe } from './entities/swipe.entity';
import { SwipesService } from './swipes.service';
import { UsersModule } from 'src/users/users.module';
import { PairsModule } from 'src/pairs/pairs.module';
@Module({
imports: [TypeOrmModule.forFeature([Swipe]), UsersModule, PairsModule],
providers: [SwipesService],
})
export class SwipesModule {}

View File

@ -0,0 +1,58 @@
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { Swipe } from './entities/swipe.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { UsersService } from 'src/users/users.service';
import { UserIdNotExistsException } from 'src/utils/errors';
import { PairsService } from 'src/pairs/pairs.service';
@Injectable()
export class SwipesService {
constructor(
@InjectRepository(Swipe)
private readonly swipesRepository: Repository<Swipe>,
private readonly usersService: UsersService,
private readonly pairsService: PairsService,
) {}
async createSwipe(
sourceUserId: number,
targetUserId: number,
liked: boolean,
) {
const users = await this.usersService.getByIds([
sourceUserId,
targetUserId,
]);
if (users.length !== 2) {
throw new UserIdNotExistsException();
}
const swipe = this.swipesRepository.create({
sourceUser: { id: sourceUserId },
targetUser: { id: targetUserId },
liked,
});
await this.swipesRepository.save(swipe);
if (liked) {
const answeringLikeSwipe = await this.swipesRepository.findBy({
sourceUser: { id: targetUserId },
targetUser: { id: sourceUserId },
});
if (answeringLikeSwipe) {
await this.pairsService.createPair([sourceUserId, targetUserId]); // TODO use transaction?
}
}
return swipe;
}
async getSwipesByUser(sourceUserId: number) {
const user = await this.usersService.getById(sourceUserId);
if (!user) {
throw new UserIdNotExistsException();
}
return this.swipesRepository.findBy({ sourceUser: { id: sourceUserId } });
}
}

View File

@ -1,11 +1,7 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Swipe } from 'src/swipes/entities/swipe.entity';
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
export enum UserRole {
ADMIN = 'admin',
STANDARD = 'standard',
}
@Entity('Users')
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id!: number;
@ -19,10 +15,9 @@ export class User {
@Column({ unique: true })
email: string;
@Column({
type: 'enum',
enum: UserRole,
default: UserRole.STANDARD,
})
role: UserRole;
@OneToMany(() => Swipe, (swipe) => swipe.sourceUser)
swipes: Swipe[];
@OneToMany(() => Swipe, (swipe) => swipe.targetUser)
swiped: Swipe[];
}

View File

@ -2,12 +2,16 @@ import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { Repository } from 'typeorm';
import { In, Not, Repository } from 'typeorm';
import {
UserEmailNotExistsException,
UserIdNotExistsException,
} from 'src/utils/errors';
interface GetOptions {
limit: number;
}
@Injectable()
export class UsersService {
constructor(
@ -31,9 +35,38 @@ export class UsersService {
return user;
}
async getByIds(ids: number[]) {
return this.usersRepository.findBy({ id: In(ids) });
}
async create(userData: CreateUserDto) {
const user = this.usersRepository.create(userData);
await this.usersRepository.save(user);
return user;
}
async get(userId: number, options: GetOptions) {
// TODO get user preferences
const user = await this.usersRepository.findBy({ id: userId });
if (!user) {
throw new UserIdNotExistsException();
}
const users = await this.usersRepository.find({
where: {
swiped: {
sourceUser: {
id: Not(userId),
},
},
},
relations: {
swiped: true,
},
take: options.limit || 10,
});
return users;
}
}

View File

@ -29,3 +29,9 @@ export class UserIdNotExistsException extends HttpException {
super('User with this id does not exist', HttpStatus.NOT_FOUND);
}
}
export class UserNotBelongsToPair extends HttpException {
constructor() {
super('User do not belongs to this pair', HttpStatus.BAD_REQUEST);
}
}

18
typeorm.config.ts Normal file
View File

@ -0,0 +1,18 @@
import { DataSource } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import { config } from 'dotenv';
config();
const configService = new ConfigService();
export default new DataSource({
type: 'postgres',
host: configService.get('DB_HOST'),
port: +configService.get('DB_PORT'),
username: configService.get('DB_USER'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_NAME'),
entities: ['dist/**/*.entity.js'],
migrations: ['migrations/**/*'],
});

View File

@ -1053,6 +1053,13 @@
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
"@types/multer@^1.4.7":
version "1.4.7"
resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.7.tgz#89cf03547c28c7bbcc726f029e2a76a7232cc79e"
integrity sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==
dependencies:
"@types/express" "*"
"@types/node@*":
version "20.6.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.0.tgz#9d7daa855d33d4efec8aea88cd66db1c2f0ebe16"