feat(pl24): on-demand drill to resolve unseeded "bk. tablo:" references
When a reference's target illustration isn't seeded yet (load-time index miss → categoryId null), clicking it now calls a new resolve endpoint that drills the relevant main-group root (its external_id = the code's first digit; the illustration is a direct child) and re-resolves. One PL24 call in the common case, bounded + cached; falls back to pre-filled search if not found. UI shows a spinner on the button while drilling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1481,13 +1481,9 @@ export class CategoriesService {
|
||||
private static readonly REF_CODE_RE = /\d{3}-\d{3}/g;
|
||||
private static readonly REF_CODE_IN_NAME_RE = /\{(\d{3}-\d{3})\}/g;
|
||||
|
||||
private async resolvePartReferences<T extends { name: string; remark: string | null }>(
|
||||
rows: T[],
|
||||
vehicleId: string | null,
|
||||
): Promise<(T & { references?: Array<{ code: string; categoryId: string | null }> })[]> {
|
||||
const isRef = (p: T) => p.name?.includes("bk. tablo");
|
||||
if (!vehicleId || !rows.some(isRef)) return rows;
|
||||
|
||||
// Build a code→categoryId index for a vehicle from the `{NNN-NNN}` illustration
|
||||
// codes embedded in category names. Reflects only what's been seeded so far.
|
||||
private async buildReferenceIndex(vehicleId: string): Promise<Map<string, string>> {
|
||||
const cats = await this.db
|
||||
.select({ id: categories.id, name: categories.name })
|
||||
.from(categories)
|
||||
@@ -1499,6 +1495,17 @@ export class CategoriesService {
|
||||
if (!index.has(m[1])) index.set(m[1], c.id);
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
private async resolvePartReferences<T extends { name: string; remark: string | null }>(
|
||||
rows: T[],
|
||||
vehicleId: string | null,
|
||||
): Promise<(T & { references?: Array<{ code: string; categoryId: string | null }> })[]> {
|
||||
const isRef = (p: T) => p.name?.includes("bk. tablo");
|
||||
if (!vehicleId || !rows.some(isRef)) return rows;
|
||||
|
||||
const index = await this.buildReferenceIndex(vehicleId);
|
||||
|
||||
return rows.map((p) => {
|
||||
if (!isRef(p)) return p;
|
||||
@@ -1516,6 +1523,83 @@ export class CategoriesService {
|
||||
});
|
||||
}
|
||||
|
||||
// On-demand resolution of a single "bk. tablo:" code. The target illustration
|
||||
// lives as a direct (occasionally one-deeper) child of the main-group root
|
||||
// whose number is the code's first digit — but that branch may not be drilled
|
||||
// yet, so the load-time index missed it. Drill that root (one PL24 call seeds
|
||||
// the whole illustration list), then re-resolve. Bounded: root + its group
|
||||
// children only, capped, stopping on first hit. getChildren is DB-first, so
|
||||
// repeat clicks on the same code are cheap.
|
||||
private static readonly REF_DRILL_CALL_CAP = 25;
|
||||
|
||||
async resolveReferenceCode(
|
||||
vehicleId: string,
|
||||
rawCode: string,
|
||||
): Promise<{ code: string; categoryId: string | null }> {
|
||||
const code =
|
||||
(rawCode ?? "")
|
||||
.replace(/\\/g, "")
|
||||
.trim()
|
||||
.match(/\d{3}-\d{3}/)?.[0] ?? "";
|
||||
if (!code) return { code: rawCode, categoryId: null };
|
||||
|
||||
// Already seeded? cheap path.
|
||||
let index = await this.buildReferenceIndex(vehicleId);
|
||||
if (index.has(code)) return { code, categoryId: index.get(code) ?? null };
|
||||
|
||||
// Locate the main-group root: its external_id is the code's leading digit.
|
||||
const mainGroup = code[0];
|
||||
const roots = await this.db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(
|
||||
and(
|
||||
eq(categories.vehicleId, vehicleId),
|
||||
isNull(categories.parentId),
|
||||
eq(categories.externalId, mainGroup),
|
||||
),
|
||||
);
|
||||
if (roots.length === 0) return { code, categoryId: null };
|
||||
|
||||
let calls = 0;
|
||||
const drillAndCheck = async (categoryId: string): Promise<string | null> => {
|
||||
if (calls >= CategoriesService.REF_DRILL_CALL_CAP) return null;
|
||||
calls++;
|
||||
try {
|
||||
await this.getChildren(categoryId);
|
||||
} catch (err) {
|
||||
this.logger.warn(`[ref-drill] getChildren ${categoryId} failed: ${(err as Error).message}`);
|
||||
return null;
|
||||
}
|
||||
index = await this.buildReferenceIndex(vehicleId);
|
||||
return index.get(code) ?? null;
|
||||
};
|
||||
|
||||
for (const root of roots) {
|
||||
const hit = await drillAndCheck(root.id);
|
||||
if (hit) return { code, categoryId: hit };
|
||||
|
||||
// Not a direct child — descend one level into freshly-seeded group nodes.
|
||||
const subs = await this.db
|
||||
.select({
|
||||
id: categories.id,
|
||||
linkWid: categories.linkWid,
|
||||
hasSubgroups: categories.hasSubgroups,
|
||||
})
|
||||
.from(categories)
|
||||
.where(eq(categories.parentId, root.id));
|
||||
for (const sub of subs) {
|
||||
const isGroup = sub.hasSubgroups === true || sub.linkWid?.includes("Group");
|
||||
if (!isGroup) continue;
|
||||
const subHit = await drillAndCheck(sub.id);
|
||||
if (subHit) return { code, categoryId: subHit };
|
||||
if (calls >= CategoriesService.REF_DRILL_CALL_CAP) break;
|
||||
}
|
||||
}
|
||||
|
||||
return { code, categoryId: null };
|
||||
}
|
||||
|
||||
async getById(categoryId: string) {
|
||||
const [category] = await this.db
|
||||
.select()
|
||||
|
||||
@@ -94,6 +94,15 @@ export class VehiclesController {
|
||||
return this.categoriesService.getCategoryWithParts(categoryId);
|
||||
}
|
||||
|
||||
// On-demand resolution of a "bk. tablo:" cross-reference code whose target
|
||||
// illustration wasn't seeded at category-load time — drills the relevant
|
||||
// main group, then returns the resolved category (or null if not found).
|
||||
@Get(":vehicleId/references/resolve")
|
||||
async resolveReference(@Param("vehicleId") vehicleId: string, @Query("code") code: string) {
|
||||
await this.vehiclesService.getById(vehicleId);
|
||||
return this.categoriesService.resolveReferenceCode(vehicleId, code ?? "");
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
async getById(@Param("id") id: string) {
|
||||
return this.vehiclesService.getById(id);
|
||||
|
||||
Reference in New Issue
Block a user