@sparring/tech-roles-library
Version:
Comprehensive tech roles and competencies library for 78 technical roles with 9 career levels each. Includes detailed competencies and career progression paths with complete bilingual support (EN/ES).
358 lines (301 loc) • 11.6 kB
JavaScript
/**
* Query API - Core data access layer
*
* Provides the main API for querying roles, levels, and competencies.
* Includes support for accumulated competencies, career paths, and experience-based lookups.
*
* @module api/queries
* @author 686f6c61
* @license MIT
*/
const { Validator, RoleNotFoundError, LevelNotFoundError } = require('../core/validator');
class QueryAPI {
constructor(database, translator = null) {
this.db = database;
this.translator = translator;
}
getRoles() {
return this.db.getAllRoles();
}
getCategories() {
return this.db.getAllCategories();
}
getRoleByCode(code) {
const entry = this.db.getByCode(code);
if (!entry) {
throw new RoleNotFoundError(code);
}
return this.cloneEntry(entry);
}
getRoleByNameAndLevel(roleName, level) {
Validator.validateRoleName(roleName);
Validator.validateLevel(level);
const normalizedLevel = Validator.normalizeLevel(level);
const entries = this.db.getByRole(roleName);
if (entries.length === 0) {
throw new RoleNotFoundError(roleName);
}
const found = entries.find(e => e.level.includes(normalizedLevel));
if (!found) {
throw new LevelNotFoundError(roleName, level);
}
return this.cloneEntry(found);
}
getAllLevelsForRole(roleName) {
Validator.validateRoleName(roleName);
const entries = this.db.getByRole(roleName);
if (entries.length === 0) {
throw new RoleNotFoundError(roleName);
}
return entries.map(e => this.cloneEntry(e));
}
getCompetencies(roleName, level, options = {}) {
const entry = this.getRoleByNameAndLevel(roleName, level);
const result = {
role: entry.role,
level: entry.level,
code: entry.code,
yearsRange: entry.yearsRange,
core: entry.coreCompetencies
};
if (options.includeComplementary !== false) {
result.complementary = entry.complementaryCompetencies;
}
if (options.includeIndicators !== false) {
result.indicators = entry.indicators;
}
return result;
}
getAccumulatedCompetencies(roleName, targetLevel) {
Validator.validateRoleName(roleName);
Validator.validateLevel(targetLevel);
const allLevels = this.getAllLevelsForRole(roleName);
const normalizedLevel = Validator.normalizeLevel(targetLevel);
const targetLevelNum = parseInt(normalizedLevel.match(/\d/)[0]);
const accumulated = {
role: roleName,
targetLevel: normalizedLevel,
levels: []
};
for (let i = 1; i <= targetLevelNum; i++) {
const level = allLevels.find(l => l.levelNumber === i);
if (level) {
accumulated.levels.push({
level: level.level,
code: level.code,
yearsRange: level.yearsRange,
coreCompetencies: [...level.coreCompetencies],
complementaryCompetencies: [...level.complementaryCompetencies],
indicators: [...level.indicators]
});
}
}
return accumulated;
}
getByExperience(roleName, years) {
Validator.validateRoleName(roleName);
if (typeof years !== 'number' || years < 0) {
throw new Error('Years must be a positive number');
}
const allLevels = this.getAllLevelsForRole(roleName);
for (const level of allLevels) {
const range = level.yearsRange;
if (range.max === null && years >= range.min) {
return this.cloneEntry(level);
}
if (years >= range.min && years <= range.max) {
return this.cloneEntry(level);
}
}
return this.cloneEntry(allLevels[allLevels.length - 1]);
}
/**
* Get complete career path view (mastered + current + growth)
* Perfect for HR, career planning, and skill gap analysis
*/
getCareerPathComplete(roleName, currentLevel) {
Validator.validateRoleName(roleName);
Validator.validateLevel(currentLevel);
const allLevels = this.getAllLevelsForRole(roleName);
const normalizedLevel = Validator.normalizeLevel(currentLevel);
const currentLevelNum = parseInt(normalizedLevel.match(/\d/)[0]);
// Find current level details
const currentLevelData = allLevels.find(l => l.levelNumber === currentLevelNum);
if (!currentLevelData) {
throw new Error(`Level ${currentLevel} not found for role ${roleName}`);
}
// Split into mastered (L1 to L(n-1)), current (Ln), and growth (L(n+1) to L9)
const masteredLevels = [];
const growthPath = [];
allLevels.forEach(level => {
if (level.levelNumber < currentLevelNum) {
masteredLevels.push({
level: level.level,
code: level.code,
levelNumber: level.levelNumber,
yearsRange: level.yearsRange,
coreCompetencies: [...level.coreCompetencies],
complementaryCompetencies: [...level.complementaryCompetencies],
indicators: [...level.indicators]
});
} else if (level.levelNumber > currentLevelNum) {
growthPath.push({
level: level.level,
code: level.code,
levelNumber: level.levelNumber,
yearsRange: level.yearsRange,
coreCompetencies: [...level.coreCompetencies],
complementaryCompetencies: [...level.complementaryCompetencies],
indicators: [...level.indicators]
});
}
});
// Calculate statistics
const countCompetencies = (levels) => {
return levels.reduce((acc, level) => ({
core: acc.core + level.coreCompetencies.length,
complementary: acc.complementary + level.complementaryCompetencies.length,
indicators: acc.indicators + level.indicators.length
}), { core: 0, complementary: 0, indicators: 0 });
};
const masteredStats = countCompetencies(masteredLevels);
const currentStats = {
core: currentLevelData.coreCompetencies.length,
complementary: currentLevelData.complementaryCompetencies.length,
indicators: currentLevelData.indicators.length
};
const growthStats = countCompetencies(growthPath);
const totalMastered = masteredStats.core + masteredStats.complementary + masteredStats.indicators;
const totalCurrent = currentStats.core + currentStats.complementary + currentStats.indicators;
const totalRemaining = growthStats.core + growthStats.complementary + growthStats.indicators;
const totalAll = totalMastered + totalCurrent + totalRemaining;
return {
role: currentLevelData.role,
currentLevel: {
level: currentLevelData.level,
code: currentLevelData.code,
levelNumber: currentLevelData.levelNumber,
yearsRange: currentLevelData.yearsRange,
coreCompetencies: [...currentLevelData.coreCompetencies],
complementaryCompetencies: [...currentLevelData.complementaryCompetencies],
indicators: [...currentLevelData.indicators]
},
masteredLevels: masteredLevels,
growthPath: growthPath,
summary: {
totalMasteredCompetencies: totalMastered,
currentLevelCompetencies: totalCurrent,
remainingToLearn: totalRemaining,
progressPercentage: totalAll > 0 ? Math.round((totalMastered + totalCurrent) / totalAll * 100) : 0,
masteredStats: masteredStats,
currentStats: currentStats,
growthStats: growthStats
}
};
}
filterByCategory(category) {
const entries = this.db.getByCategory(category);
return entries.map(e => this.cloneEntry(e));
}
filterByLevel(levelNumber) {
if (typeof levelNumber !== 'number' || levelNumber < 1 || levelNumber > 9) {
throw new Error('Level number must be between 1 and 9');
}
const entries = this.db.indexes.byLevelNumber.get(levelNumber) || [];
return entries.map(e => this.cloneEntry(e));
}
/**
* Get all roles with complete metadata
* Returns a comprehensive catalog of all 78 roles with level counts and categories
* Perfect for displaying role catalogs, navigation, and overview dashboards
*/
getAllRolesWithMetadata() {
const allRoleNames = this.db.getAllRoles();
const categories = this.db.getAllCategories();
const rolesWithMetadata = allRoleNames.map(roleName => {
const levels = this.db.getByRole(roleName);
if (levels.length === 0) {
return null;
}
// Get the first entry for basic metadata
const firstLevel = levels[0];
// Calculate total competencies across all levels
let totalCoreCompetencies = 0;
let totalComplementaryCompetencies = 0;
let totalIndicators = 0;
levels.forEach(level => {
totalCoreCompetencies += level.coreCompetencies.length;
totalComplementaryCompetencies += level.complementaryCompetencies.length;
totalIndicators += level.indicators.length;
});
// Get available levels and their codes
const availableLevels = levels.map(level => ({
level: level.level,
code: level.code,
levelNumber: level.levelNumber,
yearsRange: { ...level.yearsRange },
competenciesCount: level.coreCompetencies.length + level.complementaryCompetencies.length,
indicatorsCount: level.indicators.length
})).sort((a, b) => a.levelNumber - b.levelNumber);
// Calculate years range across all levels
const minYears = Math.min(...levels.map(l => l.yearsRange.min));
const maxYears = Math.max(...levels.map(l => l.yearsRange.max === null ? 99 : l.yearsRange.max));
// Translate role name if translator is available
const translatedRoleName = this.translator
? this.translator.translateRoleName(roleName)
: roleName;
return {
role: translatedRoleName,
originalRole: roleName, // Keep original for queries
category: firstLevel.category,
availableLevels: availableLevels,
levelCount: levels.length,
yearsRange: {
min: minYears,
max: maxYears === 99 ? null : maxYears
},
statistics: {
totalCoreCompetencies: totalCoreCompetencies,
totalComplementaryCompetencies: totalComplementaryCompetencies,
totalIndicators: totalIndicators,
totalCompetencies: totalCoreCompetencies + totalComplementaryCompetencies + totalIndicators,
avgCompetenciesPerLevel: Math.round((totalCoreCompetencies + totalComplementaryCompetencies) / levels.length)
}
};
}).filter(role => role !== null);
// Group by category
const byCategory = {};
categories.forEach(category => {
byCategory[category] = rolesWithMetadata.filter(r => r.category === category);
});
return {
roles: rolesWithMetadata.sort((a, b) => a.originalRole.localeCompare(b.originalRole)),
byCategory: byCategory,
summary: {
totalRoles: rolesWithMetadata.length,
totalCategories: categories.length,
categories: categories,
totalLevels: rolesWithMetadata.reduce((sum, role) => sum + role.levelCount, 0)
}
};
}
cloneEntry(entry) {
const cloned = {
category: entry.category,
role: entry.role,
level: entry.level,
code: entry.code,
levelNumber: entry.levelNumber,
yearsRange: { ...entry.yearsRange },
coreCompetencies: [...entry.coreCompetencies],
complementaryCompetencies: [...entry.complementaryCompetencies],
indicators: [...entry.indicators]
};
// Translate if translator is available
if (this.translator) {
return this.translator.translate(cloned);
}
return cloned;
}
}
module.exports = QueryAPI;