feat: Add core services for user, category, item, and restaurant management, including caching and ownership verification.

This commit is contained in:
2025-12-11 11:16:09 +03:00
parent 66cab810c1
commit 6ad4871bdc
6 changed files with 122 additions and 24 deletions
+6 -1
View File
@@ -1,7 +1,12 @@
export const keys = { export const keys = {
restaurantBySlug: (slug: string) => `restaurant:slug:${slug}`, restaurantBySlug: (slug: string) => `restaurant:slug:${slug}`,
restaurantCategories: (restaurantId: string) => `restaurant:${restaurantId}:categories`, restaurantById: (id: string) => `restaurant:id:${id}`,
restaurantCategories: (restaurantId: string) =>
`restaurant:${restaurantId}:categories`,
restaurantItems: (restaurantId: string) => `restaurant:${restaurantId}:items`, restaurantItems: (restaurantId: string) => `restaurant:${restaurantId}:items`,
userRestaurants: (ownerId: string) => `user:${ownerId}:restaurants`,
category: (categoryId: string) => `category:${categoryId}`, category: (categoryId: string) => `category:${categoryId}`,
categoryItems: (categoryId: string) => `category:${categoryId}:items`,
item: (itemId: string) => `item:${itemId}`, item: (itemId: string) => `item:${itemId}`,
user: (userId: string) => `user:${userId}`,
}; };
+36 -15
View File
@@ -16,22 +16,31 @@ export class CategoryService {
) {} ) {}
async _getCategorie(categoryId: string) { async _getCategorie(categoryId: string) {
const category = await this.prisma.category.findFirstOrThrow({ const cacheKey = keys.category(categoryId);
where: { id: categoryId }, const cached = await this.cache.get(cacheKey);
include: { items: true, restaurant: true }, if (cached) return cached;
}).catch(() => {
throw new NotFoundException('Category not found');
});
const category = await this.prisma.category
.findFirstOrThrow({
where: { id: categoryId },
include: { items: true, restaurant: true },
})
.catch(() => {
throw new NotFoundException('Category not found');
});
await this.cache.set(cacheKey, category, 600);
return category; return category;
} }
async create(dto: CreateCategoryDto, userId: string) { async create(dto: CreateCategoryDto, userId: string) {
const restaurant = await this.prisma.restaurant.findUniqueOrThrow({ const restaurant = await this.prisma.restaurant
where: { id: dto.restaurantId }, .findUniqueOrThrow({
}).catch(() => { where: { id: dto.restaurantId },
throw new NotFoundException('Restaurant not found'); })
}); .catch(() => {
throw new NotFoundException('Restaurant not found');
});
await this.restaurantService.verifyOwnership(userId, restaurant.id); await this.restaurantService.verifyOwnership(userId, restaurant.id);
@@ -51,19 +60,31 @@ export class CategoryService {
async findAllForRestaurant(restaurantId: string, userId: string) { async findAllForRestaurant(restaurantId: string, userId: string) {
await this.restaurantService.verifyOwnership(userId, restaurantId); await this.restaurantService.verifyOwnership(userId, restaurantId);
const cacheKey = keys.restaurantCategories(restaurantId);
const cached = await this.cache.get(cacheKey);
if (cached) return cached;
const categories = await this.prisma.category.findMany({ const categories = await this.prisma.category.findMany({
where: { restaurantId }, where: { restaurantId },
include: { items: true }, include: { items: true },
}); });
return categories.map(CategoryDto.fromEntity); const result = categories.map(CategoryDto.fromEntity);
await this.cache.set(cacheKey, result, 300);
return result;
} }
async findOne(id: string) { async findOne(id: string) {
const category = await this._getCategorie(id); const cacheKey = keys.category(id);
const cached = await this.cache.get(cacheKey);
if (cached) return cached;
return CategoryDto.fromEntity(category); const category = await this._getCategorie(id);
const result = CategoryDto.fromEntity(category);
await this.cache.set(cacheKey, result, 600);
return result;
} }
async update(id: string, dto: UpdateCategoryDto, userId: string) { async update(id: string, dto: UpdateCategoryDto, userId: string) {
+38 -3
View File
@@ -36,33 +36,54 @@ export class ItemService {
await this.cache.del(keys.restaurantBySlug(category.restaurant.slug)); await this.cache.del(keys.restaurantBySlug(category.restaurant.slug));
} }
await this.cache.del(keys.restaurantCategories(category.restaurantId)); await this.cache.del(keys.restaurantCategories(category.restaurantId));
await this.cache.del(keys.restaurantItems(category.restaurantId));
await this.cache.del(keys.categoryItems(category.id));
await this.cache.del(keys.category(category.id));
return MenuItemDto.fromEntity(item); return MenuItemDto.fromEntity(item);
} }
async findAllByCategory(categoryId: string) { async findAllByCategory(categoryId: string) {
const cacheKey = keys.categoryItems(categoryId);
const cached = await this.cache.get(cacheKey);
if (cached) return cached;
const items = await this.prisma.menuItem.findMany({ const items = await this.prisma.menuItem.findMany({
where: { categoryId }, where: { categoryId },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
}); });
return items.map(MenuItemDto.fromEntity); const result = items.map(MenuItemDto.fromEntity);
await this.cache.set(cacheKey, result, 300);
return result;
} }
async findAllForRestaurant(restaurantId: string) { async findAllForRestaurant(restaurantId: string) {
const cacheKey = keys.restaurantItems(restaurantId);
const cached = await this.cache.get(cacheKey);
if (cached) return cached;
const items = await this.prisma.menuItem.findMany({ const items = await this.prisma.menuItem.findMany({
where: { restaurantId }, where: { restaurantId },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
}); });
return items.map(MenuItemDto.fromEntity); const result = items.map(MenuItemDto.fromEntity);
await this.cache.set(cacheKey, result, 300);
return result;
} }
async findOne(id: string) { async findOne(id: string) {
const cacheKey = keys.item(id);
const cached = await this.cache.get(cacheKey);
if (cached) return cached;
const item = await this.prisma.menuItem.findUnique({ where: { id } }); const item = await this.prisma.menuItem.findUnique({ where: { id } });
if (!item) throw new NotFoundException('Menu item not found'); if (!item) throw new NotFoundException('Menu item not found');
return MenuItemDto.fromEntity(item); const result = MenuItemDto.fromEntity(item);
await this.cache.set(cacheKey, result, 600);
return result;
} }
async update(id: string, dto: UpdateItemDto, userId: string) { async update(id: string, dto: UpdateItemDto, userId: string) {
@@ -89,6 +110,16 @@ export class ItemService {
await this.cache.del(keys.restaurantBySlug(category.restaurant.slug)); await this.cache.del(keys.restaurantBySlug(category.restaurant.slug));
} }
await this.cache.del(keys.restaurantCategories(category.restaurantId)); await this.cache.del(keys.restaurantCategories(category.restaurantId));
await this.cache.del(keys.restaurantItems(category.restaurantId));
await this.cache.del(keys.categoryItems(category.id));
await this.cache.del(keys.category(category.id));
await this.cache.del(keys.item(id));
// If category changed, invalidate old category caches too
if (dto.categoryId && dto.categoryId !== item.categoryId) {
await this.cache.del(keys.categoryItems(item.categoryId));
await this.cache.del(keys.category(item.categoryId));
}
return MenuItemDto.fromEntity(updated); return MenuItemDto.fromEntity(updated);
} }
@@ -106,6 +137,10 @@ export class ItemService {
await this.cache.del(keys.restaurantBySlug(category.restaurant.slug)); await this.cache.del(keys.restaurantBySlug(category.restaurant.slug));
} }
await this.cache.del(keys.restaurantCategories(category.restaurantId)); await this.cache.del(keys.restaurantCategories(category.restaurantId));
await this.cache.del(keys.restaurantItems(category.restaurantId));
await this.cache.del(keys.categoryItems(category.id));
await this.cache.del(keys.category(category.id));
await this.cache.del(keys.item(id));
return deleted; return deleted;
} }
+20 -2
View File
@@ -40,15 +40,23 @@ export class RestaurantService {
}, },
}); });
await this.cache.del(keys.userRestaurants(ownerId));
return RestaurantDto.fromEntity(restaurant); return RestaurantDto.fromEntity(restaurant);
} }
async findByOwner(ownerId: string) { async findByOwner(ownerId: string) {
const cacheKey = keys.userRestaurants(ownerId);
const cached = await this.cache.get(cacheKey);
if (cached) return cached;
const restaurants = await this.prisma.restaurant.findMany({ const restaurants = await this.prisma.restaurant.findMany({
where: { ownerId }, where: { ownerId },
}); });
return restaurants.map(RestaurantDto.fromEntity); const result = restaurants.map(RestaurantDto.fromEntity);
await this.cache.set(cacheKey, result, 300);
return result;
} }
async findAll() { async findAll() {
@@ -57,6 +65,10 @@ export class RestaurantService {
} }
async findOne(id: string) { async findOne(id: string) {
const cacheKey = keys.restaurantById(id);
const cached = await this.cache.get(cacheKey);
if (cached) return cached;
const restaurant = await this.prisma.restaurant.findUnique({ const restaurant = await this.prisma.restaurant.findUnique({
where: { id }, where: { id },
}); });
@@ -65,7 +77,9 @@ export class RestaurantService {
throw new NotFoundException('Restaurant not found'); throw new NotFoundException('Restaurant not found');
} }
return RestaurantDto.fromEntity(restaurant); const result = RestaurantDto.fromEntity(restaurant);
await this.cache.set(cacheKey, result, 600);
return result;
} }
async findMyRestaurant(id: string, userId: string) { async findMyRestaurant(id: string, userId: string) {
@@ -113,8 +127,10 @@ export class RestaurantService {
// Invalidate cache by slug and by id // Invalidate cache by slug and by id
await this.cache.del(keys.restaurantBySlug(updated.slug)); await this.cache.del(keys.restaurantBySlug(updated.slug));
await this.cache.del(keys.restaurantById(updated.id));
await this.cache.del(keys.restaurantCategories(updated.id)); await this.cache.del(keys.restaurantCategories(updated.id));
await this.cache.del(keys.restaurantItems(updated.id)); await this.cache.del(keys.restaurantItems(updated.id));
await this.cache.del(keys.userRestaurants(userId));
return RestaurantDto.fromEntity(updated); return RestaurantDto.fromEntity(updated);
} }
@@ -125,8 +141,10 @@ export class RestaurantService {
// invalidate // invalidate
await this.cache.del(keys.restaurantBySlug(deleted.slug)); await this.cache.del(keys.restaurantBySlug(deleted.slug));
await this.cache.del(keys.restaurantById(deleted.id));
await this.cache.del(keys.restaurantCategories(deleted.id)); await this.cache.del(keys.restaurantCategories(deleted.id));
await this.cache.del(keys.restaurantItems(deleted.id)); await this.cache.del(keys.restaurantItems(deleted.id));
await this.cache.del(keys.userRestaurants(userId));
return deleted; return deleted;
} }
+2 -1
View File
@@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
import { UserController } from './user.controller'; import { UserController } from './user.controller';
import { UserService } from './user.service'; import { UserService } from './user.service';
import { PrismaModule } from 'src/prisma/prisma.module'; import { PrismaModule } from 'src/prisma/prisma.module';
import { CacheModule } from 'src/cache/cache.module';
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule, CacheModule],
controllers: [UserController], controllers: [UserController],
providers: [UserService], providers: [UserService],
}) })
+20 -2
View File
@@ -1,9 +1,14 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { CacheService } from 'src/cache/cache.service';
import { keys } from 'src/cache/cache.keys';
@Injectable() @Injectable()
export class UserService { export class UserService {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
private readonly cache: CacheService,
) {}
async getAllUsers() { async getAllUsers() {
return this.prisma.user.findMany({ return this.prisma.user.findMany({
@@ -12,11 +17,17 @@ export class UserService {
} }
async getUserById(id: string) { async getUserById(id: string) {
const cacheKey = keys.user(id);
const cached = await this.cache.get(cacheKey);
if (cached) return cached;
const user = await this.prisma.user.findUnique({ const user = await this.prisma.user.findUnique({
where: { id }, where: { id },
include: { accounts: true, sessions: true, restaurants: true }, include: { accounts: true, sessions: true, restaurants: true },
}); });
if (!user) throw new NotFoundException('User not found'); if (!user) throw new NotFoundException('User not found');
await this.cache.set(cacheKey, user, 600);
return user; return user;
} }
@@ -25,7 +36,14 @@ export class UserService {
await this.prisma.session.deleteMany({ where: { userId: id } }); await this.prisma.session.deleteMany({ where: { userId: id } });
await this.prisma.account.deleteMany({ where: { userId: id } }); await this.prisma.account.deleteMany({ where: { userId: id } });
await this.prisma.restaurant.deleteMany({ where: { ownerId: id } }); await this.prisma.restaurant.deleteMany({ where: { ownerId: id } });
return this.prisma.user.delete({ where: { id } });
const deleted = await this.prisma.user.delete({ where: { id } });
// Invalidate caches
await this.cache.del(keys.user(id));
await this.cache.del(keys.userRestaurants(id));
return deleted;
} }
async getSessionsByUserId(id: string) { async getSessionsByUserId(id: string) {