feat: add Pcat catalog browsing — import pc dump, API endpoints, frontend routes
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled

Import parts-catalogs.com dump into pc schema (72 catalogs, 2788 models,
134K groups). Add pcat-catalog service/controller with Redis caching.
Create frontend routes for catalog → model → group navigation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 10:52:41 +00:00
parent 13bd112add
commit c61ad8c175
7 changed files with 976 additions and 494 deletions

View File

@@ -3,14 +3,16 @@ import { CatalogController } from "./catalog.controller";
import { CatalogService } from "./catalog.service";
import { EmexCatalogController } from "./emex-catalog.controller";
import { EmexCatalogService } from "./emex-catalog.service";
import { PcatCatalogController } from "./pcat-catalog.controller";
import { PcatCatalogService } from "./pcat-catalog.service";
import { PL24Module } from "../integrations/pl24/pl24.module";
import { SubscriptionsModule } from "../subscriptions/subscriptions.module";
import { StorageModule } from "../storage/storage.module";
@Module({
imports: [PL24Module, SubscriptionsModule, StorageModule],
controllers: [CatalogController, EmexCatalogController],
providers: [CatalogService, EmexCatalogService],
exports: [CatalogService, EmexCatalogService],
controllers: [CatalogController, EmexCatalogController, PcatCatalogController],
providers: [CatalogService, EmexCatalogService, PcatCatalogService],
exports: [CatalogService, EmexCatalogService, PcatCatalogService],
})
export class CatalogModule {}

View File

@@ -0,0 +1,25 @@
import { Controller, Get, Param, Query } from "@nestjs/common";
import { PcatCatalogService } from "./pcat-catalog.service";
@Controller("catalog/pcat")
export class PcatCatalogController {
constructor(private pcatCatalogService: PcatCatalogService) {}
@Get("catalogs")
getCatalogs() {
return this.pcatCatalogService.getCatalogs();
}
@Get("catalogs/:catalogId/models")
getModels(@Param("catalogId") catalogId: string) {
return this.pcatCatalogService.getModels(catalogId);
}
@Get("catalogs/:catalogId/groups")
getGroups(
@Param("catalogId") catalogId: string,
@Query("parentId") parentId?: string,
) {
return this.pcatCatalogService.getGroups(catalogId, parentId);
}
}

View File

@@ -0,0 +1,132 @@
import { Inject, Injectable, Logger } from "@nestjs/common";
import { sql } from "drizzle-orm";
import { DATABASE, Database } from "../database/database.provider";
import { RedisService } from "../redis/redis.service";
export interface PcatCatalogDto {
id: string;
name: string;
modelsCount: number;
}
export interface PcatModelDto {
id: string;
catalogId: string;
name: string;
imgUrl: string | null;
}
export interface PcatGroupDto {
id: string;
catalogId: string;
parentId: string | null;
name: string;
imgUrl: string | null;
hasSubgroups: boolean;
hasParts: boolean;
}
const CACHE_TTL = {
catalogs: 86400, // 24h
models: 43200, // 12h
groups: 21600, // 6h
};
@Injectable()
export class PcatCatalogService {
private readonly logger = new Logger(PcatCatalogService.name);
constructor(
@Inject(DATABASE) private db: Database,
private redis: RedisService,
) {}
async getCatalogs(): Promise<PcatCatalogDto[]> {
const cacheKey = "pcat:catalogs";
const cached = await this.redis.getJson<PcatCatalogDto[]>(cacheKey);
if (cached) return cached;
const rows = await this.db.execute<{
id: string;
name: string;
models_count: number;
}>(sql`SELECT id, name, models_count FROM pc.catalogs WHERE is_active = true ORDER BY name`);
const result: PcatCatalogDto[] = rows.map((r) => ({
id: r.id,
name: r.name,
modelsCount: r.models_count ?? 0,
}));
await this.redis.setJson(cacheKey, result, CACHE_TTL.catalogs);
return result;
}
async getModels(catalogId: string): Promise<PcatModelDto[]> {
const cacheKey = `pcat:models:${catalogId}`;
const cached = await this.redis.getJson<PcatModelDto[]>(cacheKey);
if (cached) return cached;
const rows = await this.db.execute<{
id: string;
catalog_id: string;
name: string;
img_url: string | null;
}>(
sql`SELECT id, catalog_id, name, img_url FROM pc.models WHERE catalog_id = ${catalogId} AND is_active = true ORDER BY name`,
);
const result: PcatModelDto[] = rows.map((r) => ({
id: r.id,
catalogId: r.catalog_id,
name: r.name,
imgUrl: r.img_url,
}));
await this.redis.setJson(cacheKey, result, CACHE_TTL.models);
return result;
}
async getGroups(catalogId: string, parentId?: string): Promise<PcatGroupDto[]> {
const cacheKey = `pcat:groups:${catalogId}:${parentId || "root"}`;
const cached = await this.redis.getJson<PcatGroupDto[]>(cacheKey);
if (cached) return cached;
const rows = parentId
? await this.db.execute<{
id: string;
catalog_id: string;
parent_id: string | null;
name: string;
img_url: string | null;
has_subgroups: boolean;
has_parts: boolean;
}>(
sql`SELECT id, catalog_id, parent_id, name, img_url, has_subgroups, has_parts FROM pc.groups WHERE catalog_id = ${catalogId} AND parent_id = ${parentId} ORDER BY sort_order, name`,
)
: await this.db.execute<{
id: string;
catalog_id: string;
parent_id: string | null;
name: string;
img_url: string | null;
has_subgroups: boolean;
has_parts: boolean;
}>(
sql`SELECT id, catalog_id, parent_id, name, img_url, has_subgroups, has_parts FROM pc.groups WHERE catalog_id = ${catalogId} AND parent_id IS NULL ORDER BY sort_order, name`,
);
const result: PcatGroupDto[] = rows.map((r) => ({
id: r.id,
catalogId: r.catalog_id,
parentId: r.parent_id,
name: r.name,
imgUrl: r.img_url,
hasSubgroups: r.has_subgroups ?? false,
hasParts: r.has_parts ?? false,
}));
await this.redis.setJson(cacheKey, result, CACHE_TTL.groups);
return result;
}
}