Sifa professional network API (Fastify, AT Protocol, Jetstream)
sifa.id/
1import type { Database } from '../db/index.js';
2import { canonicalSkills, unresolvedSkills } from '../db/schema/index.js';
3import { eq, sql } from 'drizzle-orm';
4import { logger } from '../logger.js';
5
6/** Normalize a skill name for matching: lowercase, trim, collapse whitespace */
7export function normalizeSkillName(name: string): string {
8 return name.toLowerCase().trim().replace(/\s+/g, ' ');
9}
10
11/** Create a URL-safe slug from a skill name */
12export function createSlug(name: string): string {
13 return name
14 .toLowerCase()
15 .trim()
16 .replace(/c\+\+/gi, 'c-plus-plus')
17 .replace(/c#/gi, 'c-sharp')
18 .replace(/\.net/gi, 'dot-net')
19 .replace(/[^a-z0-9]+/g, '-')
20 .replace(/-+/g, '-')
21 .replace(/^-|-$/g, '');
22}
23
24/**
25 * Resolve a user-entered skill name to a canonical skill.
26 * Pipeline: normalize -> check slug match -> check aliases -> queue as unresolved.
27 * Returns the canonical skill row if matched, null if unresolved.
28 */
29export async function resolveSkill(
30 db: Database,
31 rawName: string,
32): Promise<typeof canonicalSkills.$inferSelect | null> {
33 const normalized = normalizeSkillName(rawName);
34
35 // 1. Exact match on slug
36 const bySlug = await db
37 .select()
38 .from(canonicalSkills)
39 .where(eq(canonicalSkills.slug, createSlug(rawName)))
40 .limit(1);
41 if (bySlug[0]) {
42 return bySlug[0];
43 }
44
45 // 2. Check aliases array (any canonical_skills row where normalized name is in aliases)
46 const byAlias = await db
47 .select()
48 .from(canonicalSkills)
49 .where(sql`${normalized} = ANY(${canonicalSkills.aliases})`)
50 .limit(1);
51 if (byAlias[0]) {
52 return byAlias[0];
53 }
54
55 // 3. No match -- add to unresolved queue
56 await db
57 .insert(unresolvedSkills)
58 .values({
59 rawName,
60 normalizedName: normalized,
61 })
62 .onConflictDoUpdate({
63 target: unresolvedSkills.normalizedName,
64 set: {
65 occurrences: sql`${unresolvedSkills.occurrences} + 1`,
66 },
67 });
68
69 logger.info({ rawName, normalized }, 'Skill queued as unresolved');
70 return null;
71}