Про логику пользователей не понял, но и сама суть использования репозитория - в получении данных, создании запроса и т.д.
Репозиторий дополнительный слой абстракции где пишется код построения запроса. И раз уж в статье идет речь о связанности и использовании интерфесов, то это лишь обертка над каким-то клиентом (со своим контрактом) для какого-то хранилища. У Вас в статье приведен пример:
Hidden text
// Репозиторий
@Injectable()
export class UserRepository implements IUserRepository {
constructor(
@InjectRepository(User) private readonly userRepository: Repository<User>,
) {}
async findAll(): Promise<User[]> {
// Логика для получения всех пользователей
}
async findOne(id: string): Promise<User> {
// Логика для получения пользователя по ID
}
async create(createUserDto: CreateUserDto): Promise<User> {
// Логика для создания нового пользователя
}
}
Ни в одном методе не написан пример логики, потому каждый сам будет додумывать. А мышление новичка и более опытного программиста будет различаться. Метод findAll не принимает никаких аргументов, а findOne принимает только id. Но findAll в таком виде явно не нужен, также как и поиск только по id потому что могут появиться другие условия и мы же не будем писать для этого отдельный метод, верно? Или будем, но только в другом слое.
Потому, думаю, что можно было написать +- что-то подобное. Как раз дальше показав, что у Вас в репозитории может использоваться хоть ORM, хоть чистый SQL, но они оба следуют одному контракту и всё что требуется для подмены реализации, так это только описать саму реализацию и подменить на нужную в модуле. Но в моем примере будет еще и бизнес слой затронут, так как всё таки для разных клиентов where и relations будут написаны по разному.
Hidden text
export interface IUserRepository {
findAll<W = Record<string, unknown>, R = Record<string, unknown>>(args?: { limit?: number, page?: number, where?: W, query?: string, spleen?: string, relations?: R }): Promise<User[]>;
findOne<W = Record<string, unknown>, R = Record<string, unknown>>(args: { where: W, relations?: R }): Promise<User>;
}
// Репозиторий
@Injectable()
export class UserPrismaRepository implements IUserRepository {
constructor(private readonly client: PrismaClient) {}
findAll<W = Record<string, unknown>, R = Record<string, unknown>>(args?: { limit?: number, page?: number, where?: W, query?: string, spleen?: string, relations?: R }): Promise<User[]> {
// Логика обработки входящих аргументов для построения запроса
return this.client.user.findMany({
where,
//...other
})
}
findOne<W = Record<string, unknown>, R = Record<string, unknown>>(args: { where: W, relations?: R }): Promise<User> {
// Логика обработки входящих аргументов для построения запроса
return this.client.user.findOne({
where,
//...other
})
}
}
// Репозиторий
@Injectable()
export class UserTypeOrmRepository implements IUserRepository {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>
) {}
findAll<W = Record<string, unknown>, R = Record<string, unknown>>(args?: { limit?: number, page?: number, where?: W, query?: string, spleen?: string, relations?: R }): Promise<User[]> {
// Логика обработки входящих аргументов для построения запроса
return this.usersRepository.findMany({
where,
//...other
})
}
findOne<W = Record<string, unknown>, R = Record<string, unknown>>(args: { where: W, relations?: R }): Promise<User> {
// Логика обработки входящих аргументов для построения запроса
return this.usersRepository.findOne({
where,
//...other
})
}
}
Про третий пункт - ну пример жеж, смысл в описании флоу функции, плюс мнение на реализацию у каждой команды будет свое
Да тут я вообще зря быканул)
Про исключения не понял проблемы 😞.
В таком варинте, совершенно никакой проблемы. Тут хоть в репозитории киньте HttpException (только это вообще не его ЗО), установите глобальный фильтр исключений и всё будет нормально
Hidden text
graph LR
UsersHttpController-->UserApplicationService-->UsersDomainService-->UsersRepository
Но в таком варианте глобальный фильтр уже не подойдет. На каждую точку входа (транспорт) нужно писать отдельный фильтр, потому что форматы ответа будут отличаться. Если быть точнее, то можно сделать "дефолтный" глобальный фильтр для HTTP, а на RMQ контроллер кинуть другой фильтр и тогда проблем не будет.
Hidden text
graph LR
UsersHttpController-->UserApplicationService
UsersRmqController-->UserApplicationService
UserApplicationService-->UsersDomainService
UsersDomainService-->UsersRepository
Не пишу комментарии к какой-либо статье на хабре, потому что обычно в комментариях уже более опытные люди высказали свое мнение. Но тут у меня есть с чем не согласиться. Возможно какую-то информацию я не верно интерпретировал, додумал сам, принял всё близко к сердцу и т.д., потому и "родился" данный комментарий.
1) Отсутствие абстрактного и(ли) графического (доменного) представления: Простая представленная схема. Возможно она действительно нарисованна просто для наглядности связи между модулями и я не разлядел в ней того как они на самом деле между собой связаны. Ибо глядя на эту схему я вижу "класические" модули из документации NestJS и когда модули вот так вот полностью импортируются один в другой это со временем начинает вызывать ряд проблем: связанность, в модули тянется то что им совершенно ненужно, из-за чего приложение стартует дольше.
Набираясь опыта пришел к решению (а в последующем увидел в разных статьях и официальном курсе NestJS), что каждый модуль также делится на слои, где каждый слой это модуль. И если модулю Feature1 нужен модуль Feature2, то в него будет импортироваться только бизнес слой(модуль) из модуля Feature2.
2) Игнорирование контрактов и высокая связность Про подключение провайдеров через интефейсы соглашусь, но в целом про модуль написано выше. Обойтись и без инжектов через токены можно используя абстрактные классы.
Не знаю какую Вы закладывали логику для получения/создания пользователей, но так как Вы не написали пример то приходится додумывать самому что там написано. А потому могу сказалить лишь то, что максимум какая логика тут может быть описана, так это обработка входящих параметров (условия выборки, пагинация и т.д.) в зависимости от выбранного хранилища и клиента для него. При этом в контракте эти параметры должны быть описаны так, чтобы в любой момент можно было подменить как хранилище, так и клиента (сомневаюсь, что это частая практика и тем не менее, вдруг с TypeOrm решили перейти например на Kysely).
3) Отсутствие комментариев С решением согласен, а вот пример функции тригернул.
Функция, которая добававляет пользователя в базу данных и отсылает ему на почту сообщение с приветствием
Ну не должна функция заниматься и созданием пользователя и отправкой уведомления. После создания пользователя лучше либо кинуть событие, на которое отрегарует соответствующий подписчик, либо в бизнес слое после создания пользователя отправляется уведомление.
4) Неправильное управление исключениями. Вот тут крайне спорная ситуация. Если же с Вашим бекэндом общаются только через TCP, то решение кинуть HttpException в методе SomeService имеет место быть, иначе управление исключениями явно должно быть иное.
Omg, что мешает просто использовать
import { OmitType, PartialType } from '@nestjs/mapped-types'
И тогда не будет речи о том чтобы дублировать поля в разных файлах/классах, а так же есть возможность их переопределить
Схема действительно спорная так как она мало чем отличается от той, что есть в официальной документации и в итоге не показывает решения.
Например (mermaid)
Hidden text
graph LR
AppModule-->ResourcesModule
ResourcesModule-->ResourcesPresentersModule
ResourcesPresentersModule-->ResourcesHttpApiModule
ResourcesHttpApiModule-->ResourcesApplicationModule
ResourcesApplicationModule-->ResourcesDomainModule
ResourcesDomainModule-->ResourcesRepositoryModule
ResourcesApplicationModule-->TagsApplicationModule
TagsApplicationModule-->TagsDomainModule
TagsDomainModule-->TagsRepositoryModule
Репозиторий дополнительный слой абстракции где пишется код построения запроса. И раз уж в статье идет речь о связанности и использовании интерфесов, то это лишь обертка над каким-то клиентом (со своим контрактом) для какого-то хранилища.
У Вас в статье приведен пример:
Hidden text
Ни в одном методе не написан пример логики, потому каждый сам будет додумывать. А мышление новичка и более опытного программиста будет различаться. Метод
findAll
не принимает никаких аргументов, аfindOne
принимает толькоid
. НоfindAll
в таком виде явно не нужен, также как и поиск только поid
потому что могут появиться другие условия и мы же не будем писать для этого отдельный метод, верно? Или будем, но только в другом слое.Потому, думаю, что можно было написать +- что-то подобное. Как раз дальше показав, что у Вас в репозитории может использоваться хоть ORM, хоть чистый SQL, но они оба следуют одному контракту и всё что требуется для подмены реализации, так это только описать саму реализацию и подменить на нужную в модуле. Но в моем примере будет еще и бизнес слой затронут, так как всё таки для разных клиентов
where
иrelations
будут написаны по разному.Hidden text
Да тут я вообще зря быканул)
В таком варинте, совершенно никакой проблемы. Тут хоть в репозитории киньте
HttpException
(только это вообще не его ЗО), установите глобальный фильтр исключений и всё будет нормальноHidden text
Но в таком варианте глобальный фильтр уже не подойдет. На каждую точку входа (транспорт) нужно писать отдельный фильтр, потому что форматы ответа будут отличаться. Если быть точнее, то можно сделать "дефолтный" глобальный фильтр для HTTP, а на RMQ контроллер кинуть другой фильтр и тогда проблем не будет.
Hidden text
Не пишу комментарии к какой-либо статье на хабре, потому что обычно в комментариях уже более опытные люди высказали свое мнение. Но тут у меня есть с чем не согласиться. Возможно какую-то информацию я не верно интерпретировал, додумал сам, принял всё близко к сердцу и т.д., потому и "родился" данный комментарий.
1) Отсутствие абстрактного и(ли) графического (доменного) представления:
Простая представленная схема. Возможно она действительно нарисованна просто для наглядности связи между модулями и я не разлядел в ней того как они на самом деле между собой связаны. Ибо глядя на эту схему я вижу "класические" модули из документации NestJS и когда модули вот так вот полностью импортируются один в другой это со временем начинает вызывать ряд проблем: связанность, в модули тянется то что им совершенно ненужно, из-за чего приложение стартует дольше.
Набираясь опыта пришел к решению (а в последующем увидел в разных статьях и официальном курсе NestJS), что каждый модуль также делится на слои, где каждый слой это модуль. И если модулю Feature1 нужен модуль Feature2, то в него будет импортироваться только бизнес слой(модуль) из модуля Feature2.
2) Игнорирование контрактов и высокая связность
Про подключение провайдеров через интефейсы соглашусь, но в целом про модуль написано выше. Обойтись и без инжектов через токены можно используя абстрактные классы.
Не знаю какую Вы закладывали логику для получения/создания пользователей, но так как Вы не написали пример то приходится додумывать самому что там написано. А потому могу сказалить лишь то, что максимум какая логика тут может быть описана, так это обработка входящих параметров (условия выборки, пагинация и т.д.) в зависимости от выбранного хранилища и клиента для него. При этом в контракте эти параметры должны быть описаны так, чтобы в любой момент можно было подменить как хранилище, так и клиента (сомневаюсь, что это частая практика и тем не менее, вдруг с TypeOrm решили перейти например на Kysely).
3) Отсутствие комментариев
С решением согласен, а вот пример функции тригернул.
Ну не должна функция заниматься и созданием пользователя и отправкой уведомления. После создания пользователя лучше либо кинуть событие, на которое отрегарует соответствующий подписчик, либо в бизнес слое после создания пользователя отправляется уведомление.
4) Неправильное управление исключениями.
Вот тут крайне спорная ситуация. Если же с Вашим бекэндом общаются только через TCP, то решение кинуть HttpException в методе SomeService имеет место быть, иначе управление исключениями явно должно быть иное.
Не силён в данной теме. Но есть же такой инструмент как pgbadger, позволяющий на основе логов получить информацию о времени выполнения запросов
Уже пробовали grammY?
Если да, можете сказать своё мнение о нём в сравнении с Telegraf?