chore: migrate to pnpm workspaces monorepo with Turborepo
- Restructure directories: apps/api, apps/admin, apps/website - Add root pnpm-workspace.yaml, turbo.json, .prettierrc, .gitignore - Rename packages to @inkreach/api, @inkreach/admin, @inkreach/website - Add shared packages: packages/tsconfig, packages/shared-types - Add pnpm.onlyBuiltDependencies for native builds - Update docs: README.md, structs.md - All three projects build successfully
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateGoodDto } from './dto/create-good.dto';
|
||||
import { UpdateGoodDto } from './dto/update-good.dto';
|
||||
import { QueryGoodDto } from './dto/query-good.dto';
|
||||
import { BatchCreateGoodDto } from './dto/batch-create-good.dto';
|
||||
import { BatchPriorityDto } from './dto/batch-priority.dto';
|
||||
import { GoodDto, PaginatedGoods } from './dto/good.dto';
|
||||
|
||||
const GOOD_INCLUDE = {
|
||||
country: true,
|
||||
category: true,
|
||||
tag: true,
|
||||
position: true,
|
||||
originGood: true,
|
||||
goodTags: { include: { tag: true } },
|
||||
} satisfies Prisma.GoodInclude;
|
||||
|
||||
@Injectable()
|
||||
export class GoodsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async findAll(query: QueryGoodDto): Promise<PaginatedGoods> {
|
||||
const { page, pageSize, countryId, categoryId, tagId, positionId, keyword } = query;
|
||||
const where: Prisma.GoodWhereInput = {};
|
||||
if (countryId !== undefined) where.countryId = BigInt(countryId);
|
||||
if (tagId !== undefined) where.goodTags = { some: { tagId: BigInt(tagId) } };
|
||||
if (positionId !== undefined) where.positionId = BigInt(positionId);
|
||||
if (keyword) {
|
||||
where.goodName = { contains: keyword, mode: 'insensitive' };
|
||||
}
|
||||
if (categoryId !== undefined) {
|
||||
const ids = await this.collectCategoryDescendants(BigInt(categoryId));
|
||||
where.categoryId = { in: ids };
|
||||
}
|
||||
|
||||
const [total, rows] = await this.prisma.$transaction([
|
||||
this.prisma.good.count({ where }),
|
||||
this.prisma.good.findMany({
|
||||
where,
|
||||
include: GOOD_INCLUDE,
|
||||
orderBy: [{ goodPriority: 'desc' }, { createdAt: 'desc' }],
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
items: rows.map((g) => GoodDto.from(g, {
|
||||
country: g.country,
|
||||
category: g.category,
|
||||
tag: g.tag,
|
||||
position: g.position,
|
||||
originGood: g.originGood,
|
||||
goodTags: g.goodTags,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
async findOne(id: bigint): Promise<GoodDto> {
|
||||
const good = await this.prisma.good.findUnique({
|
||||
where: { id },
|
||||
include: GOOD_INCLUDE,
|
||||
});
|
||||
if (!good) throw new NotFoundException(`Good ${id} not found`);
|
||||
return GoodDto.from(good, {
|
||||
country: good.country,
|
||||
category: good.category,
|
||||
tag: good.tag,
|
||||
position: good.position,
|
||||
originGood: good.originGood,
|
||||
goodTags: good.goodTags,
|
||||
});
|
||||
}
|
||||
|
||||
async create(dto: CreateGoodDto): Promise<GoodDto> {
|
||||
await this.ensureReferences(dto);
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const created = await tx.good.create({
|
||||
data: {
|
||||
goodName: dto.goodName,
|
||||
goodImage: dto.goodImage,
|
||||
originGoodId: BigInt(dto.originGoodId),
|
||||
countryId: BigInt(dto.countryId),
|
||||
categoryId: BigInt(dto.categoryId),
|
||||
positionId: dto.positionId === undefined ? null : BigInt(dto.positionId),
|
||||
goodPriority: dto.goodPriority ?? 0,
|
||||
},
|
||||
});
|
||||
if (dto.tagIds && dto.tagIds.length > 0) {
|
||||
await tx.goodTag.createMany({
|
||||
data: dto.tagIds.map((tagId) => ({
|
||||
goodId: created.id,
|
||||
tagId: BigInt(tagId),
|
||||
})),
|
||||
});
|
||||
}
|
||||
const result = await tx.good.findUniqueOrThrow({
|
||||
where: { id: created.id },
|
||||
include: GOOD_INCLUDE,
|
||||
});
|
||||
return GoodDto.from(result, {
|
||||
country: result.country,
|
||||
category: result.category,
|
||||
tag: result.tag,
|
||||
position: result.position,
|
||||
originGood: result.originGood,
|
||||
goodTags: result.goodTags,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: bigint, dto: UpdateGoodDto): Promise<GoodDto> {
|
||||
await this.findOne(id);
|
||||
const data: Prisma.GoodUpdateInput = {};
|
||||
if (dto.goodName !== undefined) data.goodName = dto.goodName;
|
||||
if (dto.originGoodId !== undefined) {
|
||||
await this.ensureOriginGood(dto.originGoodId);
|
||||
data.originGood = { connect: { id: BigInt(dto.originGoodId) } };
|
||||
}
|
||||
if (dto.countryId !== undefined) {
|
||||
await this.ensureCountry(dto.countryId);
|
||||
data.country = { connect: { id: BigInt(dto.countryId) } };
|
||||
}
|
||||
if (dto.categoryId !== undefined) {
|
||||
await this.ensureCategory(dto.categoryId);
|
||||
data.category = { connect: { id: BigInt(dto.categoryId) } };
|
||||
}
|
||||
if (dto.positionId !== undefined) {
|
||||
data.position =
|
||||
dto.positionId === null
|
||||
? { disconnect: true }
|
||||
: { connect: { id: BigInt(dto.positionId) } };
|
||||
}
|
||||
if (dto.goodPriority !== undefined) data.goodPriority = dto.goodPriority;
|
||||
if (dto.goodImage !== undefined) data.goodImage = dto.goodImage;
|
||||
if (dto.tagIds !== undefined) {
|
||||
for (const tagId of dto.tagIds) {
|
||||
await this.ensureTag(tagId);
|
||||
}
|
||||
}
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
if (dto.tagIds !== undefined) {
|
||||
await tx.goodTag.deleteMany({ where: { goodId: id } });
|
||||
if (dto.tagIds.length > 0) {
|
||||
await tx.goodTag.createMany({
|
||||
data: dto.tagIds.map((tagId) => ({
|
||||
goodId: id,
|
||||
tagId: BigInt(tagId),
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
const updated = await tx.good.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: GOOD_INCLUDE,
|
||||
});
|
||||
return GoodDto.from(updated, {
|
||||
country: updated.country,
|
||||
category: updated.category,
|
||||
tag: updated.tag,
|
||||
position: updated.position,
|
||||
originGood: updated.originGood,
|
||||
goodTags: updated.goodTags,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async remove(id: bigint): Promise<{ id: string }> {
|
||||
await this.findOne(id);
|
||||
await this.prisma.good.delete({ where: { id } });
|
||||
return { id: id.toString() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates priorities in a single transaction; either all rows update
|
||||
* or none do.
|
||||
*/
|
||||
async batchUpdatePriority(dto: BatchPriorityDto): Promise<{ count: number }> {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
for (const item of dto.items) {
|
||||
await tx.good.update({
|
||||
where: { id: BigInt(item.id) },
|
||||
data: { goodPriority: item.priority },
|
||||
});
|
||||
}
|
||||
return { count: dto.items.length };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates multiple goods atomically, sharing countryId/categoryId/tagIds/positionId
|
||||
* and a default priority that may be overridden per item.
|
||||
*/
|
||||
async batchCreate(dto: BatchCreateGoodDto): Promise<GoodDto[]> {
|
||||
const defaultPriority = dto.defaultPriority ?? 0;
|
||||
if (dto.tagIds && dto.tagIds.length > 0) {
|
||||
for (const tagId of dto.tagIds) {
|
||||
await this.ensureTag(tagId);
|
||||
}
|
||||
}
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const created: GoodDto[] = [];
|
||||
for (const item of dto.items) {
|
||||
const og = await tx.originGood.findUnique({
|
||||
where: { id: BigInt(item.originGoodId) },
|
||||
});
|
||||
if (!og) {
|
||||
throw new BadRequestException(
|
||||
`Origin good ${item.originGoodId} not found`,
|
||||
);
|
||||
}
|
||||
const row = await tx.good.create({
|
||||
data: {
|
||||
goodName: og.goodName ?? `Origin Good ${og.sdsGoodId}`,
|
||||
goodImage: og.goodImage,
|
||||
originGoodId: og.id,
|
||||
countryId: BigInt(dto.countryId),
|
||||
categoryId: BigInt(dto.categoryId),
|
||||
positionId: dto.positionId === undefined ? null : BigInt(dto.positionId),
|
||||
goodPriority: item.priority ?? defaultPriority,
|
||||
},
|
||||
});
|
||||
if (dto.tagIds && dto.tagIds.length > 0) {
|
||||
await tx.goodTag.createMany({
|
||||
data: dto.tagIds.map((tagId) => ({
|
||||
goodId: row.id,
|
||||
tagId: BigInt(tagId),
|
||||
})),
|
||||
});
|
||||
}
|
||||
const result = await tx.good.findUniqueOrThrow({
|
||||
where: { id: row.id },
|
||||
include: GOOD_INCLUDE,
|
||||
});
|
||||
created.push(GoodDto.from(result, {
|
||||
country: result.country,
|
||||
category: result.category,
|
||||
tag: result.tag,
|
||||
position: result.position,
|
||||
originGood: result.originGood,
|
||||
goodTags: result.goodTags,
|
||||
}));
|
||||
}
|
||||
return created;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the category tree and return the requested id + all of its
|
||||
* descendants. We use a level-by-level BFS to keep the queries small
|
||||
* for the typical tree sizes we expect.
|
||||
*/
|
||||
private async collectCategoryDescendants(rootId: bigint): Promise<bigint[]> {
|
||||
const ids: bigint[] = [rootId];
|
||||
let frontier: bigint[] = [rootId];
|
||||
while (frontier.length > 0) {
|
||||
const children = await this.prisma.category.findMany({
|
||||
where: { parentCategoryId: { in: frontier } },
|
||||
select: { id: true },
|
||||
});
|
||||
if (children.length === 0) break;
|
||||
const childIds = children.map((c) => c.id);
|
||||
ids.push(...childIds);
|
||||
frontier = childIds;
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private async ensureOriginGood(id: number) {
|
||||
const og = await this.prisma.originGood.findUnique({
|
||||
where: { id: BigInt(id) },
|
||||
});
|
||||
if (!og) throw new BadRequestException(`Origin good ${id} not found`);
|
||||
}
|
||||
|
||||
private async ensureCountry(id: number) {
|
||||
const c = await this.prisma.country.findUnique({ where: { id: BigInt(id) } });
|
||||
if (!c) throw new BadRequestException(`Country ${id} not found`);
|
||||
}
|
||||
|
||||
private async ensureCategory(id: number) {
|
||||
const c = await this.prisma.category.findUnique({ where: { id: BigInt(id) } });
|
||||
if (!c) throw new BadRequestException(`Category ${id} not found`);
|
||||
}
|
||||
|
||||
private async ensureTag(id: number) {
|
||||
const t = await this.prisma.tag.findUnique({ where: { id: BigInt(id) } });
|
||||
if (!t) throw new BadRequestException(`Tag ${id} not found`);
|
||||
}
|
||||
|
||||
private async ensureReferences(dto: CreateGoodDto) {
|
||||
await this.ensureOriginGood(dto.originGoodId);
|
||||
await this.ensureCountry(dto.countryId);
|
||||
await this.ensureCategory(dto.categoryId);
|
||||
if (dto.tagIds && dto.tagIds.length > 0) {
|
||||
for (const tagId of dto.tagIds) {
|
||||
await this.ensureTag(tagId);
|
||||
}
|
||||
}
|
||||
if (dto.positionId !== undefined) {
|
||||
const p = await this.prisma.position.findUnique({ where: { id: BigInt(dto.positionId) } });
|
||||
if (!p) throw new BadRequestException(`Position ${dto.positionId} not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user