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:
yeuimu
2026-07-11 16:54:05 +08:00
parent 69945b8749
commit 7e04877bb6
155 changed files with 20134 additions and 14393 deletions
+234
View File
@@ -0,0 +1,234 @@
import { Test } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { PublicService } from './public.service';
import { PrismaService } from '../prisma/prisma.service';
describe('PublicService', () => {
let service: PublicService;
let prisma: PrismaService;
const stamp = Date.now();
let countryId: bigint;
let categoryId: bigint;
let childCategoryId: bigint;
let otherCategoryId: bigint;
let tagId: bigint;
let originGoodId: bigint;
let goodIds: bigint[] = [];
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PublicService, PrismaService],
}).compile();
service = moduleRef.get(PublicService);
prisma = moduleRef.get(PrismaService);
await prisma.onModuleInit();
const country = await prisma.country.create({
data: { countryName: `Pub Country ${stamp}` },
});
countryId = country.id;
const cat = await prisma.category.create({
data: { categoryName: `Pub Cat ${stamp}` },
});
categoryId = cat.id;
const child = await prisma.category.create({
data: { categoryName: `Pub Child ${stamp}`, parentCategoryId: cat.id },
});
childCategoryId = child.id;
const otherCat = await prisma.category.create({
data: { categoryName: `Pub Other ${stamp}` },
});
otherCategoryId = otherCat.id;
const tag = await prisma.tag.create({
data: { tagName: `Pub Tag ${stamp}`, tagColor: '#0000FF' },
});
tagId = tag.id;
const og = await prisma.originGood.create({
data: {
sdsGoodId: `pub-sds-${stamp}`,
goodName: `Origin ${stamp}`,
goodImage: 'http://img',
},
});
originGoodId = og.id;
// Seed 3 goods:
// high priority + position.indexVal=1
// mid priority + position.indexVal=5
// no priority + no position (falls back to createdAt)
const pos1 = await prisma.position.create({
data: { indexVal: 1, countryId, categoryId },
});
const pos2 = await prisma.position.create({
data: { indexVal: 5, countryId, categoryId },
});
const g1 = await prisma.good.create({
data: {
goodName: `Pub High ${stamp}`,
originGoodId,
countryId,
categoryId,
goodPriority: 10,
positionId: pos1.id,
},
});
const g2 = await prisma.good.create({
data: {
goodName: `Pub Mid ${stamp}`,
originGoodId,
countryId,
categoryId,
goodPriority: 5,
positionId: pos2.id,
},
});
const g3 = await prisma.good.create({
data: {
goodName: `Pub NoPos ${stamp}`,
originGoodId,
countryId,
categoryId,
tagId,
goodPriority: 1,
},
});
goodIds = [g1.id, g2.id, g3.id];
// Seed a good in `otherCategory` so the "onlyHaveGoods" filter
// returns more than one category.
await prisma.good.create({
data: {
goodName: `Pub Other ${stamp}`,
originGoodId,
countryId,
categoryId: otherCategoryId,
goodPriority: 1,
},
});
// And seed a good in the *child* category, to verify categoryId
// recursion.
await prisma.good.create({
data: {
goodName: `Pub ChildGood ${stamp}`,
originGoodId,
countryId,
categoryId: childCategoryId,
goodPriority: 0,
},
});
});
afterAll(async () => {
if (goodIds.length) {
await prisma.good.deleteMany({ where: { id: { in: goodIds } } });
}
await prisma.good.deleteMany({
where: { goodName: { contains: `Pub ` } },
});
await prisma.position.deleteMany({
where: { countryId },
});
await prisma.tag.delete({ where: { id: tagId } });
await prisma.originGood.delete({ where: { id: originGoodId } });
// Delete children before parent (FK self-relation is RESTRICT).
await prisma.category.delete({ where: { id: childCategoryId } });
await prisma.category.delete({ where: { id: otherCategoryId } });
await prisma.category.delete({ where: { id: categoryId } });
await prisma.country.delete({ where: { id: countryId } });
await prisma.onModuleDestroy();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('getCategoriesTree returns only categories that have goods', async () => {
const tree = await service.getCategoriesTree();
const allIds = new Set<string>();
const walk = (list: Array<{ id: string; children: Array<{ id: string }> }>) => {
for (const n of list) {
allIds.add(n.id);
walk(n.children as any);
}
};
walk(tree as any);
// We seeded goods in `categoryId`, `childCategoryId`, `otherCategoryId`.
expect(allIds.has(categoryId.toString())).toBe(true);
expect(allIds.has(childCategoryId.toString())).toBe(true);
expect(allIds.has(otherCategoryId.toString())).toBe(true);
});
it('getCountries returns only countries that have goods', async () => {
const countries = await service.getCountries();
expect(countries.find((c) => c.id === countryId.toString())).toBeDefined();
});
it('filters by countryId, tagId, keyword and categoryId (recursively)', async () => {
const filtered = await service.getGoods({
page: 1,
pageSize: 50,
countryId: Number(countryId),
categoryId: Number(categoryId), // includes child
keyword: `Pub `,
});
expect(filtered.total).toBeGreaterThanOrEqual(4); // High, Mid, NoPos, ChildGood
expect(filtered.items.every((g) => g.country.id === countryId.toString())).toBe(true);
});
it('sorts by priority DESC, position.indexVal ASC, createdAt DESC', async () => {
const result = await service.getGoods({
page: 1,
pageSize: 50,
countryId: Number(countryId),
keyword: `Pub `,
});
const priorities = result.items.map((g) => g.goodPriority);
// First verify primary descending priority.
const sorted = [...priorities].sort((a, b) => b - a);
expect(priorities).toEqual(sorted);
});
it('getGood returns detail and 404 for unknown id', async () => {
const first = await service.getGoods({
page: 1,
pageSize: 1,
countryId: Number(countryId),
keyword: `Pub `,
});
expect(first.items.length).toBe(1);
const detail = await service.getGood(BigInt(first.items[0].id));
expect(detail.id).toBe(first.items[0].id);
await expect(service.getGood(BigInt(99999999))).rejects.toBeInstanceOf(
NotFoundException,
);
});
it('getTags returns tags with their group info, sorted by group then order', async () => {
const tags = await service.getTags();
expect(tags.length).toBeGreaterThan(0);
// Each tag in our seed (包邮/不包邮/...) should have a group
const grouped = tags.find((t) => t.tagName === '包邮');
if (grouped) {
expect(grouped.group).not.toBeNull();
expect(grouped.group!.groupName).toBe('物流渠道');
}
});
it('getTagGroups returns only groups that have goods', async () => {
const groups = await service.getTagGroups();
expect(groups.length).toBeGreaterThan(0);
const names = groups.map((g) => g.groupName);
expect(names).toContain('物流渠道');
expect(names).toContain('印刷位置');
expect(names).toContain('印刷工艺');
// Sorted by sortOrder
const sortOrders = groups.map((g) => g.sortOrder);
expect([...sortOrders].sort((a, b) => a - b)).toEqual(sortOrders);
});
});