refactor: enforce repository boundary in application (WealthLedger/backend#171) #192
@@ -1,81 +1,24 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { PrismaService } from '../../../../prisma/prisma.service';
|
||||
import { MAX_CATEGORY_NAME_LENGTH } from '../domain/bank-card-limits';
|
||||
import { BankCardsRepository } from '../infrastructure/bank-cards.repository';
|
||||
import { conflict, missing, name } from './bank-card-input';
|
||||
|
||||
const EXPECTED_UPDATED_CATEGORIES = 1;
|
||||
const VERSION_INCREMENT = 1;
|
||||
import { name } from './bank-card-input';
|
||||
|
||||
@Injectable()
|
||||
export class ExpenseCategoriesService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly repository: BankCardsRepository,
|
||||
) {}
|
||||
constructor(private readonly repository: BankCardsRepository) {}
|
||||
|
||||
async categories(userId: string) {
|
||||
return (
|
||||
await this.prisma.expenseCategory.findMany({
|
||||
where: { userId },
|
||||
orderBy: [{ name: 'asc' }, { id: 'asc' }],
|
||||
})
|
||||
).map(({ id, name, seedKey, version }) => ({
|
||||
id,
|
||||
name,
|
||||
isDefault: seedKey === 'other',
|
||||
version,
|
||||
}));
|
||||
return this.repository.categories(userId);
|
||||
}
|
||||
|
||||
createCategory(userId: string, value: string) {
|
||||
const label = name(value, MAX_CATEGORY_NAME_LENGTH);
|
||||
return this.repository.write(async (tx) => {
|
||||
await this.repository.seed(tx, userId);
|
||||
const row = await tx.expenseCategory.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
userId,
|
||||
name: label,
|
||||
nameKey: label.toLocaleLowerCase('ru-RU'),
|
||||
},
|
||||
});
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
isDefault: false,
|
||||
version: row.version,
|
||||
};
|
||||
});
|
||||
return this.repository.createCategory(userId, label);
|
||||
}
|
||||
|
||||
updateCategory(userId: string, id: string, value: string, version: number) {
|
||||
const label = name(value, MAX_CATEGORY_NAME_LENGTH);
|
||||
return this.repository.write(async (tx) => {
|
||||
const row = await tx.expenseCategory.findFirst({ where: { id, userId } });
|
||||
if (!row) missing();
|
||||
if (
|
||||
(
|
||||
await tx.expenseCategory.updateMany({
|
||||
where: { id, userId, version },
|
||||
data: {
|
||||
name: label,
|
||||
nameKey: label.toLocaleLowerCase('ru-RU'),
|
||||
version: { increment: 1 },
|
||||
},
|
||||
})
|
||||
).count !== EXPECTED_UPDATED_CATEGORIES
|
||||
) {
|
||||
conflict();
|
||||
}
|
||||
return {
|
||||
id,
|
||||
name: label,
|
||||
isDefault: row.seedKey === 'other',
|
||||
version: version + VERSION_INCREMENT,
|
||||
};
|
||||
});
|
||||
return this.repository.updateCategory(userId, id, label, version);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import { BankOperationKind } from '../domain/bank-operation-kind';
|
||||
|
||||
const INITIAL_RETRY_COUNT = 0;
|
||||
const LAST_SEED_OFFSET = 1;
|
||||
const EXPECTED_UPDATED_CATEGORIES = 1;
|
||||
const VERSION_INCREMENT = 1;
|
||||
|
||||
type Tx = Prisma.TransactionClient;
|
||||
const TRANSACTION_MAX_WAIT_MS = 10_000;
|
||||
@@ -32,6 +34,67 @@ const seeds = [
|
||||
@Injectable()
|
||||
export class BankCardsRepository {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
async categories(userId: string) {
|
||||
return (
|
||||
await this.prisma.expenseCategory.findMany({
|
||||
where: { userId },
|
||||
orderBy: [{ name: 'asc' }, { id: 'asc' }],
|
||||
})
|
||||
).map(({ id, name, seedKey, version }) => ({
|
||||
id,
|
||||
name,
|
||||
isDefault: seedKey === 'other',
|
||||
version,
|
||||
}));
|
||||
}
|
||||
|
||||
createCategory(userId: string, label: string) {
|
||||
return this.write(async (tx) => {
|
||||
await this.seed(tx, userId);
|
||||
const row = await tx.expenseCategory.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
userId,
|
||||
name: label,
|
||||
nameKey: label.toLocaleLowerCase('ru-RU'),
|
||||
},
|
||||
});
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
isDefault: false,
|
||||
version: row.version,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
updateCategory(userId: string, id: string, label: string, version: number) {
|
||||
return this.write(async (tx) => {
|
||||
const row = await tx.expenseCategory.findFirst({ where: { id, userId } });
|
||||
if (!row) missing();
|
||||
if (
|
||||
(
|
||||
await tx.expenseCategory.updateMany({
|
||||
where: { id, userId, version },
|
||||
data: {
|
||||
name: label,
|
||||
nameKey: label.toLocaleLowerCase('ru-RU'),
|
||||
version: { increment: 1 },
|
||||
},
|
||||
})
|
||||
).count !== EXPECTED_UPDATED_CATEGORIES
|
||||
) {
|
||||
conflict();
|
||||
}
|
||||
return {
|
||||
id,
|
||||
name: label,
|
||||
isDefault: row.seedKey === 'other',
|
||||
version: version + VERSION_INCREMENT,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async write<T>(run: (tx: Tx) => Promise<T>): Promise<T> {
|
||||
for (let attempt = INITIAL_RETRY_COUNT; ; attempt++) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user