entities, init schema migration
This commit is contained in:
parent
51344d2018
commit
968d83e58b
|
@ -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";
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,9 @@
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"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": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^9.0.0",
|
"@nestjs/common": "^9.0.0",
|
||||||
|
@ -51,6 +53,7 @@
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/jest": "29.5.1",
|
"@types/jest": "29.5.1",
|
||||||
"@types/lodash": "^4.14.198",
|
"@types/lodash": "^4.14.198",
|
||||||
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "18.16.12",
|
"@types/node": "18.16.12",
|
||||||
"@types/passport-jwt": "^3.0.9",
|
"@types/passport-jwt": "^3.0.9",
|
||||||
"@types/passport-local": "^1.0.35",
|
"@types/passport-local": "^1.0.35",
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { SwipesModule } from './swipes/swipes.module';
|
||||||
|
import { PairsModule } from './pairs/pairs.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -15,8 +17,11 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
password: process.env.DB_PASSWORD,
|
password: process.env.DB_PASSWORD,
|
||||||
database: process.env.DB_NAME,
|
database: process.env.DB_NAME,
|
||||||
entities: ['dist/**/*.entity.js'],
|
entities: ['dist/**/*.entity.js'],
|
||||||
|
// synchronize: true,
|
||||||
// logging: ['query']
|
// logging: ['query']
|
||||||
}),
|
}),
|
||||||
|
SwipesModule,
|
||||||
|
PairsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import cookieParser from 'cookie-parser';
|
import * as cookieParser from 'cookie-parser';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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[];
|
||||||
|
}
|
|
@ -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 {}
|
|
@ -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 },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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 {}
|
|
@ -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 } });
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
@Entity('users')
|
||||||
ADMIN = 'admin',
|
|
||||||
STANDARD = 'standard',
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity('Users')
|
|
||||||
export class User {
|
export class User {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
@ -19,10 +15,9 @@ export class User {
|
||||||
@Column({ unique: true })
|
@Column({ unique: true })
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@Column({
|
@OneToMany(() => Swipe, (swipe) => swipe.sourceUser)
|
||||||
type: 'enum',
|
swipes: Swipe[];
|
||||||
enum: UserRole,
|
|
||||||
default: UserRole.STANDARD,
|
@OneToMany(() => Swipe, (swipe) => swipe.targetUser)
|
||||||
})
|
swiped: Swipe[];
|
||||||
role: UserRole;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,16 @@ import { Injectable } from '@nestjs/common';
|
||||||
import { CreateUserDto } from './dto/create-user.dto';
|
import { CreateUserDto } from './dto/create-user.dto';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { User } from './entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
import { Repository } from 'typeorm';
|
import { In, Not, Repository } from 'typeorm';
|
||||||
import {
|
import {
|
||||||
UserEmailNotExistsException,
|
UserEmailNotExistsException,
|
||||||
UserIdNotExistsException,
|
UserIdNotExistsException,
|
||||||
} from 'src/utils/errors';
|
} from 'src/utils/errors';
|
||||||
|
|
||||||
|
interface GetOptions {
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -31,9 +35,38 @@ export class UsersService {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getByIds(ids: number[]) {
|
||||||
|
return this.usersRepository.findBy({ id: In(ids) });
|
||||||
|
}
|
||||||
|
|
||||||
async create(userData: CreateUserDto) {
|
async create(userData: CreateUserDto) {
|
||||||
const user = this.usersRepository.create(userData);
|
const user = this.usersRepository.create(userData);
|
||||||
await this.usersRepository.save(user);
|
await this.usersRepository.save(user);
|
||||||
return 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,3 +29,9 @@ export class UserIdNotExistsException extends HttpException {
|
||||||
super('User with this id does not exist', HttpStatus.NOT_FOUND);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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/**/*'],
|
||||||
|
});
|
|
@ -1053,6 +1053,13 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
||||||
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
|
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@*":
|
"@types/node@*":
|
||||||
version "20.6.0"
|
version "20.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.0.tgz#9d7daa855d33d4efec8aea88cd66db1c2f0ebe16"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.0.tgz#9d7daa855d33d4efec8aea88cd66db1c2f0ebe16"
|
||||||
|
|
Loading…
Reference in New Issue