From 8e71a9db57f1cb75ddd8306454442beeb7aabfec Mon Sep 17 00:00:00 2001 From: Nikita Kudinov Date: Sun, 4 Feb 2024 03:36:22 +0300 Subject: [PATCH] refactor: use interfaces for repositories --- .../strategies/access-token-auth.strategy.ts | 4 +- src/api/strategies/local-auth.strategy.ts | 4 +- .../strategies/refresh-token-auth.strategy.ts | 4 +- .../repositories/opened-packs.repository.ts | 37 +-- src/core/repositories/packs.repository.ts | 113 +------- src/core/repositories/pokemons.repository.ts | 37 +-- .../quick-sold-user-items.repository.ts | 39 +-- .../trades-to-user-items.repository.ts | 193 ++------------ src/core/repositories/trades.repository.ts | 238 ++--------------- .../repositories/user-items.repository.ts | 238 +++-------------- .../user-refresh-tokens.repository.ts | 119 ++------- src/core/repositories/users.repository.ts | 163 ++---------- src/core/services/auth.service.ts | 8 +- src/core/services/packs.service.ts | 16 +- src/core/services/pending-trades.service.ts | 16 +- src/core/services/pokemons.service.ts | 4 +- src/core/services/trades.service.ts | 4 +- src/core/services/user-items.service.ts | 12 +- src/core/services/users.service.ts | 4 +- src/infra/cron-jobs/cron-jobs.service.ts | 4 +- src/infra/ioc/core-modules/packs.module.ts | 20 +- src/infra/ioc/core-modules/pokemons.module.ts | 13 +- src/infra/ioc/core-modules/trades.module.ts | 20 +- .../ioc/core-modules/user-items.module.ts | 20 +- .../user-refresh-tokens.module.ts | 10 +- src/infra/ioc/core-modules/users.module.ts | 13 +- .../repositories/opened-pack.repository.ts | 36 +++ .../postgres/repositories/packs.repository.ts | 111 ++++++++ .../repositories/pokemons.repository.ts | 33 +++ .../quick-sold-user-items.repository.ts | 38 +++ .../trades-to-user-items.repository.ts | 194 ++++++++++++++ .../repositories/trades.repository.ts | 246 ++++++++++++++++++ .../repositories/user-items.repository.ts | 242 +++++++++++++++++ .../user-refresh-tokens.repository.ts | 113 ++++++++ .../postgres/repositories/users.repository.ts | 162 ++++++++++++ src/infra/postgres/seeders/pokemons.seeder.ts | 4 +- 36 files changed, 1427 insertions(+), 1105 deletions(-) create mode 100644 src/infra/postgres/repositories/opened-pack.repository.ts create mode 100644 src/infra/postgres/repositories/packs.repository.ts create mode 100644 src/infra/postgres/repositories/pokemons.repository.ts create mode 100644 src/infra/postgres/repositories/quick-sold-user-items.repository.ts create mode 100644 src/infra/postgres/repositories/trades-to-user-items.repository.ts create mode 100644 src/infra/postgres/repositories/trades.repository.ts create mode 100644 src/infra/postgres/repositories/user-items.repository.ts create mode 100644 src/infra/postgres/repositories/user-refresh-tokens.repository.ts create mode 100644 src/infra/postgres/repositories/users.repository.ts diff --git a/src/api/strategies/access-token-auth.strategy.ts b/src/api/strategies/access-token-auth.strategy.ts index b563a5b..e1e5fb9 100644 --- a/src/api/strategies/access-token-auth.strategy.ts +++ b/src/api/strategies/access-token-auth.strategy.ts @@ -5,14 +5,14 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; import { AppAuthException, AppEntityNotFoundException } from 'src/core/exceptions'; import { Nullable } from 'src/common/types'; import { UserTokenPayload } from '../types'; -import { UsersRepository } from 'src/core/repositories/users.repository'; +import { IUsersRepository } from 'src/core/repositories/users.repository'; import { EnvVariables } from 'src/infra/config/env.config'; import { UserEntity } from 'src/infra/postgres/tables'; @Injectable() export class AccessTokenAuthStrategy extends PassportStrategy(Strategy, 'access-token') { public constructor( - private readonly usersRepository: UsersRepository, + private readonly usersRepository: IUsersRepository, configService: ConfigService, ) { super({ diff --git a/src/api/strategies/local-auth.strategy.ts b/src/api/strategies/local-auth.strategy.ts index 7b4edf0..7c9cee4 100644 --- a/src/api/strategies/local-auth.strategy.ts +++ b/src/api/strategies/local-auth.strategy.ts @@ -2,14 +2,14 @@ import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Strategy } from 'passport-local'; import * as bcrypt from 'bcrypt'; -import { UsersRepository } from 'src/core/repositories/users.repository'; +import { IUsersRepository } from 'src/core/repositories/users.repository'; import { UserEntity } from 'src/infra/postgres/tables'; import { Nullable } from 'src/common/types'; import { AppAuthException } from 'src/core/exceptions'; @Injectable() export class LocalAuthStrategy extends PassportStrategy(Strategy) { - public constructor(private readonly usersRepository: UsersRepository) { + public constructor(private readonly usersRepository: IUsersRepository) { super(); } diff --git a/src/api/strategies/refresh-token-auth.strategy.ts b/src/api/strategies/refresh-token-auth.strategy.ts index 0ddf534..fa861c1 100644 --- a/src/api/strategies/refresh-token-auth.strategy.ts +++ b/src/api/strategies/refresh-token-auth.strategy.ts @@ -5,14 +5,14 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; import { AppAuthException, AppEntityNotFoundException } from 'src/core/exceptions'; import { Nullable, } from 'src/common/types'; import { UserTokenPayload } from '../types'; -import { UsersRepository } from 'src/core/repositories/users.repository'; +import { IUsersRepository } from 'src/core/repositories/users.repository'; import { EnvVariables } from 'src/infra/config/env.config'; import { UserEntity } from 'src/infra/postgres/tables'; @Injectable() export class RefreshTokenAuthStrategy extends PassportStrategy(Strategy, 'refresh-token') { public constructor( - private readonly usersRepository: UsersRepository, + private readonly usersRepository: IUsersRepository, configService: ConfigService, ) { super({ diff --git a/src/core/repositories/opened-packs.repository.ts b/src/core/repositories/opened-packs.repository.ts index 4b27678..04321f5 100644 --- a/src/core/repositories/opened-packs.repository.ts +++ b/src/core/repositories/opened-packs.repository.ts @@ -1,35 +1,8 @@ -import { Injectable } from '@nestjs/common'; -import { Database, Transaction } from 'src/infra/postgres/types'; -import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; -import { CreateOpenedPackEntityValues, OpenedPackEntity, openedPacksTable } from 'src/infra/postgres/tables'; +import { CreateOpenedPackEntityValues, OpenedPackEntity } from 'src/infra/postgres/tables'; -@Injectable() -export class OpenedPacksRepository { - public constructor( - @InjectDatabase() - private readonly db: Database, - ) {} - - public async createOpenedPack( +export abstract class IOpenedPacksRepository { + public abstract createOpenedPack( values: CreateOpenedPackEntityValues, - tx?: Transaction, - ): Promise { - const { user, pack, pokemon } = values; - - return (tx ?? this.db) - .insert(openedPacksTable) - .values({ - ...values, - userId: user.id, - packId: pack.id, - pokemonId: pokemon.id, - }) - .returning() - .then(([openedPack]) => ({ - ...openedPack!, - user, - pack, - pokemon, - })); - } + tx?: unknown, + ): Promise; } diff --git a/src/core/repositories/packs.repository.ts b/src/core/repositories/packs.repository.ts index 59f387d..f8d161f 100644 --- a/src/core/repositories/packs.repository.ts +++ b/src/core/repositories/packs.repository.ts @@ -1,12 +1,6 @@ -import { Injectable } from '@nestjs/common'; -import { Optional, PaginatedArray, UUIDv4 } from 'src/common/types'; -import { PackEntity, packsTable, packsToPokemonsTable, PokemonEntity, pokemonsTable } from 'src/infra/postgres/tables'; -import { and, eq, getTableColumns, inArray, like, SQL, sql } from 'drizzle-orm'; -import { Database } from 'src/infra/postgres/types'; -import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; -import { mapArrayToPaginatedArray } from 'src/common/helpers/map-array-to-paginated-array.helper'; -import { AppEntityNotFoundException, AppInternalException } from '../exceptions'; -import { FindEntitiesOptions, FindEntitiesWithPaginationOptions, FindEntityByIdOptions, FindEntityOptions } from '../types'; +import { PaginatedArray, UUIDv4 } from 'src/common/types'; +import { PackEntity, PokemonEntity } from 'src/infra/postgres/tables'; +import { FindEntitiesWithPaginationOptions, FindEntityByIdOptions, FindEntityOptions } from '../types'; export type FindPacksWhere = Partial<{ id: UUIDv4, @@ -15,103 +9,20 @@ export type FindPacksWhere = Partial<{ nameLike: string, }>; -@Injectable() -export class PacksRepository { - public constructor( - @InjectDatabase() - private readonly db: Database, - ) {} - - private mapWhereToSQL( - where: FindPacksWhere - ): Optional { - return and( - where.id !== undefined ? eq(packsTable.id, where.id) : undefined, - where.ids !== undefined ? inArray(packsTable.id, where.ids) : undefined, - where.name !== undefined ? eq(packsTable.name, where.name) : undefined, - where.nameLike !== undefined ? like(packsTable.name, `%${where.nameLike}%`) : undefined, - ); - } - - private baseSelectBuilder( - options: FindEntitiesOptions, - ) { - const { where = {} } = options; - - return this.db - .select() - .from(packsTable) - .where(this.mapWhereToSQL(where)); - } - - public async findPacksWithPagination( +export abstract class IPacksRepository { + public abstract findPacksWithPagination( options: FindEntitiesWithPaginationOptions, - ): Promise> { - const { - paginationOptions: { page, limit }, - } = options; - // TODO: check for boundaries - const offset = (page - 1) * limit; - - return this - .baseSelectBuilder(options) - .offset(offset) - .limit(limit) - .then((packs) => mapArrayToPaginatedArray(packs, { page, limit })) - } + ): Promise>; - public async findPack( + public abstract findPack( options: FindEntityOptions, - ): Promise { - const { - notFoundErrorMessage = 'Pack not found', - } = options; - - const pack = await this - .baseSelectBuilder(options) - .limit(1) - .then(([pack]) => pack ?? null); + ): Promise; - if (!pack) { - throw new AppEntityNotFoundException(notFoundErrorMessage); - } - - return pack; - } - - public async findPackById( + public abstract findPackById( options: FindEntityByIdOptions, - ): Promise { - const { - id, - notFoundErrorMessageFn = (id) => `Pack (\`${id}\`) not found`, - } = options; - - return this.findPack({ - where: { id }, - notFoundErrorMessage: notFoundErrorMessageFn(id), - }) - } + ): Promise; - public async findRandomPokemonFromPack( + public abstract findRandomPokemonFromPack( pack: PackEntity - ): Promise { - const pokemon = await this.db - .select({ pokemon: getTableColumns(pokemonsTable) }) - .from(packsTable) - .innerJoin(packsToPokemonsTable, eq(packsToPokemonsTable.packId, packsTable.id)) - .innerJoin(pokemonsTable, eq(pokemonsTable.id, packsToPokemonsTable.pokemonId)) - .where(eq(packsTable.id, pack.id)) - .orderBy(sql`random()`) - .limit(1) - .then(([row]) => row?.pokemon ?? null); - - if (!pokemon) { - throw new AppInternalException( - 'There are no pokemons in the pack. Please notify the developer about it :)', - ); - } - - return pokemon; - } + ): Promise; } diff --git a/src/core/repositories/pokemons.repository.ts b/src/core/repositories/pokemons.repository.ts index eb6aebc..6188d9f 100644 --- a/src/core/repositories/pokemons.repository.ts +++ b/src/core/repositories/pokemons.repository.ts @@ -1,32 +1,13 @@ -import { Injectable } from '@nestjs/common'; -import { Database, Transaction } from 'src/infra/postgres/types'; -import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; -import { CreatePokemonEntityValues, PokemonEntity, pokemonsTable } from 'src/infra/postgres/tables'; +import { CreatePokemonEntityValues, PokemonEntity } from 'src/infra/postgres/tables'; -@Injectable() -export class PokemonsRepository { - public constructor( - @InjectDatabase() - private readonly db: Database, - ) {} - - public async createPokemons( +export abstract class IPokemonsRepository { + public abstract createPokemons( values: Array, - tx?: Transaction, - ): Promise> { - if (!values.length) return []; - - return (tx ?? this.db) - .insert(pokemonsTable) - .values(values) - .returning() - } + tx?: unknown, + ): Promise>; - public async deleteAllPokemons( - tx?: Transaction, - ): Promise { - await (tx ?? this.db) - .delete(pokemonsTable) - .returning(); - } + public abstract deleteAllPokemons( + tx?: unknown, + ): Promise; } + diff --git a/src/core/repositories/quick-sold-user-items.repository.ts b/src/core/repositories/quick-sold-user-items.repository.ts index dc21a27..6d6d815 100644 --- a/src/core/repositories/quick-sold-user-items.repository.ts +++ b/src/core/repositories/quick-sold-user-items.repository.ts @@ -1,37 +1,8 @@ -import { Injectable } from '@nestjs/common'; -import { Database, Transaction } from 'src/infra/postgres/types'; -import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; -import { QuickSoldUserItemEntity, quickSoldUserItemsTable, UserItemEntity } from 'src/infra/postgres/tables'; -import { UserItemsRepository } from './user-items.repository'; +import { QuickSoldUserItemEntity, UserItemEntity } from 'src/infra/postgres/tables'; -@Injectable() -export class QuickSoldUserItemsRepository { - public constructor( - @InjectDatabase() - private readonly db: Database, - - private readonly userItemsRepository: UserItemsRepository, - ) {} - - public async createQuickSoldUserItem( +export abstract class IQuickSoldUserItemsRepository { + public abstract createQuickSoldUserItem( userItem: UserItemEntity, - tx?: Transaction, - ): Promise { - const { user, pokemon } = userItem; - - const [quickSoldUserItem] = await Promise.all([ - (tx ?? this.db) - .insert(quickSoldUserItemsTable) - .values(userItem) - .returning() - .then(([quickSoldUserItem]) => ({ - ...quickSoldUserItem!, - user, - pokemon, - })), - this.userItemsRepository.deleteUserItem(userItem, tx), - ]); - - return quickSoldUserItem; - } + tx?: unknown, + ): Promise; } diff --git a/src/core/repositories/trades-to-user-items.repository.ts b/src/core/repositories/trades-to-user-items.repository.ts index a3df37d..407fbe0 100644 --- a/src/core/repositories/trades-to-user-items.repository.ts +++ b/src/core/repositories/trades-to-user-items.repository.ts @@ -1,202 +1,43 @@ -import { Injectable } from '@nestjs/common'; -import { and, eq, SQL } from 'drizzle-orm'; -import { alias } from 'drizzle-orm/pg-core'; -import { zip } from 'lodash'; -import { Optional, UUIDv4 } from 'src/common/types'; +import { UUIDv4 } from 'src/common/types'; import { FindEntitiesOptions } from 'src/core/types'; -import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; -import { Database, Transaction } from 'src/infra/postgres/types'; import { CreateTradeToReceiverItemEntityValues, CreateTradeToSenderItemEntityValues, - CreateTradeToUserItemEntityValues, - pokemonsTable, - tradesTable, - tradesToUserItemsTable, TradeToReceiverItemEntity, TradeToSenderItemEntity, TradeToUserItemEntity, TradeToUserItemUserType, - userItemsTable, - usersTable, } from 'src/infra/postgres/tables'; -import { mapTradesRowToEntity } from './trades.repository'; -import { mapUserItemsRowToEntity } from './user-items.repository'; -type FindTradesToUserItemsWhere = Partial<{ +export type FindTradesToUserItemsWhere = Partial<{ tradeId: UUIDv4, userType: TradeToUserItemUserType, userItemId: UUIDv4, }>; -type FindTradesToSenderItemsWhere = Omit; -type FindTradesToReceiverItemsWhere = Omit; +export type FindTradesToSenderItemsWhere = Omit; +export type FindTradesToReceiverItemsWhere = Omit; -export const mapTradesToUserItemsRowToEntity = ( - row: Record< - | 'trades_to_user_items' - | 'trades' - | 'senders' - | 'receivers' - | 'user_items' - | 'users' - | 'pokemons', - any>, -): TradeToUserItemEntity => { - return { - ...row.trades_to_user_items, - trade: mapTradesRowToEntity(row), - userItem: mapUserItemsRowToEntity(row), - } -} - -@Injectable() -export class TradesToUserItemsRepository { - public constructor( - @InjectDatabase() - private readonly db: Database, - ) {} - - private mapWhereToSQL( - where: FindTradesToUserItemsWhere, - ): Optional { - return and( - where.tradeId !== undefined ? eq(tradesToUserItemsTable.tradeId, where.tradeId) : undefined, - where.userType !== undefined ? eq(tradesToUserItemsTable.userType, where.userType) : undefined, - where.userItemId !== undefined ? eq(tradesToUserItemsTable.userItemId, where.userItemId) : undefined, - ); - }; - - private baseSelectBuilder( - options: FindEntitiesOptions, - ) { - const { where = {} } = options; - - const sendersTable = alias(usersTable, 'senders'); - const receiversTable = alias(usersTable, 'receivers'); - - return this.db - .select() - .from(tradesToUserItemsTable) - .innerJoin(tradesTable, eq(tradesTable.id, tradesToUserItemsTable.tradeId)) - .innerJoin(sendersTable, eq(sendersTable.id, tradesTable.senderId)) - .innerJoin(receiversTable, eq(receiversTable.id, tradesTable.receiverId)) - .innerJoin(userItemsTable, eq(userItemsTable.id, tradesToUserItemsTable.userItemId)) - .innerJoin(usersTable, eq(usersTable.id, userItemsTable.userId)) - .innerJoin(pokemonsTable, eq(pokemonsTable.id, userItemsTable.pokemonId)) - .where(this.mapWhereToSQL(where)); - } - - public async findTradesToUserItems( +export abstract class ITradesToUserItemsRepository { + public abstract findTradesToUserItems( options: FindEntitiesOptions, - ): Promise> { - return this - .baseSelectBuilder(options) - .then((rows) => rows.map((row) => mapTradesToUserItemsRowToEntity(row))); - } + ): Promise>; - public async findTradesToSenderItems( + public abstract findTradesToSenderItems( options: FindEntitiesOptions, - ): Promise> { - const userType = 'SENDER'; - - return this - .findTradesToUserItems({ - ...options, - where: { - ...options.where, - userType, - } - }) - .then((tradesToUserItems) => tradesToUserItems.map((tradeToUserItem) => ({ - ...tradeToUserItem, - userType, - senderItem: tradeToUserItem.userItem, - }))); - } + ): Promise>; - public async findTradesToReceiverItems( + public abstract findTradesToReceiverItems( options: FindEntitiesOptions, - ): Promise> { - const userType = 'RECEIVER'; - - return this - .findTradesToUserItems({ - ...options, - where: { - ...options.where, - userType, - } - }) - .then((tradesToUserItems) => tradesToUserItems.map((tradeToUserItem) => ({ - ...tradeToUserItem, - userType, - receiverItem: tradeToUserItem.userItem, - }))); - } - - private async createTradesToUserItems( - valuesArray: Array, - tx?: Transaction, - ): Promise> { - if (!valuesArray.length) return []; + ): Promise>; - return (tx ?? this.db) - .insert(tradesToUserItemsTable) - .values(valuesArray.map((values) => ({ - ...values, - tradeId: values.trade.id, - userItemId: values.userItem.id, - }))) - .returning() - .then((tradesToUserItems) => zip(valuesArray, tradesToUserItems).map(([values, tradeToUserItem]) => ({ - ...tradeToUserItem!, - trade: values!.trade, - userItem: values!.userItem, - }))); - } - - public async createTradesToSenderItems( + public abstract createTradesToSenderItems( valuesArray: Array, - tx?: Transaction, - ): Promise> { - const userType = 'SENDER'; - - return this - .createTradesToUserItems( - valuesArray.map((values) => ({ - ...values, - userType, - userItem: values.senderItem, - })), - tx, - ) - .then((tradesToUserItems) => tradesToUserItems.map((tradesToUserItem) => ({ - ...tradesToUserItem, - userType, - senderItem: tradesToUserItem.userItem, - }))); - } + tx?: unknown, + ): Promise>; - public async createTradesToReceiverItems( + public abstract createTradesToReceiverItems( valuesArray: Array, - tx?: Transaction, - ): Promise> { - const userType = 'RECEIVER'; - - return this - .createTradesToUserItems( - valuesArray.map((values) => ({ - ...values, - userType, - userItem: values.receiverItem, - })), - tx, - ) - .then((tradesToUserItems) => tradesToUserItems.map((tradesToUserItem) => ({ - ...tradesToUserItem, - userType, - receiverItem: tradesToUserItem.userItem, - }))); - } + tx?: unknown, + ): Promise>; } diff --git a/src/core/repositories/trades.repository.ts b/src/core/repositories/trades.repository.ts index 76caf42..ed11f64 100644 --- a/src/core/repositories/trades.repository.ts +++ b/src/core/repositories/trades.repository.ts @@ -1,8 +1,4 @@ -import { Injectable } from '@nestjs/common'; -import { Database, Transaction } from 'src/infra/postgres/types'; import { - tradesTable, - usersTable, TradeEntity, TradeStatus, PendingTradeEntity, @@ -13,13 +9,8 @@ import { AcceptedTradeEntity, RejectedTradeEntity, } from 'src/infra/postgres/tables'; -import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; -import { and, eq, inArray, sql, SQL } from 'drizzle-orm'; -import { Optional, UUIDv4 } from 'src/common/types'; -import { alias } from 'drizzle-orm/pg-core'; -import { TradesToUserItemsRepository } from './trades-to-user-items.repository'; -import { AppEntityNotFoundException } from '../exceptions'; -import { FindEntitiesOptions, FindEntityByIdOptions, FindEntityOptions } from '../types'; +import { UUIDv4 } from 'src/common/types'; +import { FindEntityByIdOptions, FindEntityOptions } from '../types'; export type FindTradesWhere = Partial<{ id: UUIDv4, @@ -29,226 +20,41 @@ export type FindTradesWhere = Partial<{ export type FindPendingTradesWhere = Omit; -export const mapTradesRowToEntity = ( - row: Record<'trades' | 'senders' | 'receivers', any>, -): TradeEntity => { - return { - ...row.trades, - sender: row.senders, - receiver: row.receivers, - }; -}; - -@Injectable() -export class TradesRepository { - public constructor( - @InjectDatabase() - private readonly db: Database, - - private readonly tradesToUserItemsRepository: TradesToUserItemsRepository, - ) {} - - private mapWhereToSQL( - where: FindTradesWhere, - ): Optional { - return and( - where.id !== undefined ? eq(tradesTable.id, where.id) : undefined, - where.ids !== undefined ? inArray(tradesTable.id, where.ids) : undefined, - where.status !== undefined ? eq(tradesTable.status, where.status) : undefined, - ); - } - - private baseSelectBuilder( - options: FindEntitiesOptions, - ) { - const { where = {} } = options; - - const sendersTable = alias(usersTable, 'senders'); - const receiversTable = alias(usersTable, 'receivers'); - - return this.db - .select() - .from(tradesTable) - .innerJoin(sendersTable, eq(sendersTable.id, tradesTable.senderId)) - .innerJoin(receiversTable, eq(receiversTable.id, tradesTable.receiverId)) - .where(this.mapWhereToSQL(where)); - } - - public async findTrade( +export abstract class ITradesRepository { + public abstract findTrade( options: FindEntityOptions, - ): Promise { - const { - notFoundErrorMessage = 'Trade not found', - } = options; - - const trade = await this - .baseSelectBuilder(options) - .limit(1) - .then(([row]) => ( - row - ? mapTradesRowToEntity(row) - : null - )); - - if (!trade) { - throw new AppEntityNotFoundException(notFoundErrorMessage); - } - - return trade; - } + ): Promise; - public async findPendingTrade( + public abstract findPendingTrade( options: FindEntityOptions, - ): Promise { - const { - notFoundErrorMessage = 'Pending trade not found', - } = options; - const status = 'PENDING'; + ): Promise; - return this - .findTrade({ - ...options, - where: { - ...options.where, - status, - }, - notFoundErrorMessage, - }) - .then((trade) => ({ - ...trade, - status: status as typeof status, - })); - } - - public async findPendingTradeById( + public abstract findPendingTradeById( options: FindEntityByIdOptions, - ): Promise { - const { - id, - notFoundErrorMessageFn = (id) => `Pending trade (\`${id}\`) not found`, - } = options; - - return this.findPendingTrade({ - where: { id }, - notFoundErrorMessage: notFoundErrorMessageFn(id), - }); - } + ): Promise; - public async createPendingTrade( + public abstract createPendingTrade( values: CreatePendingTradeEntityValues, - tx?: Transaction, + tx?: unknown, ): Promise<{ pendingTrade: PendingTradeEntity, tradesToSenderItems: Array, tradesToReceiverItems: Array, - }> { - const status = 'PENDING'; - const { sender, senderItems, receiver, receiverItems } = values; - - const pendingTrade = await (tx ?? this.db) - .insert(tradesTable) - .values({ - ...values, - status, - statusedAt: sql`now()`, - senderId: sender.id, - receiverId: receiver.id, - }) - .returning() - .then(([trade]) => ({ - ...trade!, - status: trade!.status as typeof status, - sender, - receiver, - })); - - const [tradesToSenderItems, tradesToReceiverItems] = await Promise.all([ - this.tradesToUserItemsRepository.createTradesToSenderItems( - senderItems.map((senderItem) => ({ - trade: pendingTrade, - senderItem, - })), - tx, - ), - this.tradesToUserItemsRepository.createTradesToReceiverItems( - receiverItems.map((receiverItem) => ({ - trade: pendingTrade, - receiverItem, - })), - tx, - ), - ]); - - return { - pendingTrade, - tradesToSenderItems, - tradesToReceiverItems, - }; - } + }>; - public async updatePendingTradeToCancelledTrade( + public abstract updatePendingTradeToCancelledTrade( pendingTrade: PendingTradeEntity, - tx?: Transaction, - ): Promise { - const status = 'CANCELLED'; - const { sender, receiver } = pendingTrade; + tx?: unknown, + ): Promise; - return (tx ?? this.db) - .update(tradesTable) - .set({ - status, - statusedAt: sql`now()`, - }) - .returning() - .then(([trade]) => ({ - ...trade!, - status: trade!.status as typeof status, - sender, - receiver, - })); - } - - public async updatePendingTradeToAcceptedTrade( + public abstract updatePendingTradeToAcceptedTrade( pendingTrade: PendingTradeEntity, - tx?: Transaction, - ): Promise { - const status = 'ACCEPTED'; - const { sender, receiver } = pendingTrade; - - return (tx ?? this.db) - .update(tradesTable) - .set({ - status, - statusedAt: sql`now()`, - }) - .returning() - .then(([trade]) => ({ - ...trade!, - status: trade!.status as typeof status, - sender, - receiver, - })); - } + tx?: unknown, + ): Promise; - public async updatePendingTradeToRejectedTrade( + public abstract updatePendingTradeToRejectedTrade( pendingTrade: PendingTradeEntity, - tx?: Transaction, - ): Promise { - const status = 'REJECTED'; - const { sender, receiver } = pendingTrade; - - return (tx ?? this.db) - .update(tradesTable) - .set({ - status, - statusedAt: sql`now()`, - }) - .returning() - .then(([trade]) => ({ - ...trade!, - status: trade!.status as typeof status, - sender, - receiver, - })); - } + tx?: unknown, + ): Promise; } + diff --git a/src/core/repositories/user-items.repository.ts b/src/core/repositories/user-items.repository.ts index 78fdf17..27bfeb1 100644 --- a/src/core/repositories/user-items.repository.ts +++ b/src/core/repositories/user-items.repository.ts @@ -1,12 +1,5 @@ -import { Injectable } from '@nestjs/common'; -import { Database, Transaction } from 'src/infra/postgres/types'; -import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; -import { Optional, PaginatedArray, UUIDv4 } from 'src/common/types'; -import { CreateUserItemEntityValues, pokemonsTable, UpdateUserItemEntityValues, UserEntity, UserItemEntity, userItemsTable, usersTable } from 'src/infra/postgres/tables'; -import { and, eq, inArray, like, SQL } from 'drizzle-orm'; -import { mapArrayToPaginatedArray } from 'src/common/helpers/map-array-to-paginated-array.helper'; -import { zip } from 'lodash'; -import { AppConflictException, AppEntityNotFoundException } from '../exceptions'; +import { PaginatedArray, UUIDv4 } from 'src/common/types'; +import { CreateUserItemEntityValues, UpdateUserItemEntityValues, UserEntity, UserItemEntity } from 'src/infra/postgres/tables'; import { FindEntitiesOptions, FindEntityOptions, @@ -26,225 +19,52 @@ export type FindUserItemsWhere = Partial<{ pokemonNameLike: string, }>; -export const mapUserItemsRowToEntity = ( - row: Record<'user_items' | 'users' | 'pokemons', any>, -): UserItemEntity => { - return { - ...row.user_items, - user: row.users, - pokemon: row.pokemons, - }; -}; +export abstract class IUserItemsRepository { + public abstract findUserItems( + findUserItemsOptions: FindEntitiesOptions + ): Promise>; -@Injectable() -export class UserItemsRepository { - public constructor( - @InjectDatabase() - private readonly db: Database, - ) {} - - private mapWhereToSQL( - where: FindUserItemsWhere, - ): Optional { - return and( - where.id !== undefined ? eq(userItemsTable.id, where.id) : undefined, - where.ids !== undefined ? inArray(userItemsTable.id, where.ids) : undefined, - - where.userId !== undefined ? eq(userItemsTable.userId, where.userId) : undefined, - where.userName !== undefined ? eq(usersTable.name, where.userName) : undefined, - where.userNameLike !== undefined ? like(usersTable.name, `%${where.userNameLike}%`) : undefined, - - where.pokemonId !== undefined ? eq(userItemsTable.pokemonId, where.pokemonId) : undefined, - where.pokemonName !== undefined ? eq(pokemonsTable.name, where.pokemonName) : undefined, - where.pokemonNameLike !== undefined ? like(pokemonsTable.name, `%${where.pokemonNameLike}%`) : undefined, - ); - }; - - private baseSelectBuilder( - options: FindEntitiesOptions, - ) { - const { where = {} } = options; - - return this.db - .select() - .from(userItemsTable) - .innerJoin(usersTable, eq(userItemsTable.userId, usersTable.id)) - .innerJoin(pokemonsTable, eq(userItemsTable.pokemonId, pokemonsTable.id)) - .where(this.mapWhereToSQL(where)); - } - - public async findUserItems(findUserItemsOptions: FindEntitiesOptions): Promise> { - return this - .baseSelectBuilder(findUserItemsOptions) - .then((rows) => rows.map((row) => mapUserItemsRowToEntity(row))); - } - - public async findUserItemsByIds( + public abstract findUserItemsByIds( options: FindEntitiesByIdsOptions, - ): Promise> { - const { - ids, - notFoundErrorMessageFn = (id) => `User item (\`${id}\`) not found`, - } = options; - if (!ids.length) return []; - - const userItems = await this.findUserItems({ - where: { ids }, - }); - - for (const id of ids) { - const userItem = userItems.some((userItem) => userItem.id === id); - - if (!userItem) { - throw new AppEntityNotFoundException(notFoundErrorMessageFn(id)); - } - } - - return userItems; - } + ): Promise>; - public async findUserItemsWithPagination( + public abstract findUserItemsWithPagination( options: FindEntitiesWithPaginationOptions, - ): Promise> { - const { - paginationOptions: { page, limit }, - } = options; - // TODO: check for boundaries - const offset = (page - 1) * limit; + ): Promise>; - return this - .baseSelectBuilder(options) - .offset(offset) - .limit(limit) - .then((rows) => mapArrayToPaginatedArray( - rows.map((row) => mapUserItemsRowToEntity(row)), - { page, limit }, - )); - } - - public async findUserItem( + public abstract findUserItem( options: FindEntityOptions, - ): Promise { - const { - notFoundErrorMessage = 'User item not found', - } = options; - - const userItem = await this - .baseSelectBuilder(options) - .limit(1) - .then(([row]) => ( - row ? mapUserItemsRowToEntity(row) : null - )); - - if (!userItem) { - throw new AppEntityNotFoundException(notFoundErrorMessage); - } + ): Promise; - return userItem; - } - - public async findUserItemById( + public abstract findUserItemById( options: FindEntityByIdOptions, - ): Promise { - const { - id, - notFoundErrorMessageFn = (id) => `User item (\`${id}\`) not found`, - } = options; - - return this.findUserItem({ - where: { id }, - notFoundErrorMessage: notFoundErrorMessageFn(id), - }); - } + ): Promise; - public async createUserItem( + public abstract createUserItem( values: CreateUserItemEntityValues, - tx?: Transaction, - ): Promise { - const { user, pokemon } = values; - - return (tx ?? this.db) - .insert(userItemsTable) - .values({ - ...values, - userId: user.id, - pokemonId: pokemon.id, - }) - .returning() - .then(([userItem]) => ({ - ...userItem!, - user, - pokemon, - })); - } + tx?: unknown, + ): Promise; - public async updateUserItems( + public abstract updateUserItems( userItems: Array, values: UpdateUserItemEntityValues, - tx?: Transaction, - ): Promise> { - if (!userItems.length) return []; + tx?: unknown, + ): Promise>; - const { user, pokemon, ...restValues } = values; - - return (tx ?? this.db) - .update(userItemsTable) - .set({ - ...restValues, - userId: user?.id, - pokemonId: pokemon?.id, - }) - .where(inArray(userItemsTable.id, userItems.map(({ id }) => id))) - .returning() - .then((updatedUserItems) => zip(userItems, updatedUserItems).map(([userItem, updatedUserItem]) => ({ - ...updatedUserItem!, - user: user ?? userItem!.user, - pokemon: pokemon ?? userItem!.pokemon, - }))); - } - - public async transferUserItemsToAnotherUser( + public abstract transferUserItemsToAnotherUser( fromUserItems: Array, toUser: UserEntity, - tx?: Transaction, - ): Promise> { - if (!fromUserItems.length) return []; - - const set = new Set(fromUserItems.map(({ userId }) => userId)); - if (set.size > 1) { - throw new AppConflictException('All of the items must have the same user'); - } - - const fromUserId = fromUserItems[0]!.userId; - if (fromUserId === toUser.id) { - throw new AppConflictException('You cannot transfer items to yourself'); - } - - return this.updateUserItems(fromUserItems, { user: toUser }, tx); - } + tx?: unknown, + ): Promise>; - public async updateUserItem( + public abstract updateUserItem( userItem: UserItemEntity, values: UpdateUserItemEntityValues, - tx?: Transaction, - ): Promise { - return this - .updateUserItems([userItem], values, tx) - .then(([userItem]) => userItem!); - } + tx?: unknown, + ): Promise; - public async deleteUserItem( + public abstract deleteUserItem( userItem: UserItemEntity, - tx?: Transaction, - ): Promise { - return (tx ?? this.db) - .delete(userItemsTable) - .where(eq(userItemsTable.id, userItem.id)) - .returning() - .then(([deletedUserItem]) => ({ - ...deletedUserItem!, - user: userItem.user, - pokemon: userItem.pokemon, - })) - } + tx?: unknown, + ): Promise; } diff --git a/src/core/repositories/user-refresh-tokens.repository.ts b/src/core/repositories/user-refresh-tokens.repository.ts index 989f081..a9ee6a6 100644 --- a/src/core/repositories/user-refresh-tokens.repository.ts +++ b/src/core/repositories/user-refresh-tokens.repository.ts @@ -1,117 +1,28 @@ -import { Injectable } from '@nestjs/common'; -import { SQL, and, eq, gt, sql } from 'drizzle-orm'; -import { JWT, Optional, UUIDv4 } from 'src/common/types'; -import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; -import { Database, Transaction } from 'src/infra/postgres/types'; -import { CreateUserRefreshTokenEntityValues, UserRefreshTokenEntity, userRefreshTokensTable, usersTable } from 'src/infra/postgres/tables'; -import { hashRefreshToken } from 'src/common/helpers/hash-refresh-token.helper'; -import { AppEntityNotFoundException } from '../exceptions'; -import { FindEntitiesOptions, FindEntityOptions } from '../types'; +import { JWT, UUIDv4 } from 'src/common/types'; +import { CreateUserRefreshTokenEntityValues, UserRefreshTokenEntity } from 'src/infra/postgres/tables'; +import { FindEntityOptions } from '../types'; export type FindUserRefreshTokensWhere = Partial<{ userId: UUIDv4, refreshToken: JWT, }>; -export const mapUserRefreshTokensRowToEntity = ( - row: Record<'user_refresh_tokens' | 'users', any>, -): UserRefreshTokenEntity => ({ - ...row.user_refresh_tokens, - user: row.users, -}); - -@Injectable() -export class UserRefreshTokensRepository { - public constructor( - @InjectDatabase() - private readonly db: Database, - ) {} - - private mapWhereToSQL( - where: FindUserRefreshTokensWhere, - ): Optional { - return and( - where.userId !== undefined ? eq(userRefreshTokensTable.userId, where.userId) : undefined, - where.refreshToken !== undefined - ? eq(userRefreshTokensTable.hashedRefreshToken, hashRefreshToken(where.refreshToken)) - : undefined, - ); - } - - private baseSelectBuilder( - options: FindEntitiesOptions, - ) { - const { where = {} } = options; - - return this.db - .select() - .from(userRefreshTokensTable) - .innerJoin(usersTable, eq(userRefreshTokensTable.userId, usersTable.id)) - .where(this.mapWhereToSQL(where)); - } - - public async findUserRefreshToken( +export abstract class IUserRefreshTokensRepository { + public abstract findUserRefreshToken( options: FindEntityOptions, - ): Promise { - const { - notFoundErrorMessage = 'User refresh token not found', - } = options; + ): Promise; - const userRefreshToken = await this - .baseSelectBuilder(options) - .limit(1) - .then(([row]) => ( - row ? mapUserRefreshTokensRowToEntity(row) : null - )); - - if (!userRefreshToken) { - throw new AppEntityNotFoundException(notFoundErrorMessage); - } - - return userRefreshToken; - } - - public async createUserRefreshToken( + public abstract createUserRefreshToken( values: CreateUserRefreshTokenEntityValues, - tx?: Transaction, - ): Promise { - const { user } = values; - - return (tx ?? this.db) - .insert(userRefreshTokensTable) - .values({ - ...values, - userId: user.id, - }) - .returning() - .then(([userRefreshToken]) => ({ - ...userRefreshToken!, - user, - })); - } + tx?: unknown, + ): Promise; - public async deleteUserRefreshToken( + public abstract deleteUserRefreshToken( userRefreshToken: UserRefreshTokenEntity, - tx?: Transaction, - ): Promise { - return (tx ?? this.db) - .delete(userRefreshTokensTable) - .where(and( - eq(userRefreshTokensTable.userId, userRefreshToken.userId), - eq(userRefreshTokensTable.hashedRefreshToken, userRefreshToken.hashedRefreshToken), - )) - .returning() - .then(([deletedUserRefreshToken]) => ({ - ...deletedUserRefreshToken!, - user: userRefreshToken.user, - })); - } + tx?: unknown, + ): Promise; - public async deleteExpiredUserRefreshTokens( - tx?: Transaction, - ): Promise { - await (tx ?? this.db) - .delete(userRefreshTokensTable) - .where(gt(sql`now()`, userRefreshTokensTable.expiresAt)); - } + public abstract deleteExpiredUserRefreshTokens( + tx?: unknown, + ): Promise; } diff --git a/src/core/repositories/users.repository.ts b/src/core/repositories/users.repository.ts index 8e2fcbe..8099f6a 100644 --- a/src/core/repositories/users.repository.ts +++ b/src/core/repositories/users.repository.ts @@ -1,11 +1,5 @@ -import { Injectable } from '@nestjs/common'; -import { Database, Transaction } from 'src/infra/postgres/types'; -import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; -import { Nullable, Optional, PaginatedArray, UUIDv4 } from 'src/common/types'; -import { CreateUserEntityValues, UpdateUserEntityValues, UserEntity, usersTable } from 'src/infra/postgres/tables'; -import { and, eq, inArray, like, SQL } from 'drizzle-orm'; -import { mapArrayToPaginatedArray } from 'src/common/helpers/map-array-to-paginated-array.helper'; -import { AppEntityNotFoundException } from '../exceptions'; +import { PaginatedArray, UUIDv4 } from 'src/common/types'; +import { CreateUserEntityValues, UpdateUserEntityValues, UserEntity } from 'src/infra/postgres/tables'; import { FindEntitiesOptions, FindEntitiesWithPaginationOptions, FindEntityByIdOptions, FindEntityOptions } from '../types'; export type FindUsersWhere = Partial<{ @@ -15,154 +9,47 @@ export type FindUsersWhere = Partial<{ nameLike: string, }>; -@Injectable() -export class UsersRepository { - public constructor( - @InjectDatabase() - private readonly db: Database, - ) {} - - private mapWhereToSQL( - where: FindUsersWhere, - ): Optional { - return and( - where.id !== undefined ? eq(usersTable.id, where.id) : undefined, - where.ids !== undefined ? inArray(usersTable.id, where.ids) : undefined, - where.name !== undefined ? eq(usersTable.name, where.name) : undefined, - where.nameLike !== undefined ? like(usersTable.name, `%${where.nameLike}%`) : undefined, - ); -} - - private baseSelectBuilder( +export abstract class IUsersRepository { + public abstract findUsers( options: FindEntitiesOptions, - ) { - const { where = {} } = options; + ): Promise>; - return this.db - .select() - .from(usersTable) - .where(this.mapWhereToSQL(where)); - } - - public async findUsers( - options: FindEntitiesOptions, - ): Promise> { - return this.baseSelectBuilder(options); - } - - public async findUsersWithPagination( + public abstract findUsersWithPagination( options: FindEntitiesWithPaginationOptions, - ): Promise> { - const { - paginationOptions: { page, limit }, - } = options; - // TODO: check for boundaries - const offset = (page - 1) * limit; - - // TODO: Pass these values to `mapArrayToPaginatedArray` - // const totalItems = await this.db - // .select({ - // totalItems: count(), - // }) - // .from(usersTable) - // .where(this.mapWhereToSQL(where)) - // .then(([row]) => row!.totalItems); - // const totalPages = Math.ceil(totalItems / offset); + ): Promise>; - return this - .baseSelectBuilder(options) - .offset(offset) - .limit(limit) - .then((users) => mapArrayToPaginatedArray(users, { page, limit })); - } - - public async findUser( + public abstract findUser( options: FindEntityOptions, - ): Promise { - const { - notFoundErrorMessage = 'User not found', - } = options; - - const user = await this - .baseSelectBuilder(options) - .limit(1) - .then(([user]) => user ?? null); - - if (!user) { - throw new AppEntityNotFoundException(notFoundErrorMessage); - } - - return user; - } + ): Promise; - public async userExists( + public abstract userExists( where: FindUsersWhere, - ): Promise { - let user: Nullable = null; - try { - user = await this.findUser({ where }); - } catch (error) { - if (error instanceof AppEntityNotFoundException) { - return false; - } + ): Promise; - throw error; - } - - return true; - } - - public async findUserById( + public abstract findUserById( options: FindEntityByIdOptions, - ): Promise { - const { - id, - notFoundErrorMessageFn = (id) => `User (\`${id}\`) not found`, - } = options; - - return this.findUser({ - where: { id }, - notFoundErrorMessage: notFoundErrorMessageFn(id), - }); - } + ): Promise; - public async createUser( + public abstract createUser( values: CreateUserEntityValues, - tx?: Transaction, - ): Promise { - return (tx ?? this.db) - .insert(usersTable) - .values(values) - .returning() - .then(([user]) => user!); - } + tx?: unknown, + ): Promise; - public async updateUser( + public abstract updateUser( user: UserEntity, values: UpdateUserEntityValues, - tx?: Transaction, - ): Promise { - return (tx ?? this.db) - .update(usersTable) - .set(values) - .where(eq(usersTable.id, user.id)) - .returning() - .then(([updatedUser]) => updatedUser!); - } + tx?: unknown, + ): Promise; - public async spendUserBalance( + public abstract spendUserBalance( user: UserEntity, amount: number, - tx?: Transaction, - ): Promise { - return this.updateUser(user, { balance: user.balance - amount }, tx); - } + tx?: unknown, + ): Promise; - public async replenishUserBalance( + public abstract replenishUserBalance( user: UserEntity, amount: number, - tx?: Transaction, - ): Promise { - return this.updateUser(user, { balance: user.balance + amount }, tx); - } + tx?: unknown, + ): Promise; } diff --git a/src/core/services/auth.service.ts b/src/core/services/auth.service.ts index 2202c84..e2a65a3 100644 --- a/src/core/services/auth.service.ts +++ b/src/core/services/auth.service.ts @@ -3,7 +3,7 @@ import { JwtService } from '@nestjs/jwt'; import { RegisterUserInputDTO } from 'src/api/dtos/auth/register-user.input.dto'; import { JWT } from 'src/common/types'; import { UserEntity } from 'src/infra/postgres/tables'; -import { UserRefreshTokensRepository } from '../repositories/user-refresh-tokens.repository'; +import { IUserRefreshTokensRepository } from '../repositories/user-refresh-tokens.repository'; import ms from 'ms'; import { addMilliseconds } from 'date-fns'; import { ConfigService } from '@nestjs/config'; @@ -11,7 +11,7 @@ import { EnvVariables } from 'src/infra/config/env.config'; import { hashUserPassword } from 'src/common/helpers/hash-user-password.helper'; import { hashRefreshToken } from 'src/common/helpers/hash-refresh-token.helper'; import { DatabaseError } from 'pg'; -import { UsersRepository } from '../repositories/users.repository'; +import { IUsersRepository } from '../repositories/users.repository'; import { AppConflictException, AppValidationException } from '../exceptions'; type AuthTokens = { accessToken: JWT, refreshToken: JWT }; @@ -21,8 +21,8 @@ export class AuthService { public constructor( private readonly jwtService: JwtService, private readonly configService: ConfigService, - private readonly usersRepository: UsersRepository, - private readonly userRefreshTokensRepository: UserRefreshTokensRepository, + private readonly usersRepository: IUsersRepository, + private readonly userRefreshTokensRepository: IUserRefreshTokensRepository, ) {} private async generateAccessToken(user: UserEntity): Promise { diff --git a/src/core/services/packs.service.ts b/src/core/services/packs.service.ts index 4e6308a..d0e6d75 100644 --- a/src/core/services/packs.service.ts +++ b/src/core/services/packs.service.ts @@ -1,23 +1,23 @@ import { Injectable } from '@nestjs/common'; import { PaginatedArray, UUIDv4 } from 'src/common/types'; -import { PacksRepository } from '../repositories/packs.repository'; +import { IPacksRepository } from '../repositories/packs.repository'; import { Database, Transaction } from 'src/infra/postgres/types'; import { OpenedPackEntity, PackEntity, UserEntity } from 'src/infra/postgres/tables'; import { GetPacksInputDTO } from 'src/api/dtos/packs/get-packs.input.dto'; import { PaginationInputDTO } from 'src/api/dtos/pagination.input.dto'; -import { OpenedPacksRepository } from '../repositories/opened-packs.repository'; -import { UsersRepository } from '../repositories/users.repository'; -import { UserItemsRepository } from '../repositories/user-items.repository'; +import { IUsersRepository } from '../repositories/users.repository'; +import { IUserItemsRepository } from '../repositories/user-items.repository'; import { AppConflictException } from '../exceptions'; import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; +import { IOpenedPacksRepository } from '../repositories/opened-packs.repository'; @Injectable() export class PacksService { public constructor( - private readonly packsRepository: PacksRepository, - private readonly openedPacksRepository: OpenedPacksRepository, - private readonly usersRepository: UsersRepository, - private readonly userItemsRepository: UserItemsRepository, + private readonly packsRepository: IPacksRepository, + private readonly openedPacksRepository: IOpenedPacksRepository, + private readonly usersRepository: IUsersRepository, + private readonly userItemsRepository: IUserItemsRepository, @InjectDatabase() private readonly db: Database, diff --git a/src/core/services/pending-trades.service.ts b/src/core/services/pending-trades.service.ts index d1e3b61..3c22bbb 100644 --- a/src/core/services/pending-trades.service.ts +++ b/src/core/services/pending-trades.service.ts @@ -3,22 +3,22 @@ import { CreatePendingTradeInputDTO } from 'src/api/dtos/pending-trades/create-p import { UUIDv4 } from 'src/common/types'; import { Database, Transaction } from 'src/infra/postgres/types'; import { AcceptedTradeEntity, CancelledTradeEntity, PendingTradeEntity, RejectedTradeEntity, UserEntity } from 'src/infra/postgres/tables'; -import { TradesRepository } from '../repositories/trades.repository'; -import { TradesToUserItemsRepository } from '../repositories/trades-to-user-items.repository'; +import { ITradesRepository } from '../repositories/trades.repository'; +import { ITradesToUserItemsRepository } from '../repositories/trades-to-user-items.repository'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { PENDING_TRADE_ACCEPTED_EVENT, PENDING_TRADE_CREATED_EVENT } from '../events'; -import { UsersRepository } from '../repositories/users.repository'; -import { UserItemsRepository } from '../repositories/user-items.repository'; +import { IUsersRepository } from '../repositories/users.repository'; +import { IUserItemsRepository } from '../repositories/user-items.repository'; import { AppConflictException, AppValidationException } from '../exceptions'; import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; @Injectable() export class PendingTradesService { public constructor( - private readonly tradesRepository: TradesRepository, - private readonly tradesToUserItemsRepository: TradesToUserItemsRepository, - private readonly userItemsRepository: UserItemsRepository, - private readonly usersRepository: UsersRepository, + private readonly tradesRepository: ITradesRepository, + private readonly tradesToUserItemsRepository: ITradesToUserItemsRepository, + private readonly userItemsRepository: IUserItemsRepository, + private readonly usersRepository: IUsersRepository, private readonly eventEmitter: EventEmitter2, @InjectDatabase() private readonly db: Database, diff --git a/src/core/services/pokemons.service.ts b/src/core/services/pokemons.service.ts index fadc6db..c1ddbbc 100644 --- a/src/core/services/pokemons.service.ts +++ b/src/core/services/pokemons.service.ts @@ -1,9 +1,9 @@ import { Injectable } from "@nestjs/common"; -import { PokemonsRepository } from "../repositories/pokemons.repository"; +import { IPokemonsRepository } from "../repositories/pokemons.repository"; @Injectable() export class PokemonsService { public constructor( - private readonly pokemonsRepository: PokemonsRepository, + private readonly pokemonsRepository: IPokemonsRepository, ) {} } diff --git a/src/core/services/trades.service.ts b/src/core/services/trades.service.ts index 1ef9f5c..4ddda08 100644 --- a/src/core/services/trades.service.ts +++ b/src/core/services/trades.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { TradesRepository } from '../repositories/trades.repository'; +import { ITradesRepository } from '../repositories/trades.repository'; @Injectable() export class TradesService { public constructor( - private readonly tradesRepository: TradesRepository, + private readonly tradesRepository: ITradesRepository, ) {} } diff --git a/src/core/services/user-items.service.ts b/src/core/services/user-items.service.ts index aacd95a..23ee18b 100644 --- a/src/core/services/user-items.service.ts +++ b/src/core/services/user-items.service.ts @@ -3,18 +3,18 @@ import { PaginationInputDTO } from 'src/api/dtos/pagination.input.dto'; import { PaginatedArray, UUIDv4 } from 'src/common/types'; import { Database, Transaction } from 'src/infra/postgres/types'; import { QuickSoldUserItemEntity, UserEntity, UserItemEntity } from 'src/infra/postgres/tables'; -import { QuickSoldUserItemsRepository } from '../repositories/quick-sold-user-items.repository'; -import { UserItemsRepository } from '../repositories/user-items.repository'; -import { UsersRepository } from '../repositories/users.repository'; +import { IQuickSoldUserItemsRepository } from '../repositories/quick-sold-user-items.repository'; +import { IUserItemsRepository } from '../repositories/user-items.repository'; +import { IUsersRepository } from '../repositories/users.repository'; import { AppConflictException } from '../exceptions'; import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; @Injectable() export class UserItemsService { public constructor( - private readonly userItemsRepository: UserItemsRepository, - private readonly quickSoldUserItemsRepository: QuickSoldUserItemsRepository, - private readonly usersRepository: UsersRepository, + private readonly userItemsRepository: IUserItemsRepository, + private readonly quickSoldUserItemsRepository: IQuickSoldUserItemsRepository, + private readonly usersRepository: IUsersRepository, @InjectDatabase() private readonly db: Database, diff --git a/src/core/services/users.service.ts b/src/core/services/users.service.ts index ff025d4..0ed258f 100644 --- a/src/core/services/users.service.ts +++ b/src/core/services/users.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { UsersRepository } from '../repositories/users.repository'; +import { IUsersRepository } from '../repositories/users.repository'; import { PaginatedArray } from 'src/common/types'; import { GetUsersInputDTO } from 'src/api/dtos/users/get-users.input.dto'; import { UserEntity } from 'src/infra/postgres/tables'; @@ -8,7 +8,7 @@ import { PaginationInputDTO } from 'src/api/dtos/pagination.input.dto'; @Injectable() export class UsersService { public constructor( - private readonly usersRepository: UsersRepository, + private readonly usersRepository: IUsersRepository, ) {} public getUsersWithPagination( diff --git a/src/infra/cron-jobs/cron-jobs.service.ts b/src/infra/cron-jobs/cron-jobs.service.ts index 97ea10c..dc17c76 100644 --- a/src/infra/cron-jobs/cron-jobs.service.ts +++ b/src/infra/cron-jobs/cron-jobs.service.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; -import { UserRefreshTokensRepository } from 'src/core/repositories/user-refresh-tokens.repository'; +import { IUserRefreshTokensRepository } from 'src/core/repositories/user-refresh-tokens.repository'; @Injectable() export class CronJobsService { public constructor( - private readonly userRefreshTokensRepository: UserRefreshTokensRepository, + private readonly userRefreshTokensRepository: IUserRefreshTokensRepository, ) {} @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) diff --git a/src/infra/ioc/core-modules/packs.module.ts b/src/infra/ioc/core-modules/packs.module.ts index b8334d2..93926b5 100644 --- a/src/infra/ioc/core-modules/packs.module.ts +++ b/src/infra/ioc/core-modules/packs.module.ts @@ -3,9 +3,11 @@ import { PostgresModule } from '../../postgres/postgres.module'; import { PokemonsModule } from './pokemons.module'; import { PacksService } from 'src/core/services/packs.service'; import { UsersModule } from './users.module'; -import { PacksRepository } from 'src/core/repositories/packs.repository'; -import { OpenedPacksRepository } from 'src/core/repositories/opened-packs.repository'; +import { IPacksRepository } from 'src/core/repositories/packs.repository'; +import { IOpenedPacksRepository } from 'src/core/repositories/opened-packs.repository'; import { UserItemsModule } from './user-items.module'; +import { OpenedPacksRepository } from 'src/infra/postgres/repositories/opened-pack.repository'; +import { PacksRepository } from 'src/infra/postgres/repositories/packs.repository'; @Module({ imports: [ @@ -14,7 +16,17 @@ import { UserItemsModule } from './user-items.module'; UsersModule, UserItemsModule, ], - providers: [PacksService, PacksRepository, OpenedPacksRepository], - exports: [PacksService, PacksRepository, OpenedPacksRepository], + providers: [ + PacksService, + { + provide: IPacksRepository, + useClass: PacksRepository, + }, + { + provide: IOpenedPacksRepository, + useClass: OpenedPacksRepository, + }, + ], + exports: [PacksService, IPacksRepository, IOpenedPacksRepository], }) export class PacksModule {} diff --git a/src/infra/ioc/core-modules/pokemons.module.ts b/src/infra/ioc/core-modules/pokemons.module.ts index b46c5ef..ef86029 100644 --- a/src/infra/ioc/core-modules/pokemons.module.ts +++ b/src/infra/ioc/core-modules/pokemons.module.ts @@ -1,11 +1,18 @@ import { Module } from '@nestjs/common'; import { PostgresModule } from '../../postgres/postgres.module'; import { PokemonsService } from 'src/core/services/pokemons.service'; -import { PokemonsRepository } from 'src/core/repositories/pokemons.repository'; +import { IPokemonsRepository } from 'src/core/repositories/pokemons.repository'; +import { PokemonsRepository } from 'src/infra/postgres/repositories/pokemons.repository'; @Module({ imports: [PostgresModule], - providers: [PokemonsService, PokemonsRepository], - exports: [PokemonsService, PokemonsRepository], + providers: [ + PokemonsService, + { + provide: IPokemonsRepository, + useClass: PokemonsRepository, + }, + ], + exports: [PokemonsService, IPokemonsRepository], }) export class PokemonsModule {} diff --git a/src/infra/ioc/core-modules/trades.module.ts b/src/infra/ioc/core-modules/trades.module.ts index e0136d1..4ae730d 100644 --- a/src/infra/ioc/core-modules/trades.module.ts +++ b/src/infra/ioc/core-modules/trades.module.ts @@ -1,11 +1,13 @@ import { Module } from '@nestjs/common'; -import { TradesRepository } from 'src/core/repositories/trades.repository'; +import { ITradesRepository } from 'src/core/repositories/trades.repository'; import { TradesService } from 'src/core/services/trades.service'; import { PostgresModule } from '../../postgres/postgres.module'; import { UsersModule } from './users.module'; import { UserItemsModule } from './user-items.module'; import { PendingTradesService } from 'src/core/services/pending-trades.service'; -import { TradesToUserItemsRepository } from 'src/core/repositories/trades-to-user-items.repository'; +import { ITradesToUserItemsRepository } from 'src/core/repositories/trades-to-user-items.repository'; +import { TradesRepository } from 'src/infra/postgres/repositories/trades.repository'; +import { TradesToUserItemsRepository } from 'src/infra/postgres/repositories/trades-to-user-items.repository'; @Module({ imports: [ @@ -15,15 +17,21 @@ import { TradesToUserItemsRepository } from 'src/core/repositories/trades-to-use ], providers: [ TradesService, - TradesRepository, - TradesToUserItemsRepository, PendingTradesService, + { + provide: ITradesRepository, + useClass: TradesRepository, + }, + { + provide: ITradesToUserItemsRepository, + useClass: TradesToUserItemsRepository, + }, ], exports: [ TradesService, - TradesRepository, - TradesToUserItemsRepository, PendingTradesService, + ITradesRepository, + ITradesToUserItemsRepository, ], }) export class TradesModule {} diff --git a/src/infra/ioc/core-modules/user-items.module.ts b/src/infra/ioc/core-modules/user-items.module.ts index 3189005..ae1d036 100644 --- a/src/infra/ioc/core-modules/user-items.module.ts +++ b/src/infra/ioc/core-modules/user-items.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; -import { QuickSoldUserItemsRepository } from 'src/core/repositories/quick-sold-user-items.repository'; -import { UserItemsRepository } from 'src/core/repositories/user-items.repository'; +import { IQuickSoldUserItemsRepository } from 'src/core/repositories/quick-sold-user-items.repository'; +import { IUserItemsRepository } from 'src/core/repositories/user-items.repository'; import { UserItemsService } from 'src/core/services/user-items.service'; import { PostgresModule } from '../../postgres/postgres.module'; import { UsersModule } from './users.module'; +import { UserItemsRepository } from 'src/infra/postgres/repositories/user-items.repository'; +import { QuickSoldUserItemsRepository } from 'src/infra/postgres/repositories/quick-sold-user-items.repository'; @Module({ imports: [ @@ -12,13 +14,19 @@ import { UsersModule } from './users.module'; ], providers: [ UserItemsService, - UserItemsRepository, - QuickSoldUserItemsRepository, + { + provide: IUserItemsRepository, + useClass: UserItemsRepository, + }, + { + provide: IQuickSoldUserItemsRepository, + useClass: QuickSoldUserItemsRepository, + }, ], exports: [ UserItemsService, - UserItemsRepository, - QuickSoldUserItemsRepository, + IUserItemsRepository, + IQuickSoldUserItemsRepository, ], }) export class UserItemsModule {} diff --git a/src/infra/ioc/core-modules/user-refresh-tokens.module.ts b/src/infra/ioc/core-modules/user-refresh-tokens.module.ts index be57930..34e461d 100644 --- a/src/infra/ioc/core-modules/user-refresh-tokens.module.ts +++ b/src/infra/ioc/core-modules/user-refresh-tokens.module.ts @@ -1,12 +1,16 @@ import { Module } from '@nestjs/common'; -import { UserRefreshTokensRepository } from 'src/core/repositories/user-refresh-tokens.repository'; +import { IUserRefreshTokensRepository } from 'src/core/repositories/user-refresh-tokens.repository'; import { PostgresModule } from '../../postgres/postgres.module'; +import { UserRefreshTokensRepository } from 'src/infra/postgres/repositories/user-refresh-tokens.repository'; @Module({ providers: [ PostgresModule, - UserRefreshTokensRepository, + { + provide: IUserRefreshTokensRepository, + useClass: UserRefreshTokensRepository, + } ], - exports: [UserRefreshTokensRepository], + exports: [IUserRefreshTokensRepository], }) export class UserRefreshTokensModule {} diff --git a/src/infra/ioc/core-modules/users.module.ts b/src/infra/ioc/core-modules/users.module.ts index a98fa72..624eba8 100644 --- a/src/infra/ioc/core-modules/users.module.ts +++ b/src/infra/ioc/core-modules/users.module.ts @@ -1,11 +1,18 @@ import { Module } from '@nestjs/common'; import { PostgresModule } from '../../postgres/postgres.module'; -import { UsersRepository } from 'src/core/repositories/users.repository'; +import { IUsersRepository } from 'src/core/repositories/users.repository'; import { UsersService } from 'src/core/services/users.service'; +import { UsersRepository } from 'src/infra/postgres/repositories/users.repository'; @Module({ imports: [PostgresModule], - providers: [UsersService, UsersRepository], - exports: [UsersService, UsersRepository], + providers: [ + UsersService, + { + provide: IUsersRepository, + useClass: UsersRepository, + }, + ], + exports: [UsersService, IUsersRepository], }) export class UsersModule {} diff --git a/src/infra/postgres/repositories/opened-pack.repository.ts b/src/infra/postgres/repositories/opened-pack.repository.ts new file mode 100644 index 0000000..2b62e89 --- /dev/null +++ b/src/infra/postgres/repositories/opened-pack.repository.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { Database, Transaction } from 'src/infra/postgres/types'; +import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; +import { CreateOpenedPackEntityValues, OpenedPackEntity, openedPacksTable } from 'src/infra/postgres/tables'; +import { IOpenedPacksRepository } from 'src/core/repositories/opened-packs.repository'; + +@Injectable() +export class OpenedPacksRepository implements IOpenedPacksRepository { + public constructor( + @InjectDatabase() + private readonly db: Database, + ) {} + + public async createOpenedPack( + values: CreateOpenedPackEntityValues, + tx?: Transaction, + ): Promise { + const { user, pack, pokemon } = values; + + return (tx ?? this.db) + .insert(openedPacksTable) + .values({ + ...values, + userId: user.id, + packId: pack.id, + pokemonId: pokemon.id, + }) + .returning() + .then(([openedPack]) => ({ + ...openedPack!, + user, + pack, + pokemon, + })); + } +} diff --git a/src/infra/postgres/repositories/packs.repository.ts b/src/infra/postgres/repositories/packs.repository.ts new file mode 100644 index 0000000..61bffa3 --- /dev/null +++ b/src/infra/postgres/repositories/packs.repository.ts @@ -0,0 +1,111 @@ +import { Injectable } from '@nestjs/common'; +import { Optional, PaginatedArray } from 'src/common/types'; +import { PackEntity, packsTable, packsToPokemonsTable, PokemonEntity, pokemonsTable } from 'src/infra/postgres/tables'; +import { and, eq, getTableColumns, inArray, like, SQL, sql } from 'drizzle-orm'; +import { Database } from 'src/infra/postgres/types'; +import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; +import { mapArrayToPaginatedArray } from 'src/common/helpers/map-array-to-paginated-array.helper'; +import { AppEntityNotFoundException, AppInternalException } from 'src/core/exceptions'; +import { FindEntitiesOptions, FindEntitiesWithPaginationOptions, FindEntityByIdOptions, FindEntityOptions } from 'src/core/types'; +import { FindPacksWhere, IPacksRepository } from 'src/core/repositories/packs.repository'; + +@Injectable() +export class PacksRepository implements IPacksRepository { + public constructor( + @InjectDatabase() + private readonly db: Database, + ) {} + + private mapWhereToSQL( + where: FindPacksWhere, + ): Optional { + return and( + where.id !== undefined ? eq(packsTable.id, where.id) : undefined, + where.ids !== undefined ? inArray(packsTable.id, where.ids) : undefined, + where.name !== undefined ? eq(packsTable.name, where.name) : undefined, + where.nameLike !== undefined ? like(packsTable.name, `%${where.nameLike}%`) : undefined, + ); + } + + private baseSelectBuilder( + options: FindEntitiesOptions, + ) { + const { where = {} } = options; + + return this.db + .select() + .from(packsTable) + .where(this.mapWhereToSQL(where)); + } + + public async findPacksWithPagination( + options: FindEntitiesWithPaginationOptions, + ): Promise> { + const { + paginationOptions: { page, limit }, + } = options; + // TODO: check for boundaries + const offset = (page - 1) * limit; + + return this + .baseSelectBuilder(options) + .offset(offset) + .limit(limit) + .then((packs) => mapArrayToPaginatedArray(packs, { page, limit })) + } + + public async findPack( + options: FindEntityOptions, + ): Promise { + const { + notFoundErrorMessage = 'Pack not found', + } = options; + + const pack = await this + .baseSelectBuilder(options) + .limit(1) + .then(([pack]) => pack ?? null); + + if (!pack) { + throw new AppEntityNotFoundException(notFoundErrorMessage); + } + + return pack; + } + + public async findPackById( + options: FindEntityByIdOptions, + ): Promise { + const { + id, + notFoundErrorMessageFn = (id) => `Pack (\`${id}\`) not found`, + } = options; + + return this.findPack({ + where: { id }, + notFoundErrorMessage: notFoundErrorMessageFn(id), + }) + } + + public async findRandomPokemonFromPack( + pack: PackEntity + ): Promise { + const pokemon = await this.db + .select({ pokemon: getTableColumns(pokemonsTable) }) + .from(packsTable) + .innerJoin(packsToPokemonsTable, eq(packsToPokemonsTable.packId, packsTable.id)) + .innerJoin(pokemonsTable, eq(pokemonsTable.id, packsToPokemonsTable.pokemonId)) + .where(eq(packsTable.id, pack.id)) + .orderBy(sql`random()`) + .limit(1) + .then(([row]) => row?.pokemon ?? null); + + if (!pokemon) { + throw new AppInternalException( + 'There are no pokemons in the pack. Please notify the developer about it :)', + ); + } + + return pokemon; + } +} diff --git a/src/infra/postgres/repositories/pokemons.repository.ts b/src/infra/postgres/repositories/pokemons.repository.ts new file mode 100644 index 0000000..2f6945f --- /dev/null +++ b/src/infra/postgres/repositories/pokemons.repository.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { Database, Transaction } from 'src/infra/postgres/types'; +import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; +import { CreatePokemonEntityValues, PokemonEntity, pokemonsTable } from 'src/infra/postgres/tables'; +import { IPokemonsRepository } from 'src/core/repositories/pokemons.repository'; + +@Injectable() +export class PokemonsRepository implements IPokemonsRepository { + public constructor( + @InjectDatabase() + private readonly db: Database, + ) {} + + public async createPokemons( + values: Array, + tx?: Transaction, + ): Promise> { + if (!values.length) return []; + + return (tx ?? this.db) + .insert(pokemonsTable) + .values(values) + .returning() + } + + public async deleteAllPokemons( + tx?: Transaction, + ): Promise { + await (tx ?? this.db) + .delete(pokemonsTable) + .returning(); + } +} diff --git a/src/infra/postgres/repositories/quick-sold-user-items.repository.ts b/src/infra/postgres/repositories/quick-sold-user-items.repository.ts new file mode 100644 index 0000000..d926475 --- /dev/null +++ b/src/infra/postgres/repositories/quick-sold-user-items.repository.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { Database, Transaction } from 'src/infra/postgres/types'; +import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; +import { QuickSoldUserItemEntity, quickSoldUserItemsTable, UserItemEntity } from 'src/infra/postgres/tables'; +import { IUserItemsRepository } from 'src/core/repositories/user-items.repository'; +import { IQuickSoldUserItemsRepository } from 'src/core/repositories/quick-sold-user-items.repository'; + +@Injectable() +export class QuickSoldUserItemsRepository implements IQuickSoldUserItemsRepository { + public constructor( + @InjectDatabase() + private readonly db: Database, + + private readonly userItemsRepository: IUserItemsRepository, + ) {} + + public async createQuickSoldUserItem( + userItem: UserItemEntity, + tx?: Transaction, + ): Promise { + const { user, pokemon } = userItem; + + const [quickSoldUserItem] = await Promise.all([ + (tx ?? this.db) + .insert(quickSoldUserItemsTable) + .values(userItem) + .returning() + .then(([quickSoldUserItem]) => ({ + ...quickSoldUserItem!, + user, + pokemon, + })), + this.userItemsRepository.deleteUserItem(userItem, tx), + ]); + + return quickSoldUserItem; + } +} diff --git a/src/infra/postgres/repositories/trades-to-user-items.repository.ts b/src/infra/postgres/repositories/trades-to-user-items.repository.ts new file mode 100644 index 0000000..6a9c824 --- /dev/null +++ b/src/infra/postgres/repositories/trades-to-user-items.repository.ts @@ -0,0 +1,194 @@ +import { Injectable } from '@nestjs/common'; +import { and, eq, SQL } from 'drizzle-orm'; +import { alias } from 'drizzle-orm/pg-core'; +import { zip } from 'lodash'; +import { Optional } from 'src/common/types'; +import { FindEntitiesOptions } from 'src/core/types'; +import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; +import { Database, Transaction } from 'src/infra/postgres/types'; +import { + CreateTradeToReceiverItemEntityValues, + CreateTradeToSenderItemEntityValues, + CreateTradeToUserItemEntityValues, + pokemonsTable, + tradesTable, + tradesToUserItemsTable, + TradeToReceiverItemEntity, + TradeToSenderItemEntity, + TradeToUserItemEntity, + userItemsTable, + usersTable, +} from 'src/infra/postgres/tables'; +import { mapTradesRowToEntity } from './trades.repository'; +import { mapUserItemsRowToEntity } from './user-items.repository'; +import { FindTradesToReceiverItemsWhere, FindTradesToSenderItemsWhere, FindTradesToUserItemsWhere, ITradesToUserItemsRepository } from 'src/core/repositories/trades-to-user-items.repository'; + +export const mapTradesToUserItemsRowToEntity = ( + row: Record< + | 'trades_to_user_items' + | 'trades' + | 'senders' + | 'receivers' + | 'user_items' + | 'users' + | 'pokemons', + any>, +): TradeToUserItemEntity => { + return { + ...row.trades_to_user_items, + trade: mapTradesRowToEntity(row), + userItem: mapUserItemsRowToEntity(row), + } +} + + +@Injectable() +export class TradesToUserItemsRepository implements ITradesToUserItemsRepository { + public constructor( + @InjectDatabase() + private readonly db: Database, + ) {} + + private mapWhereToSQL( + where: FindTradesToUserItemsWhere, + ): Optional { + return and( + where.tradeId !== undefined ? eq(tradesToUserItemsTable.tradeId, where.tradeId) : undefined, + where.userType !== undefined ? eq(tradesToUserItemsTable.userType, where.userType) : undefined, + where.userItemId !== undefined ? eq(tradesToUserItemsTable.userItemId, where.userItemId) : undefined, + ); + }; + + private baseSelectBuilder( + options: FindEntitiesOptions, + ) { + const { where = {} } = options; + + const sendersTable = alias(usersTable, 'senders'); + const receiversTable = alias(usersTable, 'receivers'); + + return this.db + .select() + .from(tradesToUserItemsTable) + .innerJoin(tradesTable, eq(tradesTable.id, tradesToUserItemsTable.tradeId)) + .innerJoin(sendersTable, eq(sendersTable.id, tradesTable.senderId)) + .innerJoin(receiversTable, eq(receiversTable.id, tradesTable.receiverId)) + .innerJoin(userItemsTable, eq(userItemsTable.id, tradesToUserItemsTable.userItemId)) + .innerJoin(usersTable, eq(usersTable.id, userItemsTable.userId)) + .innerJoin(pokemonsTable, eq(pokemonsTable.id, userItemsTable.pokemonId)) + .where(this.mapWhereToSQL(where)); + } + + public async findTradesToUserItems( + options: FindEntitiesOptions, + ): Promise> { + return this + .baseSelectBuilder(options) + .then((rows) => rows.map((row) => mapTradesToUserItemsRowToEntity(row))); + } + + public async findTradesToSenderItems( + options: FindEntitiesOptions, + ): Promise> { + const userType = 'SENDER'; + + return this + .findTradesToUserItems({ + ...options, + where: { + ...options.where, + userType, + } + }) + .then((tradesToUserItems) => tradesToUserItems.map((tradeToUserItem) => ({ + ...tradeToUserItem, + userType, + senderItem: tradeToUserItem.userItem, + }))); + } + + public async findTradesToReceiverItems( + options: FindEntitiesOptions, + ): Promise> { + const userType = 'RECEIVER'; + + return this + .findTradesToUserItems({ + ...options, + where: { + ...options.where, + userType, + } + }) + .then((tradesToUserItems) => tradesToUserItems.map((tradeToUserItem) => ({ + ...tradeToUserItem, + userType, + receiverItem: tradeToUserItem.userItem, + }))); + } + + private async createTradesToUserItems( + valuesArray: Array, + tx?: Transaction, + ): Promise> { + if (!valuesArray.length) return []; + + return (tx ?? this.db) + .insert(tradesToUserItemsTable) + .values(valuesArray.map((values) => ({ + ...values, + tradeId: values.trade.id, + userItemId: values.userItem.id, + }))) + .returning() + .then((tradesToUserItems) => zip(valuesArray, tradesToUserItems).map(([values, tradeToUserItem]) => ({ + ...tradeToUserItem!, + trade: values!.trade, + userItem: values!.userItem, + }))); + } + + public async createTradesToSenderItems( + valuesArray: Array, + tx?: Transaction, + ): Promise> { + const userType = 'SENDER'; + + return this + .createTradesToUserItems( + valuesArray.map((values) => ({ + ...values, + userType, + userItem: values.senderItem, + })), + tx, + ) + .then((tradesToUserItems) => tradesToUserItems.map((tradesToUserItem) => ({ + ...tradesToUserItem, + userType, + senderItem: tradesToUserItem.userItem, + }))); + } + + public async createTradesToReceiverItems( + valuesArray: Array, + tx?: Transaction, + ): Promise> { + const userType = 'RECEIVER'; + + return this + .createTradesToUserItems( + valuesArray.map((values) => ({ + ...values, + userType, + userItem: values.receiverItem, + })), + tx, + ) + .then((tradesToUserItems) => tradesToUserItems.map((tradesToUserItem) => ({ + ...tradesToUserItem, + userType, + receiverItem: tradesToUserItem.userItem, + }))); + } +} diff --git a/src/infra/postgres/repositories/trades.repository.ts b/src/infra/postgres/repositories/trades.repository.ts new file mode 100644 index 0000000..992c162 --- /dev/null +++ b/src/infra/postgres/repositories/trades.repository.ts @@ -0,0 +1,246 @@ +import { Injectable } from '@nestjs/common'; +import { Database, Transaction } from 'src/infra/postgres/types'; +import { + tradesTable, + usersTable, + TradeEntity, + PendingTradeEntity, + CreatePendingTradeEntityValues, + TradeToSenderItemEntity, + TradeToReceiverItemEntity, + CancelledTradeEntity, + AcceptedTradeEntity, + RejectedTradeEntity, +} from 'src/infra/postgres/tables'; +import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; +import { and, eq, inArray, sql, SQL } from 'drizzle-orm'; +import { Optional } from 'src/common/types'; +import { alias } from 'drizzle-orm/pg-core'; +import { AppEntityNotFoundException } from 'src/core/exceptions'; +import { FindEntitiesOptions, FindEntityByIdOptions, FindEntityOptions } from 'src/core/types'; +import { FindPendingTradesWhere, FindTradesWhere, ITradesRepository } from 'src/core/repositories/trades.repository'; +import { ITradesToUserItemsRepository } from 'src/core/repositories/trades-to-user-items.repository'; + +export const mapTradesRowToEntity = ( + row: Record<'trades' | 'senders' | 'receivers', any>, +): TradeEntity => { + return { + ...row.trades, + sender: row.senders, + receiver: row.receivers, + }; +}; + +@Injectable() +export class TradesRepository implements ITradesRepository { + public constructor( + @InjectDatabase() + private readonly db: Database, + + private readonly tradesToUserItemsRepository: ITradesToUserItemsRepository, + ) {} + + private mapWhereToSQL( + where: FindTradesWhere, + ): Optional { + return and( + where.id !== undefined ? eq(tradesTable.id, where.id) : undefined, + where.ids !== undefined ? inArray(tradesTable.id, where.ids) : undefined, + where.status !== undefined ? eq(tradesTable.status, where.status) : undefined, + ); + } + + private baseSelectBuilder( + options: FindEntitiesOptions, + ) { + const { where = {} } = options; + + const sendersTable = alias(usersTable, 'senders'); + const receiversTable = alias(usersTable, 'receivers'); + + return this.db + .select() + .from(tradesTable) + .innerJoin(sendersTable, eq(sendersTable.id, tradesTable.senderId)) + .innerJoin(receiversTable, eq(receiversTable.id, tradesTable.receiverId)) + .where(this.mapWhereToSQL(where)); + } + + public async findTrade( + options: FindEntityOptions, + ): Promise { + const { + notFoundErrorMessage = 'Trade not found', + } = options; + + const trade = await this + .baseSelectBuilder(options) + .limit(1) + .then(([row]) => ( + row + ? mapTradesRowToEntity(row) + : null + )); + + if (!trade) { + throw new AppEntityNotFoundException(notFoundErrorMessage); + } + + return trade; + } + + public async findPendingTrade( + options: FindEntityOptions, + ): Promise { + const { + notFoundErrorMessage = 'Pending trade not found', + } = options; + const status = 'PENDING'; + + return this + .findTrade({ + ...options, + where: { + ...options.where, + status, + }, + notFoundErrorMessage, + }) + .then((trade) => ({ + ...trade, + status: status as typeof status, + })); + } + + public async findPendingTradeById( + options: FindEntityByIdOptions, + ): Promise { + const { + id, + notFoundErrorMessageFn = (id) => `Pending trade (\`${id}\`) not found`, + } = options; + + return this.findPendingTrade({ + where: { id }, + notFoundErrorMessage: notFoundErrorMessageFn(id), + }); + } + + public async createPendingTrade( + values: CreatePendingTradeEntityValues, + tx?: Transaction, + ): Promise<{ + pendingTrade: PendingTradeEntity, + tradesToSenderItems: Array, + tradesToReceiverItems: Array, + }> { + const status = 'PENDING'; + const { sender, senderItems, receiver, receiverItems } = values; + + const pendingTrade = await (tx ?? this.db) + .insert(tradesTable) + .values({ + ...values, + status, + statusedAt: sql`now()`, + senderId: sender.id, + receiverId: receiver.id, + }) + .returning() + .then(([trade]) => ({ + ...trade!, + status: trade!.status as typeof status, + sender, + receiver, + })); + + const [tradesToSenderItems, tradesToReceiverItems] = await Promise.all([ + this.tradesToUserItemsRepository.createTradesToSenderItems( + senderItems.map((senderItem) => ({ + trade: pendingTrade, + senderItem, + })), + tx, + ), + this.tradesToUserItemsRepository.createTradesToReceiverItems( + receiverItems.map((receiverItem) => ({ + trade: pendingTrade, + receiverItem, + })), + tx, + ), + ]); + + return { + pendingTrade, + tradesToSenderItems, + tradesToReceiverItems, + }; + } + + public async updatePendingTradeToCancelledTrade( + pendingTrade: PendingTradeEntity, + tx?: Transaction, + ): Promise { + const status = 'CANCELLED'; + const { sender, receiver } = pendingTrade; + + return (tx ?? this.db) + .update(tradesTable) + .set({ + status, + statusedAt: sql`now()`, + }) + .returning() + .then(([trade]) => ({ + ...trade!, + status: trade!.status as typeof status, + sender, + receiver, + })); + } + + public async updatePendingTradeToAcceptedTrade( + pendingTrade: PendingTradeEntity, + tx?: Transaction, + ): Promise { + const status = 'ACCEPTED'; + const { sender, receiver } = pendingTrade; + + return (tx ?? this.db) + .update(tradesTable) + .set({ + status, + statusedAt: sql`now()`, + }) + .returning() + .then(([trade]) => ({ + ...trade!, + status: trade!.status as typeof status, + sender, + receiver, + })); + } + + public async updatePendingTradeToRejectedTrade( + pendingTrade: PendingTradeEntity, + tx?: Transaction, + ): Promise { + const status = 'REJECTED'; + const { sender, receiver } = pendingTrade; + + return (tx ?? this.db) + .update(tradesTable) + .set({ + status, + statusedAt: sql`now()`, + }) + .returning() + .then(([trade]) => ({ + ...trade!, + status: trade!.status as typeof status, + sender, + receiver, + })); + } +} diff --git a/src/infra/postgres/repositories/user-items.repository.ts b/src/infra/postgres/repositories/user-items.repository.ts new file mode 100644 index 0000000..d5194f2 --- /dev/null +++ b/src/infra/postgres/repositories/user-items.repository.ts @@ -0,0 +1,242 @@ +import { Injectable } from '@nestjs/common'; +import { Database, Transaction } from 'src/infra/postgres/types'; +import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; +import { Optional, PaginatedArray, UUIDv4 } from 'src/common/types'; +import { CreateUserItemEntityValues, pokemonsTable, UpdateUserItemEntityValues, UserEntity, UserItemEntity, userItemsTable, usersTable } from 'src/infra/postgres/tables'; +import { and, eq, inArray, like, SQL } from 'drizzle-orm'; +import { mapArrayToPaginatedArray } from 'src/common/helpers/map-array-to-paginated-array.helper'; +import { zip } from 'lodash'; +import { AppConflictException, AppEntityNotFoundException } from 'src/core/exceptions'; +import { + FindEntitiesOptions, + FindEntityOptions, + FindEntitiesWithPaginationOptions, + FindEntityByIdOptions, + FindEntitiesByIdsOptions, +} from 'src/core/types'; +import { FindUserItemsWhere, IUserItemsRepository } from 'src/core/repositories/user-items.repository'; + +export const mapUserItemsRowToEntity = ( + row: Record<'user_items' | 'users' | 'pokemons', any>, +): UserItemEntity => { + return { + ...row.user_items, + user: row.users, + pokemon: row.pokemons, + }; +}; + +@Injectable() +export class UserItemsRepository implements IUserItemsRepository { + public constructor( + @InjectDatabase() + private readonly db: Database, + ) {} + + private mapWhereToSQL( + where: FindUserItemsWhere, + ): Optional { + return and( + where.id !== undefined ? eq(userItemsTable.id, where.id) : undefined, + where.ids !== undefined ? inArray(userItemsTable.id, where.ids) : undefined, + + where.userId !== undefined ? eq(userItemsTable.userId, where.userId) : undefined, + where.userName !== undefined ? eq(usersTable.name, where.userName) : undefined, + where.userNameLike !== undefined ? like(usersTable.name, `%${where.userNameLike}%`) : undefined, + + where.pokemonId !== undefined ? eq(userItemsTable.pokemonId, where.pokemonId) : undefined, + where.pokemonName !== undefined ? eq(pokemonsTable.name, where.pokemonName) : undefined, + where.pokemonNameLike !== undefined ? like(pokemonsTable.name, `%${where.pokemonNameLike}%`) : undefined, + ); + }; + + private baseSelectBuilder( + options: FindEntitiesOptions, + ) { + const { where = {} } = options; + + return this.db + .select() + .from(userItemsTable) + .innerJoin(usersTable, eq(userItemsTable.userId, usersTable.id)) + .innerJoin(pokemonsTable, eq(userItemsTable.pokemonId, pokemonsTable.id)) + .where(this.mapWhereToSQL(where)); + } + + public async findUserItems( + findUserItemsOptions: FindEntitiesOptions + ): Promise> { + return this + .baseSelectBuilder(findUserItemsOptions) + .then((rows) => rows.map((row) => mapUserItemsRowToEntity(row))); + } + + public async findUserItemsByIds( + options: FindEntitiesByIdsOptions, + ): Promise> { + const { + ids, + notFoundErrorMessageFn = (id) => `User item (\`${id}\`) not found`, + } = options; + if (!ids.length) return []; + + const userItems = await this.findUserItems({ + where: { ids }, + }); + + for (const id of ids) { + const userItem = userItems.some((userItem) => userItem.id === id); + + if (!userItem) { + throw new AppEntityNotFoundException(notFoundErrorMessageFn(id)); + } + } + + return userItems; + } + + public async findUserItemsWithPagination( + options: FindEntitiesWithPaginationOptions, + ): Promise> { + const { + paginationOptions: { page, limit }, + } = options; + // TODO: check for boundaries + const offset = (page - 1) * limit; + + return this + .baseSelectBuilder(options) + .offset(offset) + .limit(limit) + .then((rows) => mapArrayToPaginatedArray( + rows.map((row) => mapUserItemsRowToEntity(row)), + { page, limit }, + )); + } + + public async findUserItem( + options: FindEntityOptions, + ): Promise { + const { + notFoundErrorMessage = 'User item not found', + } = options; + + const userItem = await this + .baseSelectBuilder(options) + .limit(1) + .then(([row]) => ( + row ? mapUserItemsRowToEntity(row) : null + )); + + if (!userItem) { + throw new AppEntityNotFoundException(notFoundErrorMessage); + } + + return userItem; + } + + public async findUserItemById( + options: FindEntityByIdOptions, + ): Promise { + const { + id, + notFoundErrorMessageFn = (id) => `User item (\`${id}\`) not found`, + } = options; + + return this.findUserItem({ + where: { id }, + notFoundErrorMessage: notFoundErrorMessageFn(id), + }); + } + + public async createUserItem( + values: CreateUserItemEntityValues, + tx?: Transaction, + ): Promise { + const { user, pokemon } = values; + + return (tx ?? this.db) + .insert(userItemsTable) + .values({ + ...values, + userId: user.id, + pokemonId: pokemon.id, + }) + .returning() + .then(([userItem]) => ({ + ...userItem!, + user, + pokemon, + })); + } + + public async updateUserItems( + userItems: Array, + values: UpdateUserItemEntityValues, + tx?: Transaction, + ): Promise> { + if (!userItems.length) return []; + + const { user, pokemon, ...restValues } = values; + + return (tx ?? this.db) + .update(userItemsTable) + .set({ + ...restValues, + userId: user?.id, + pokemonId: pokemon?.id, + }) + .where(inArray(userItemsTable.id, userItems.map(({ id }) => id))) + .returning() + .then((updatedUserItems) => zip(userItems, updatedUserItems).map(([userItem, updatedUserItem]) => ({ + ...updatedUserItem!, + user: user ?? userItem!.user, + pokemon: pokemon ?? userItem!.pokemon, + }))); + } + + public async transferUserItemsToAnotherUser( + fromUserItems: Array, + toUser: UserEntity, + tx?: Transaction, + ): Promise> { + if (!fromUserItems.length) return []; + + const set = new Set(fromUserItems.map(({ userId }) => userId)); + if (set.size > 1) { + throw new AppConflictException('All of the items must have the same user'); + } + + const fromUserId = fromUserItems[0]!.userId; + if (fromUserId === toUser.id) { + throw new AppConflictException('You cannot transfer items to yourself'); + } + + return this.updateUserItems(fromUserItems, { user: toUser }, tx); + } + + public async updateUserItem( + userItem: UserItemEntity, + values: UpdateUserItemEntityValues, + tx?: Transaction, + ): Promise { + return this + .updateUserItems([userItem], values, tx) + .then(([userItem]) => userItem!); + } + + public async deleteUserItem( + userItem: UserItemEntity, + tx?: Transaction, + ): Promise { + return (tx ?? this.db) + .delete(userItemsTable) + .where(eq(userItemsTable.id, userItem.id)) + .returning() + .then(([deletedUserItem]) => ({ + ...deletedUserItem!, + user: userItem.user, + pokemon: userItem.pokemon, + })) + } +} diff --git a/src/infra/postgres/repositories/user-refresh-tokens.repository.ts b/src/infra/postgres/repositories/user-refresh-tokens.repository.ts new file mode 100644 index 0000000..cf37ab4 --- /dev/null +++ b/src/infra/postgres/repositories/user-refresh-tokens.repository.ts @@ -0,0 +1,113 @@ +import { Injectable } from '@nestjs/common'; +import { SQL, and, eq, gt, sql } from 'drizzle-orm'; +import { Optional } from 'src/common/types'; +import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; +import { Database, Transaction } from 'src/infra/postgres/types'; +import { CreateUserRefreshTokenEntityValues, UserRefreshTokenEntity, userRefreshTokensTable, usersTable } from 'src/infra/postgres/tables'; +import { hashRefreshToken } from 'src/common/helpers/hash-refresh-token.helper'; +import { AppEntityNotFoundException } from 'src/core/exceptions'; +import { FindEntitiesOptions, FindEntityOptions } from 'src/core/types'; +import { FindUserRefreshTokensWhere, IUserRefreshTokensRepository } from 'src/core/repositories/user-refresh-tokens.repository'; + +export const mapUserRefreshTokensRowToEntity = ( + row: Record<'user_refresh_tokens' | 'users', any>, +): UserRefreshTokenEntity => ({ + ...row.user_refresh_tokens, + user: row.users, +}); + +@Injectable() +export class UserRefreshTokensRepository implements IUserRefreshTokensRepository { + public constructor( + @InjectDatabase() + private readonly db: Database, + ) {} + + private mapWhereToSQL( + where: FindUserRefreshTokensWhere, + ): Optional { + return and( + where.userId !== undefined ? eq(userRefreshTokensTable.userId, where.userId) : undefined, + where.refreshToken !== undefined + ? eq(userRefreshTokensTable.hashedRefreshToken, hashRefreshToken(where.refreshToken)) + : undefined, + ); + } + + private baseSelectBuilder( + options: FindEntitiesOptions, + ) { + const { where = {} } = options; + + return this.db + .select() + .from(userRefreshTokensTable) + .innerJoin(usersTable, eq(userRefreshTokensTable.userId, usersTable.id)) + .where(this.mapWhereToSQL(where)); + } + + public async findUserRefreshToken( + options: FindEntityOptions, + ): Promise { + const { + notFoundErrorMessage = 'User refresh token not found', + } = options; + + const userRefreshToken = await this + .baseSelectBuilder(options) + .limit(1) + .then(([row]) => ( + row ? mapUserRefreshTokensRowToEntity(row) : null + )); + + if (!userRefreshToken) { + throw new AppEntityNotFoundException(notFoundErrorMessage); + } + + return userRefreshToken; + } + + public async createUserRefreshToken( + values: CreateUserRefreshTokenEntityValues, + tx?: Transaction, + ): Promise { + const { user } = values; + + return (tx ?? this.db) + .insert(userRefreshTokensTable) + .values({ + ...values, + userId: user.id, + }) + .returning() + .then(([userRefreshToken]) => ({ + ...userRefreshToken!, + user, + })); + } + + public async deleteUserRefreshToken( + userRefreshToken: UserRefreshTokenEntity, + tx?: Transaction, + ): Promise { + return (tx ?? this.db) + .delete(userRefreshTokensTable) + .where(and( + eq(userRefreshTokensTable.userId, userRefreshToken.userId), + eq(userRefreshTokensTable.hashedRefreshToken, userRefreshToken.hashedRefreshToken), + )) + .returning() + .then(([deletedUserRefreshToken]) => ({ + ...deletedUserRefreshToken!, + user: userRefreshToken.user, + })); + } + + public async deleteExpiredUserRefreshTokens( + tx?: Transaction, + ): Promise { + await (tx ?? this.db) + .delete(userRefreshTokensTable) + .where(gt(sql`now()`, userRefreshTokensTable.expiresAt)); + } +} diff --git a/src/infra/postgres/repositories/users.repository.ts b/src/infra/postgres/repositories/users.repository.ts new file mode 100644 index 0000000..0fe60c4 --- /dev/null +++ b/src/infra/postgres/repositories/users.repository.ts @@ -0,0 +1,162 @@ +import { Injectable } from '@nestjs/common'; +import { Database, Transaction } from 'src/infra/postgres/types'; +import { InjectDatabase } from 'src/infra/ioc/decorators/inject-database.decorator'; +import { Nullable, Optional, PaginatedArray } from 'src/common/types'; +import { CreateUserEntityValues, UpdateUserEntityValues, UserEntity, usersTable } from 'src/infra/postgres/tables'; +import { and, eq, inArray, like, SQL } from 'drizzle-orm'; +import { mapArrayToPaginatedArray } from 'src/common/helpers/map-array-to-paginated-array.helper'; +import { AppEntityNotFoundException } from 'src/core/exceptions'; +import { FindEntitiesOptions, FindEntitiesWithPaginationOptions, FindEntityByIdOptions, FindEntityOptions } from 'src/core/types'; +import { FindUsersWhere, IUsersRepository } from 'src/core/repositories/users.repository'; + +@Injectable() +export class UsersRepository implements IUsersRepository { + public constructor( + @InjectDatabase() + private readonly db: Database, + ) {} + + private mapWhereToSQL( + where: FindUsersWhere, + ): Optional { + return and( + where.id !== undefined ? eq(usersTable.id, where.id) : undefined, + where.ids !== undefined ? inArray(usersTable.id, where.ids) : undefined, + where.name !== undefined ? eq(usersTable.name, where.name) : undefined, + where.nameLike !== undefined ? like(usersTable.name, `%${where.nameLike}%`) : undefined, + ); +} + + private baseSelectBuilder( + options: FindEntitiesOptions, + ) { + const { where = {} } = options; + + return this.db + .select() + .from(usersTable) + .where(this.mapWhereToSQL(where)); + } + + public async findUsers( + options: FindEntitiesOptions, + ): Promise> { + return this.baseSelectBuilder(options); + } + + public async findUsersWithPagination( + options: FindEntitiesWithPaginationOptions, + ): Promise> { + const { + paginationOptions: { page, limit }, + } = options; + // TODO: check for boundaries + const offset = (page - 1) * limit; + + // TODO: Pass these values to `mapArrayToPaginatedArray` + // const totalItems = await this.db + // .select({ + // totalItems: count(), + // }) + // .from(usersTable) + // .where(this.mapWhereToSQL(where)) + // .then(([row]) => row!.totalItems); + // const totalPages = Math.ceil(totalItems / offset); + + return this + .baseSelectBuilder(options) + .offset(offset) + .limit(limit) + .then((users) => mapArrayToPaginatedArray(users, { page, limit })); + } + + public async findUser( + options: FindEntityOptions, + ): Promise { + const { + notFoundErrorMessage = 'User not found', + } = options; + + const user = await this + .baseSelectBuilder(options) + .limit(1) + .then(([user]) => user ?? null); + + if (!user) { + throw new AppEntityNotFoundException(notFoundErrorMessage); + } + + return user; + } + + public async userExists( + where: FindUsersWhere, + ): Promise { + let user: Nullable = null; + try { + user = await this.findUser({ where }); + } catch (error) { + if (error instanceof AppEntityNotFoundException) { + return false; + } + + throw error; + } + + return true; + } + + public async findUserById( + options: FindEntityByIdOptions, + ): Promise { + const { + id, + notFoundErrorMessageFn = (id) => `User (\`${id}\`) not found`, + } = options; + + return this.findUser({ + where: { id }, + notFoundErrorMessage: notFoundErrorMessageFn(id), + }); + } + + public async createUser( + values: CreateUserEntityValues, + tx?: Transaction, + ): Promise { + return (tx ?? this.db) + .insert(usersTable) + .values(values) + .returning() + .then(([user]) => user!); + } + + public async updateUser( + user: UserEntity, + values: UpdateUserEntityValues, + tx?: Transaction, + ): Promise { + return (tx ?? this.db) + .update(usersTable) + .set(values) + .where(eq(usersTable.id, user.id)) + .returning() + .then(([updatedUser]) => updatedUser!); + } + + public async spendUserBalance( + user: UserEntity, + amount: number, + tx?: Transaction, + ): Promise { + return this.updateUser(user, { balance: user.balance - amount }, tx); + } + + public async replenishUserBalance( + user: UserEntity, + amount: number, + tx?: Transaction, + ): Promise { + return this.updateUser(user, { balance: user.balance + amount }, tx); + } +} diff --git a/src/infra/postgres/seeders/pokemons.seeder.ts b/src/infra/postgres/seeders/pokemons.seeder.ts index 014f4c8..523335d 100644 --- a/src/infra/postgres/seeders/pokemons.seeder.ts +++ b/src/infra/postgres/seeders/pokemons.seeder.ts @@ -1,10 +1,10 @@ import { Injectable } from "@nestjs/common"; import { Seeder } from "nestjs-seeder"; -import { PokemonsRepository } from 'src/core/repositories/pokemons.repository'; +import { IPokemonsRepository } from 'src/core/repositories/pokemons.repository'; @Injectable() export class PokemonsSeeder implements Seeder { - constructor(private readonly pokemonsRepository: PokemonsRepository) {} + constructor(private readonly pokemonsRepository: IPokemonsRepository) {} async seed() { // TODO: Maybe i can do that with a single request? Research on that