From 9d8cbcff5ab067370d6db548d55a3e219f76d85c Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Thu, 4 Dec 2025 10:21:03 +0100 Subject: [PATCH] refactor: use cert as key (#64293) --- api/src/routes/protected/certificate.ts | 4 +- api/src/routes/public/certificate.ts | 9 ++- client/src/redux/selectors.js | 15 +---- shared/config/certification-settings.test.ts | 15 ++++- shared/config/certification-settings.ts | 62 ++++++++++++-------- shared/config/challenge-types.ts | 4 +- 6 files changed, 64 insertions(+), 45 deletions(-) diff --git a/api/src/routes/protected/certificate.ts b/api/src/routes/protected/certificate.ts index ad1d8aab8ea..538de1c2200 100644 --- a/api/src/routes/protected/certificate.ts +++ b/api/src/routes/protected/certificate.ts @@ -6,7 +6,7 @@ import { getChallenges } from '../../utils/get-challenges.js'; import { certIds, certSlugTypeMap, - certTypeTitleMap, + certToTitleMap, certTypes, currentCertifications, legacyCertifications, @@ -298,7 +298,7 @@ export const protectedCertificateRoutes: FastifyPluginCallbackTypebox = ( } const certType = certSlugTypeMap[certSlug]; - const certName = certTypeTitleMap[certType]; + const certName = certToTitleMap[certSlug]; const user = await fastify.prisma.user.findUnique({ where: { id: req.user?.id } diff --git a/api/src/routes/public/certificate.ts b/api/src/routes/public/certificate.ts index ebca823245c..a3dfc10979e 100644 --- a/api/src/routes/public/certificate.ts +++ b/api/src/routes/public/certificate.ts @@ -4,7 +4,7 @@ import { find } from 'lodash-es'; import * as schemas from '../../schemas.js'; import { certSlugTypeMap, - certTypeTitleMap, + certToTitleMap, certTypeIdMap, completionHours, oldDataVizId @@ -53,7 +53,7 @@ export const unprotectedCertificateRoutes: FastifyPluginCallbackTypebox = ( const certType = certSlugTypeMap[certSlug]; const certId = certTypeIdMap[certType]; - const certTitle = certTypeTitleMap[certType]; + const certTitle = certToTitleMap[certSlug]; const completionTime = completionHours[certType] || 300; const user = await fastify.prisma.user.findFirst({ where: { username }, @@ -180,16 +180,15 @@ export const unprotectedCertificateRoutes: FastifyPluginCallbackTypebox = ( } if (!user[certType]) { - const cert = certTypeTitleMap[certType]; logger.info( - `User ${username} has not completed the ${cert} certification.` + `User ${username} has not completed the ${certTitle} certification.` ); return reply.send({ messages: [ { type: 'info', message: 'flash.user-not-certified', - variables: { username, cert } + variables: { username, cert: certTitle } } ] }); diff --git a/client/src/redux/selectors.js b/client/src/redux/selectors.js index 8e249a2c4e7..76c5832935e 100644 --- a/client/src/redux/selectors.js +++ b/client/src/redux/selectors.js @@ -2,7 +2,7 @@ import { createSelector } from 'reselect'; import { liveCerts } from '../../config/cert-and-project-map'; import { certTypeIdMap, - certTypeTitleMap + certToTitleMap } from '../../../shared-dist/config/certification-settings.js'; import { randomBetween } from '../utils/random-between'; @@ -238,18 +238,10 @@ export const claimableCertsSelector = createSelector([userSelector], user => { }, {} ); - // Invert certTypeIdMap ({[userFlag]: certId} => {[certId]: userFlag}) to get certType from id - const invertedCertTypeIdMap = Object.entries(certTypeIdMap).reduce( - (acc, [userFlag, certId]) => { - acc[certId] = userFlag; - return acc; - }, - {} - ); const claimable = []; - for (const { id, projects } of liveCerts) { + for (const { id, projects, certSlug } of liveCerts) { if (!projects) continue; if (isClaimedById[id]) continue; @@ -258,8 +250,7 @@ export const claimableCertsSelector = createSelector([userSelector], user => { completedChallengeIds.includes(id) ); - const certType = invertedCertTypeIdMap[id]; - const certTitle = certTypeTitleMap[certType]; + const certTitle = certToTitleMap[certSlug]; if (allProjectsComplete) { claimable.push({ certTitle diff --git a/shared/config/certification-settings.test.ts b/shared/config/certification-settings.test.ts index c2db72d3069..132840506a2 100644 --- a/shared/config/certification-settings.test.ts +++ b/shared/config/certification-settings.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { Certification, linkedInCredentialIds } from './certification-settings'; +import { + Certification, + linkedInCredentialIds, + certToTitleMap +} from './certification-settings'; describe('linkedInCredentialIds', () => { it('should contain a value for all certifications', () => { @@ -9,3 +13,12 @@ describe('linkedInCredentialIds', () => { expect(linkedInCredentialIdsKeys).toEqual(allCertifications); }); }); + +describe('certToTitleMap', () => { + it('should not contain duplicate titles', () => { + const titles = Object.values(certToTitleMap); + const uniqueTitles = Array.from(new Set(titles)); + + expect(titles.length).toBe(uniqueTitles.length); + }); +}); diff --git a/shared/config/certification-settings.ts b/shared/config/certification-settings.ts index ea908c0ff23..07c7b90f1a5 100644 --- a/shared/config/certification-settings.ts +++ b/shared/config/certification-settings.ts @@ -269,31 +269,47 @@ export const certTypeIdMap = { [certTypes.a2English]: certIds.a2EnglishId }; -export const certTypeTitleMap = { - [certTypes.frontEnd]: 'Legacy Front End', - [certTypes.backEnd]: 'Legacy Back End', - [certTypes.dataVis]: 'Legacy Data Visualization', - [certTypes.infosecQa]: 'Legacy Information Security and Quality Assurance', - [certTypes.fullStack]: 'Legacy Full Stack', - [certTypes.respWebDesign]: 'Legacy Responsive Web Design V8', - [certTypes.respWebDesignV9]: 'Responsive Web Design', - [certTypes.frontEndDevLibs]: 'Front End Development Libraries V8', - [certTypes.jsAlgoDataStruct]: +// TODO: use i18n keys instead of hardcoded titles +export const certToTitleMap: Record = { + // Legacy certifications + [Certification.LegacyFrontEnd]: 'Legacy Front End', + [Certification.JsAlgoDataStruct]: 'Legacy JavaScript Algorithms and Data Structures V7', - [certTypes.dataVis2018]: 'Data Visualization V8', - [certTypes.apisMicroservices]: 'Back End Development and APIs V8', - [certTypes.qaV7]: 'Quality Assurance', - [certTypes.infosecV7]: 'Information Security', - [certTypes.sciCompPyV7]: 'Scientific Computing with Python', - [certTypes.dataAnalysisPyV7]: 'Data Analysis with Python', - [certTypes.machineLearningPyV7]: 'Machine Learning with Python', - [certTypes.relationalDatabaseV8]: 'Relational Database V8', - [certTypes.collegeAlgebraPyV8]: 'College Algebra with Python', - [certTypes.foundationalCSharpV8]: 'Foundational C# with Microsoft', - [certTypes.jsAlgoDataStructV8]: + [Certification.LegacyBackEnd]: 'Legacy Back End', + [Certification.LegacyDataVis]: 'Legacy Data Visualization', + [Certification.LegacyInfoSecQa]: + 'Legacy Information Security and Quality Assurance', + [Certification.LegacyFullStack]: 'Legacy Full Stack', + + // Current certifications + [Certification.RespWebDesign]: 'Legacy Responsive Web Design V8', + [Certification.JsAlgoDataStructNew]: 'Legacy JavaScript Algorithms and Data Structures V8', - [certTypes.javascriptV9]: 'JavaScript', - [certTypes.a2English]: 'A2 English for Developers' + [Certification.FrontEndDevLibs]: 'Front End Development Libraries V8', + [Certification.DataVis]: 'Data Visualization V8', + [Certification.BackEndDevApis]: 'Back End Development and APIs V8', + [Certification.QualityAssurance]: 'Quality Assurance', + [Certification.InfoSec]: 'Information Security', + [Certification.SciCompPy]: 'Scientific Computing with Python', + [Certification.DataAnalysisPy]: 'Data Analysis with Python', + [Certification.MachineLearningPy]: 'Machine Learning with Python', + [Certification.RelationalDb]: 'Relational Database V8', + [Certification.CollegeAlgebraPy]: 'College Algebra with Python', + [Certification.FoundationalCSharp]: 'Foundational C# with Microsoft', + [Certification.A2English]: 'A2 English for Developers', + + // Upcoming certifications + [Certification.RespWebDesignV9]: 'Responsive Web Design', + [Certification.JsV9]: 'JavaScript', + [Certification.FrontEndDevLibsV9]: 'Front End Development Libraries', + [Certification.PythonV9]: 'Python', + [Certification.RelationalDbV9]: 'Relational Database', + [Certification.BackEndDevApisV9]: 'Back End Development and APIs', + [Certification.FullStackDeveloperV9]: 'Full Stack Developer', + [Certification.B1English]: 'B1 English for Developers', + [Certification.A2Spanish]: 'A2 Professional Spanish', + [Certification.A2Chinese]: 'A2 Professional Chinese', + [Certification.A1Chinese]: 'A1 Professional Chinese' }; export const superBlockToCertMap: { diff --git a/shared/config/challenge-types.ts b/shared/config/challenge-types.ts index b7cec06a181..634acf7aa65 100644 --- a/shared/config/challenge-types.ts +++ b/shared/config/challenge-types.ts @@ -174,7 +174,7 @@ export const canSaveToDB = (challengeType: number): boolean => challengeType === challengeTypes.multifileCertProject || challengeType === challengeTypes.multifilePythonCertProject; -export const dailyCodingChallengeTypes = [ +const dailyCodingChallengeTypes = [ challengeTypes.dailyChallengeJs, challengeTypes.dailyChallengePy ]; @@ -182,7 +182,7 @@ export const dailyCodingChallengeTypes = [ export const getIsDailyCodingChallenge = (challengeType: number): boolean => dailyCodingChallengeTypes.includes(challengeType); -export const dailyCodingChallengeLanguages = { +const dailyCodingChallengeLanguages = { [challengeTypes.dailyChallengeJs]: 'javascript', [challengeTypes.dailyChallengePy]: 'python' };