Refactor code structure for improved readability and maintainability
Test Suite / test (24.15.0) (push) Has been cancelled
Test Suite / test (24.15.0) (push) Has been cancelled
This commit is contained in:
+425
@@ -0,0 +1,425 @@
|
||||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ProductsService = void 0;
|
||||
const common_1 = require("@nestjs/common");
|
||||
const prisma_service_1 = require("../prisma/prisma.service");
|
||||
const normalize_name_1 = require("../common/utils/normalize-name");
|
||||
const ai_service_1 = require("../ai/ai.service");
|
||||
const categories_service_1 = require("../categories/categories.service");
|
||||
let ProductsService = class ProductsService {
|
||||
constructor(prisma, aiService, categoriesService) {
|
||||
this.prisma = prisma;
|
||||
this.aiService = aiService;
|
||||
this.categoriesService = categoriesService;
|
||||
}
|
||||
async findAll(filters) {
|
||||
return this.prisma.product.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
isPrivate: false,
|
||||
...(filters?.tag
|
||||
? { tags: { some: { tag: { name: filters.tag } } } }
|
||||
: {}),
|
||||
},
|
||||
include: {
|
||||
tags: { include: { tag: true } },
|
||||
nutrition: true,
|
||||
categoryRef: { include: { parent: { include: { parent: true } } } },
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
}
|
||||
async findByOwner(userId) {
|
||||
return this.prisma.product.findMany({
|
||||
where: { ownerId: userId, isPrivate: true, isActive: true },
|
||||
select: { id: true, name: true, canonicalName: true, categoryId: true },
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
}
|
||||
async createPrivate(data, userId) {
|
||||
const name = data.name.trim();
|
||||
const normalizedName = `private:${userId}:${(0, normalize_name_1.normalizeName)(name)}`;
|
||||
const existing = await this.prisma.product.findUnique({
|
||||
where: { normalizedName },
|
||||
});
|
||||
if (existing && existing.isActive)
|
||||
return existing;
|
||||
if (existing) {
|
||||
return this.prisma.product.update({
|
||||
where: { id: existing.id },
|
||||
data: { isActive: true, deletedAt: null, name, canonicalName: name },
|
||||
});
|
||||
}
|
||||
return this.prisma.product.create({
|
||||
data: {
|
||||
name,
|
||||
normalizedName,
|
||||
canonicalName: name,
|
||||
isActive: true,
|
||||
isPrivate: true,
|
||||
ownerId: userId,
|
||||
...(data.categoryId != null ? { categoryId: data.categoryId } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
async findDuplicateCandidates() {
|
||||
const products = await this.prisma.product.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
});
|
||||
const grouped = new Map();
|
||||
for (const product of products) {
|
||||
const key = product.normalizedName;
|
||||
if (!grouped.has(key)) {
|
||||
grouped.set(key, []);
|
||||
}
|
||||
grouped.get(key).push(product);
|
||||
}
|
||||
return Array.from(grouped.entries())
|
||||
.filter(([, items]) => items.length > 1)
|
||||
.map(([normalizedName, items]) => ({
|
||||
normalizedName,
|
||||
count: items.length,
|
||||
products: items,
|
||||
}));
|
||||
}
|
||||
async findOne(id) {
|
||||
const product = await this.prisma.product.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
if (!product) {
|
||||
throw new common_1.NotFoundException(`Product with id ${id} not found`);
|
||||
}
|
||||
return product;
|
||||
}
|
||||
async create(data, ownerId) {
|
||||
const name = data.name.trim();
|
||||
const normalizedName = (0, normalize_name_1.normalizeName)(name);
|
||||
const existing = await this.prisma.product.findUnique({
|
||||
where: { normalizedName },
|
||||
});
|
||||
if (existing) {
|
||||
if (!existing.isActive) {
|
||||
return this.prisma.product.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
name,
|
||||
canonicalName: name,
|
||||
},
|
||||
});
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
if (!ownerId) {
|
||||
throw new Error('ownerId är obligatorisk för att skapa en produkt');
|
||||
}
|
||||
return this.prisma.product.create({
|
||||
data: {
|
||||
ownerId,
|
||||
name,
|
||||
normalizedName,
|
||||
canonicalName: name,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
...(data.categoryId != null ? { categoryId: data.categoryId } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
async update(id, data) {
|
||||
await this.findOne(id);
|
||||
const updateData = {};
|
||||
if (typeof data.name === 'string') {
|
||||
const name = data.name.trim();
|
||||
const normalizedName = (0, normalize_name_1.normalizeName)(name);
|
||||
const existing = await this.prisma.product.findUnique({
|
||||
where: { normalizedName },
|
||||
});
|
||||
if (existing && existing.id !== id) {
|
||||
throw new Error('Det finns redan en annan produkt med detta namn. Välj ett unikt namn.');
|
||||
}
|
||||
updateData.name = name;
|
||||
updateData.normalizedName = normalizedName;
|
||||
}
|
||||
if (typeof data.canonicalName === 'string') {
|
||||
updateData.canonicalName = data.canonicalName.trim() || undefined;
|
||||
}
|
||||
if (typeof data.category === 'string') {
|
||||
updateData.category = data.category.trim() || null;
|
||||
}
|
||||
if ('categoryId' in data) {
|
||||
updateData.categoryId = data.categoryId ?? null;
|
||||
}
|
||||
return this.prisma.product.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: { tags: { include: { tag: true } }, nutrition: true },
|
||||
});
|
||||
}
|
||||
async updateCanonicalName(id, canonicalName) {
|
||||
await this.findOne(id);
|
||||
const cleaned = canonicalName.trim();
|
||||
return this.prisma.product.update({
|
||||
where: { id },
|
||||
data: {
|
||||
canonicalName: cleaned,
|
||||
},
|
||||
});
|
||||
}
|
||||
async findDeleted() {
|
||||
return this.prisma.product.findMany({
|
||||
where: { isActive: false },
|
||||
orderBy: { deletedAt: 'desc' },
|
||||
});
|
||||
}
|
||||
async remove(id) {
|
||||
await this.findOne(id);
|
||||
return this.prisma.product.update({
|
||||
where: { id },
|
||||
data: {
|
||||
isActive: false,
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
async permanentDelete(id) {
|
||||
await this.findOne(id);
|
||||
await this.prisma.productTag.deleteMany({ where: { productId: id } });
|
||||
await this.prisma.userProduct.deleteMany({ where: { productId: id } });
|
||||
return this.prisma.product.delete({ where: { id } });
|
||||
}
|
||||
async restore(id) {
|
||||
const product = await this.findOne(id);
|
||||
if (product.isActive) {
|
||||
return product;
|
||||
}
|
||||
return this.prisma.product.update({
|
||||
where: { id },
|
||||
data: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
async findProductByIdOrThrow(id, label) {
|
||||
const product = await this.prisma.product.findUnique({ where: { id } });
|
||||
if (!product) {
|
||||
throw new common_1.NotFoundException(`${label} product with id ${id} not found`);
|
||||
}
|
||||
return product;
|
||||
}
|
||||
async merge(sourceProductId, targetProductId) {
|
||||
if (sourceProductId === targetProductId) {
|
||||
throw new Error('sourceProductId och targetProductId kan inte vara samma');
|
||||
}
|
||||
const source = await this.findProductByIdOrThrow(sourceProductId, 'Source');
|
||||
const target = await this.findProductByIdOrThrow(targetProductId, 'Target');
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const movedInventoryCount = await tx.inventoryItem.updateMany({
|
||||
where: { productId: sourceProductId },
|
||||
data: { productId: targetProductId },
|
||||
});
|
||||
const softDeletedSource = await tx.product.update({
|
||||
where: { id: sourceProductId },
|
||||
data: {
|
||||
isActive: false,
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
return {
|
||||
message: 'Products merged successfully',
|
||||
sourceProductId,
|
||||
targetProductId,
|
||||
movedInventoryCount: movedInventoryCount.count,
|
||||
softDeletedSource,
|
||||
};
|
||||
});
|
||||
}
|
||||
async previewMerge(sourceProductId, targetProductId) {
|
||||
if (sourceProductId === targetProductId) {
|
||||
throw new Error('sourceProductId och targetProductId kan inte vara samma');
|
||||
}
|
||||
const [source, target, sourceInventoryCount, targetInventoryCount] = await Promise.all([
|
||||
this.findProductByIdOrThrow(sourceProductId, 'Source'),
|
||||
this.findProductByIdOrThrow(targetProductId, 'Target'),
|
||||
this.prisma.inventoryItem.count({
|
||||
where: { productId: sourceProductId },
|
||||
}),
|
||||
this.prisma.inventoryItem.count({
|
||||
where: { productId: targetProductId },
|
||||
}),
|
||||
]);
|
||||
return {
|
||||
source: {
|
||||
...source,
|
||||
inventoryCount: sourceInventoryCount,
|
||||
},
|
||||
target: {
|
||||
...target,
|
||||
inventoryCount: targetInventoryCount,
|
||||
},
|
||||
outcome: {
|
||||
inventoryItemsToMove: sourceInventoryCount,
|
||||
sourceWillBeSoftDeleted: true,
|
||||
targetWillRemainActive: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
async backfillCanonicalNames() {
|
||||
const products = await this.prisma.product.findMany({
|
||||
where: {
|
||||
canonicalName: null,
|
||||
},
|
||||
});
|
||||
const results = await this.prisma.$transaction(products.map((product) => this.prisma.product.update({
|
||||
where: { id: product.id },
|
||||
data: {
|
||||
canonicalName: product.name,
|
||||
},
|
||||
})));
|
||||
return {
|
||||
message: 'Canonical names backfilled successfully',
|
||||
updatedCount: results.length,
|
||||
products: results,
|
||||
};
|
||||
}
|
||||
async setTags(productId, tagNames) {
|
||||
await this.findOne(productId);
|
||||
const tags = await this.prisma.$transaction(tagNames.map((name) => this.prisma.tag.upsert({
|
||||
where: { name },
|
||||
create: { name },
|
||||
update: {},
|
||||
})));
|
||||
await this.prisma.productTag.deleteMany({ where: { productId } });
|
||||
if (tags.length > 0) {
|
||||
await this.prisma.productTag.createMany({
|
||||
data: tags.map((tag) => ({ productId, tagId: tag.id })),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
return this.prisma.product.findUnique({
|
||||
where: { id: productId },
|
||||
include: { tags: { include: { tag: true } }, nutrition: true },
|
||||
});
|
||||
}
|
||||
async upsertNutrition(productId, data) {
|
||||
await this.findOne(productId);
|
||||
return this.prisma.nutrition.upsert({
|
||||
where: { productId },
|
||||
create: { productId, ...data },
|
||||
update: { ...data },
|
||||
});
|
||||
}
|
||||
async findAllTags() {
|
||||
return this.prisma.tag.findMany({ orderBy: { name: 'asc' } });
|
||||
}
|
||||
async resetAll() {
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.receiptAlias.deleteMany(),
|
||||
this.prisma.inventoryConsumption.deleteMany(),
|
||||
this.prisma.inventoryItem.deleteMany(),
|
||||
this.prisma.pantryItem.deleteMany(),
|
||||
this.prisma.nutrition.deleteMany(),
|
||||
this.prisma.productTag.deleteMany(),
|
||||
this.prisma.tag.deleteMany(),
|
||||
this.prisma.userProduct.deleteMany(),
|
||||
this.prisma.recipeIngredient.deleteMany(),
|
||||
this.prisma.product.deleteMany(),
|
||||
]);
|
||||
return { ok: true };
|
||||
}
|
||||
async bulkUpdate(ids, data) {
|
||||
const updateData = {};
|
||||
if ('categoryId' in data) {
|
||||
updateData.categoryId = data.categoryId;
|
||||
}
|
||||
if (Object.keys(updateData).length === 0)
|
||||
return { updated: 0 };
|
||||
await this.prisma.product.updateMany({ where: { id: { in: ids } }, data: updateData });
|
||||
return { updated: ids.length };
|
||||
}
|
||||
async findUncategorized() {
|
||||
return this.prisma.product.findMany({
|
||||
where: { isActive: true, categoryId: null, status: 'active' },
|
||||
select: { id: true, name: true, canonicalName: true },
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
}
|
||||
async aiCategorizeBulk(productIds) {
|
||||
const categories = await this.categoriesService.findFlattened();
|
||||
let products;
|
||||
if (productIds && productIds.length > 0) {
|
||||
const found = await Promise.all(productIds.map((id) => this.findOne(id)));
|
||||
products = found.map((p) => ({ id: p.id, name: p.canonicalName ?? p.name }));
|
||||
}
|
||||
else {
|
||||
products = await this.findUncategorized();
|
||||
}
|
||||
const results = [];
|
||||
for (const product of products) {
|
||||
const suggestion = await this.aiService.suggestCategory(product.name, categories);
|
||||
results.push({ productId: product.id, productName: product.name, suggestion });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
async findPending() {
|
||||
return this.prisma.product.findMany({
|
||||
where: { status: 'pending' },
|
||||
include: {
|
||||
categoryRef: { include: { parent: true } },
|
||||
owner: { select: { id: true, username: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
async createPending(data, userId) {
|
||||
const name = data.name.trim();
|
||||
const normalizedName = (0, normalize_name_1.normalizeName)(name);
|
||||
const existing = await this.prisma.product.findUnique({
|
||||
where: { normalizedName },
|
||||
});
|
||||
if (existing) {
|
||||
if (existing.isActive && existing.status === 'active') {
|
||||
return existing;
|
||||
}
|
||||
if (existing.status === 'pending') {
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
return this.prisma.product.create({
|
||||
data: {
|
||||
name,
|
||||
normalizedName,
|
||||
canonicalName: name,
|
||||
isActive: false,
|
||||
status: 'pending',
|
||||
ownerId: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
setStatus(id, status) {
|
||||
return this.prisma.product.update({ where: { id }, data: { status } });
|
||||
}
|
||||
};
|
||||
exports.ProductsService = ProductsService;
|
||||
exports.ProductsService = ProductsService = __decorate([
|
||||
(0, common_1.Injectable)(),
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
|
||||
ai_service_1.AiService,
|
||||
categories_service_1.CategoriesService])
|
||||
], ProductsService);
|
||||
//# sourceMappingURL=products.service.js.map
|
||||
Reference in New Issue
Block a user