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
+178
View File
@@ -0,0 +1,178 @@
import { Test } from '@nestjs/testing';
import { ConfigModule } from '@nestjs/config';
import { SyncService } from './sync.service';
import { SdsClientService } from './sds-client.service';
import { PrismaService } from '../prisma/prisma.service';
describe('SyncService', () => {
let service: SyncService;
let sds: jest.Mocked<SdsClientService>;
let prisma: PrismaService;
const createdSdsCategoryIds: string[] = [];
const createdSdsGoodIds: string[] = [];
beforeAll(async () => {
const sdsMock: Partial<SdsClientService> = {
fetchCategoryTree: jest.fn(),
fetchProductsPage: jest.fn(),
};
const moduleRef = await Test.createTestingModule({
imports: [ConfigModule.forRoot({ isGlobal: true })],
providers: [
SyncService,
{ provide: SdsClientService, useValue: sdsMock },
PrismaService,
],
}).compile();
service = moduleRef.get(SyncService);
sds = moduleRef.get(SdsClientService) as jest.Mocked<SdsClientService>;
prisma = moduleRef.get(PrismaService);
await prisma.onModuleInit();
});
afterAll(async () => {
if (createdSdsCategoryIds.length) {
await prisma.category.deleteMany({
where: { sdsCategoryId: { in: createdSdsCategoryIds } },
});
}
if (createdSdsGoodIds.length) {
await prisma.originGood.deleteMany({
where: { sdsGoodId: { in: createdSdsGoodIds } },
});
}
await prisma.onModuleDestroy();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('flattenCategoryTree', () => {
it('flattens a nested SDS tree and preserves parent linkage', () => {
const tree = [
{
id: 1,
name: 'Root',
children: [
{ id: 11, name: 'Child A' },
{ id: 12, name: 'Child B', children: [{ id: 121, name: 'Leaf' }] },
],
},
];
const flat = service.flattenCategoryTree(tree);
expect(flat).toHaveLength(4);
const byId = Object.fromEntries(flat.map((n) => [n.sdsId, n]));
expect(byId['1'].name).toBe('Root');
expect(byId['1'].parentSdsId).toBeUndefined();
expect(byId['11'].parentSdsId).toBe('1');
expect(byId['12'].parentSdsId).toBe('1');
expect(byId['121'].parentSdsId).toBe('12');
});
it('skips nodes without a usable id', () => {
const flat = service.flattenCategoryTree([{ id: null, name: 'no-id' }]);
expect(flat).toHaveLength(0);
});
});
describe('syncCategories', () => {
it('inserts + updates rows and links parents', async () => {
const stamp = Date.now();
sds.fetchCategoryTree.mockResolvedValueOnce([
{
id: `r-${stamp}`,
name: `Root ${stamp}`,
children: [{ id: `c-${stamp}`, name: `Child ${stamp}` }],
},
]);
const result = await service.syncCategories();
expect(result.total).toBe(2);
expect(result.inserted).toBe(2);
expect(result.updated).toBe(0);
createdSdsCategoryIds.push(`r-${stamp}`, `c-${stamp}`);
const child = await prisma.category.findUnique({
where: { sdsCategoryId: `c-${stamp}` },
});
const root = await prisma.category.findUnique({
where: { sdsCategoryId: `r-${stamp}` },
});
expect(child?.parentCategoryId).toBe(root?.id);
});
it('marks SyncLog SUCCESS', async () => {
const logs = await prisma.syncLog.findMany({
where: { type: 'CATEGORIES' },
orderBy: { startedAt: 'desc' },
take: 1,
});
expect(logs[0]?.status).toBe('SUCCESS');
});
});
describe('syncProducts', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('upserts origin goods by sdsGoodId and updates on re-run', async () => {
// Seed a leaf category with sdsCategoryId so the sync has work.
const stamp = Date.now();
const leaf = await prisma.category.create({
data: {
sdsCategoryId: `leaf-${stamp}`,
categoryName: `Leaf ${stamp}`,
},
});
createdSdsCategoryIds.push(`leaf-${stamp}`);
// Default mock returns a single page with 2 products, then
// breaks the loop because content.length < 50.
sds.fetchProductsPage.mockImplementation(async (categoryId) => {
if (categoryId === `leaf-${stamp}`) {
return {
content: [
{ id: `p-${stamp}-1`, name: 'Product 1', price: 12.5, pic: 'http://x' },
{ id: `p-${stamp}-2`, name: 'Product 2', price: '99.00' },
],
};
}
// For any other (already-existing) category, return empty
// so the loop terminates immediately.
return { content: [] };
});
const result1 = await service.syncProducts();
expect(result1.inserted).toBeGreaterThanOrEqual(2);
createdSdsGoodIds.push(`p-${stamp}-1`, `p-${stamp}-2`);
// Re-run with updated name -> should be `updated`, not `inserted`.
sds.fetchProductsPage.mockImplementation(async (categoryId) => {
if (categoryId === `leaf-${stamp}`) {
return {
content: [
{ id: `p-${stamp}-1`, name: 'Product 1 renamed' },
],
};
}
return { content: [] };
});
const result2 = await service.syncProducts();
expect(result2.updated).toBeGreaterThanOrEqual(1);
const row = await prisma.originGood.findUnique({
where: { sdsGoodId: `p-${stamp}-1` },
});
expect(row?.goodName).toBe('Product 1 renamed');
});
});
describe('getStatus', () => {
it('returns recent logs ordered by startedAt desc', async () => {
const logs = await service.getStatus(5);
expect(Array.isArray(logs)).toBe(true);
expect(logs.length).toBeGreaterThan(0);
});
});
});