Jobber/orchestrator/src/server/services/resumeProjects.ts
Shaheer Sarfaraz 3da5ea35b4
Deduplicate shared helpers and enforce aliased imports (#228)
* Deduplicate string cleanup helpers and not-found responses

* Enforce aliased imports for infra and shared modules

* Enforce @client/@server aliases for deep relative imports

* Deduplicate visa sponsor and location filter definitions

* Use shared city filter export in extractor location checks
2026-02-22 16:13:52 +00:00

177 lines
5.2 KiB
TypeScript

import type {
ResumeProfile,
ResumeProjectCatalogItem,
ResumeProjectsSettings,
} from "@shared/types";
import { stripHtmlTags } from "@shared/utils/string";
type ResumeProjectSelectionItem = ResumeProjectCatalogItem & {
summaryText: string;
};
export function extractProjectsFromProfile(profile: ResumeProfile): {
catalog: ResumeProjectCatalogItem[];
selectionItems: ResumeProjectSelectionItem[];
} {
const items = profile?.sections?.projects?.items;
if (!Array.isArray(items)) return { catalog: [], selectionItems: [] };
const catalog: ResumeProjectCatalogItem[] = [];
const selectionItems: ResumeProjectSelectionItem[] = [];
for (const item of items) {
if (!item || typeof item !== "object") continue;
const id = item.id || "";
if (!id) continue;
const name = item.name || "";
const description = item.description || "";
const date = item.date || "";
const isVisibleInBase = Boolean(item.visible);
const summary = item.summary || "";
const summaryText = stripHtml(summary);
const base: ResumeProjectCatalogItem = {
id,
name,
description,
date,
isVisibleInBase,
};
catalog.push(base);
selectionItems.push({ ...base, summaryText });
}
return { catalog, selectionItems };
}
export function buildDefaultResumeProjectsSettings(
catalog: ResumeProjectCatalogItem[],
): ResumeProjectsSettings {
const lockedProjectIds = catalog
.filter((p) => p.isVisibleInBase)
.map((p) => p.id);
const lockedSet = new Set(lockedProjectIds);
const aiSelectableProjectIds = catalog
.map((p) => p.id)
.filter((id) => !lockedSet.has(id));
const total = catalog.length;
const preferredMax = Math.max(lockedProjectIds.length, 3);
const maxProjects = total === 0 ? 0 : Math.min(total, preferredMax);
return normalizeResumeProjectsSettings(
{ maxProjects, lockedProjectIds, aiSelectableProjectIds },
new Set(catalog.map((p) => p.id)),
);
}
export function parseResumeProjectsSettings(
raw: string | null,
): ResumeProjectsSettings | null {
if (!raw) return null;
try {
const parsed = JSON.parse(raw) as Partial<ResumeProjectsSettings>;
if (!parsed || typeof parsed !== "object") return null;
const maxProjects = parsed.maxProjects;
const lockedProjectIds = parsed.lockedProjectIds;
const aiSelectableProjectIds = parsed.aiSelectableProjectIds;
if (typeof maxProjects !== "number") return null;
if (
!Array.isArray(lockedProjectIds) ||
!Array.isArray(aiSelectableProjectIds)
)
return null;
if (!lockedProjectIds.every((v: unknown) => typeof v === "string"))
return null;
if (!aiSelectableProjectIds.every((v: unknown) => typeof v === "string"))
return null;
return {
maxProjects,
lockedProjectIds,
aiSelectableProjectIds,
};
} catch {
return null;
}
}
export function normalizeResumeProjectsSettings(
settings: ResumeProjectsSettings,
allowedProjectIds?: ReadonlySet<string>,
): ResumeProjectsSettings {
const allowed =
allowedProjectIds && allowedProjectIds.size > 0 ? allowedProjectIds : null;
const lockedProjectIds = uniqueStrings(settings.lockedProjectIds).filter(
(id) => (allowed ? allowed.has(id) : true),
);
const lockedSet = new Set(lockedProjectIds);
const aiSelectableProjectIds = uniqueStrings(settings.aiSelectableProjectIds)
.filter((id) => (allowed ? allowed.has(id) : true))
.filter((id) => !lockedSet.has(id));
const maxCap = allowed ? allowed.size : Number.POSITIVE_INFINITY;
const maxProjectsRaw = Number.isFinite(settings.maxProjects)
? settings.maxProjects
: 0;
const maxProjectsInt = Math.max(0, Math.floor(maxProjectsRaw));
const minRequired = lockedProjectIds.length;
const maxProjects = Math.min(maxCap, Math.max(minRequired, maxProjectsInt));
return { maxProjects, lockedProjectIds, aiSelectableProjectIds };
}
export function resolveResumeProjectsSettings(args: {
catalog: ResumeProjectCatalogItem[];
overrideRaw: string | null;
}): {
profileProjects: ResumeProjectCatalogItem[];
defaultResumeProjects: ResumeProjectsSettings;
overrideResumeProjects: ResumeProjectsSettings | null;
resumeProjects: ResumeProjectsSettings;
} {
const profileProjects = args.catalog;
const allowed = new Set(profileProjects.map((p) => p.id));
const defaultResumeProjects =
buildDefaultResumeProjectsSettings(profileProjects);
const overrideParsed = parseResumeProjectsSettings(args.overrideRaw);
const overrideResumeProjects = overrideParsed
? normalizeResumeProjectsSettings(overrideParsed, allowed)
: null;
const resumeProjects = overrideResumeProjects
? normalizeResumeProjectsSettings(overrideResumeProjects, allowed)
: defaultResumeProjects;
return {
profileProjects,
defaultResumeProjects,
overrideResumeProjects,
resumeProjects,
};
}
export function stripHtml(input: string): string {
return stripHtmlTags(input);
}
function uniqueStrings(values: string[]): string[] {
const seen = new Set<string>();
const out: string[] = [];
for (const value of values) {
const trimmed = value.trim();
if (!trimmed) continue;
if (seen.has(trimmed)) continue;
seen.add(trimmed);
out.push(trimmed);
}
return out;
}
export type { ResumeProjectSelectionItem };