freeCodeCamp/api-server/server/utils/map.js

232 lines
6.4 KiB
JavaScript

import _ from 'lodash';
import { Observable } from 'rx';
import { unDasherize, nameify } from '../../../utils/slugs';
import {
addNameIdMap as _addNameIdToMap,
checkMapData,
getFirstChallenge as _getFirstChallenge
} from '../../common/utils/map.js';
const isDev = process.env.FREECODECAMP_NODE_ENV !== 'production';
const isBeta = !!process.env.BETA;
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
const addNameIdMap = _.once(_addNameIdToMap);
const getFirstChallenge = _.once(_getFirstChallenge);
/*
* interface ChallengeMap {
* result: {
* superBlocks: [ ...superBlockDashedName: String ]
* },
* entities: {
* superBlock: {
* [ ...superBlockDashedName ]: SuperBlock
* },
* block: {
* [ ...blockDashedNameg ]: Block,
* challenge: {
* [ ...challengeDashedNameg ]: Challenge
* }
* }
* }
*/
export function _cachedMap({ Block, Challenge }) {
const challenges = Challenge.find$({
order: ['order ASC', 'suborder ASC'],
where: { isPrivate: false }
});
const challengeMap = challenges.map(challenges =>
challenges
.map(challenge => challenge.toJSON())
.reduce((hash, challenge) => {
hash[challenge.dashedName] = challenge;
return hash;
}, {})
);
const blocks = Block.find$({
order: ['superOrder ASC', 'order ASC'],
where: { isPrivate: false }
});
const blockMap = Observable.combineLatest(
blocks.map(blocks =>
blocks
.map(block => block.toJSON())
.reduce((hash, block) => {
hash[block.dashedName] = block;
return hash;
}, {})
),
challenges
).map(([blocksMap, challenges]) => {
return challenges.reduce((blocksMap, challenge) => {
if (blocksMap[challenge.block].challenges) {
blocksMap[challenge.block].challenges.push(challenge.dashedName);
} else {
blocksMap[challenge.block] = {
...blocksMap[challenge.block],
challenges: [challenge.dashedName]
};
}
return blocksMap;
}, blocksMap);
});
const superBlockMap = blocks.map(blocks =>
blocks.reduce((map, block) => {
if (map[block.superBlock] && map[block.superBlock].blocks) {
map[block.superBlock].blocks.push(block.dashedName);
} else {
map[block.superBlock] = {
title: _.startCase(block.superBlock),
order: block.superOrder,
name: nameify(_.startCase(block.superBlock)),
dashedName: block.superBlock,
blocks: [block.dashedName],
message: block.superBlockMessage
};
}
return map;
}, {})
);
const superBlocks = superBlockMap.map(superBlockMap => {
return Object.keys(superBlockMap)
.map(key => superBlockMap[key])
.map(({ dashedName }) => dashedName);
});
return Observable.combineLatest(
superBlockMap,
blockMap,
challengeMap,
superBlocks,
(superBlock, block, challenge, superBlocks) => ({
entities: {
superBlock,
block,
challenge
},
result: {
superBlocks
}
})
)
.do(checkMapData)
.shareReplay();
}
export const cachedMap = _.once(_cachedMap);
// type ObjectId: String;
// getChallengeById(
// map: Observable[map],
// id: ObjectId
// ) => Observable[Challenge] | Void;
export function getChallengeById(map, id) {
return Observable.if(
() => !id,
map.map(getFirstChallenge),
map.map(addNameIdMap).map(map => {
const {
entities: { challenge: challengeMap, challengeIdToName }
} = map;
let finalChallenge;
const dashedName = challengeIdToName[id];
finalChallenge = challengeMap[dashedName];
if (!finalChallenge) {
finalChallenge = getFirstChallenge(map);
}
return finalChallenge;
})
);
}
export function getChallengeInfo(map) {
return map
.map(addNameIdMap)
.map(({ entities: { challenge: challengeMap, challengeIdToName } }) => ({
challengeMap,
challengeIdToName
}));
}
// if challenge is not isComingSoon or isBeta => load
// if challenge is ComingSoon we are in beta||dev => load
// if challenge is beta and we are in beta||dev => load
// else hide
function loadComingSoonOrBetaChallenge({
isComingSoon,
isBeta: challengeIsBeta
}) {
return !(isComingSoon || challengeIsBeta) || isDev || isBeta;
}
// this is a hard search
// falls back to soft search
export function getChallenge(challengeDashedName, blockDashedName, map) {
return map.flatMap(({ entities, result: { superBlocks } }) => {
const superBlock = entities.superBlock;
const block = entities.block[blockDashedName];
const challenge = entities.challenge[challengeDashedName];
return Observable.if(
() =>
!blockDashedName ||
!block ||
!challenge ||
!loadComingSoonOrBetaChallenge(challenge),
getChallengeByDashedName(challengeDashedName, map),
Observable.just({ block, challenge })
).map(({ challenge, block }) => ({
redirect:
challenge.block !== blockDashedName
? `/challenges/${block.dashedName}/${challenge.dashedName}`
: false,
entities: {
superBlock,
challenge: {
[challenge.dashedName]: challenge
}
},
result: {
block: block.dashedName,
challenge: challenge.dashedName,
superBlocks
}
}));
});
}
export function getBlockForChallenge(map, challenge) {
return map.map(({ entities: { block } }) => block[challenge.block]);
}
export function getChallengeByDashedName(dashedName, map) {
const challengeName = unDasherize(dashedName).replace(challengesRegex, '');
const testChallengeName = new RegExp(challengeName, 'i');
return map
.map(({ entities }) => entities.challenge)
.flatMap(challengeMap => {
return Observable.from(Object.keys(challengeMap)).map(
key => challengeMap[key]
);
})
.filter(challenge => {
return (
loadComingSoonOrBetaChallenge(challenge) &&
testChallengeName.test(challenge.name)
);
})
.last({ defaultValue: null })
.flatMap(challengeOrNull => {
return Observable.if(
() => !!challengeOrNull,
Observable.just(challengeOrNull),
map.map(getFirstChallenge)
);
})
.flatMap(challenge => {
return getBlockForChallenge(map, challenge).map(block => ({
challenge,
block
}));
});
}