From 17812fc54950c288ff3c7f6852656af763615d97 Mon Sep 17 00:00:00 2001 From: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:19:19 +0700 Subject: [PATCH] feat(external curricula): build external curricula data v2 (#59533) Co-authored-by: sembauke --- shared/config/chapters.ts | 9 + shared/config/modules.ts | 8 + tools/scripts/build/build-curriculum.ts | 12 +- .../build-external-curricula-data-v1.test.ts | 2 +- .../build-external-curricula-data-v2.test.ts | 235 ++++++++++++ .../build/build-external-curricula-data-v2.ts | 346 ++++++++++++++++++ ...a-schema.js => external-data-schema-v1.js} | 0 .../scripts/build/external-data-schema-v2.js | 125 +++++++ 8 files changed, 733 insertions(+), 4 deletions(-) create mode 100644 shared/config/modules.ts create mode 100644 tools/scripts/build/build-external-curricula-data-v2.test.ts create mode 100644 tools/scripts/build/build-external-curricula-data-v2.ts rename tools/scripts/build/{external-data-schema.js => external-data-schema-v1.js} (100%) create mode 100644 tools/scripts/build/external-data-schema-v2.js diff --git a/shared/config/chapters.ts b/shared/config/chapters.ts index 8a5e166871b..43c2cdfed80 100644 --- a/shared/config/chapters.ts +++ b/shared/config/chapters.ts @@ -1,3 +1,5 @@ +import type { Module } from './modules'; + // TODO: Dynamically create these from intro.json or full-stack.json export enum FsdChapters { Welcome = 'freecodecamp', @@ -9,3 +11,10 @@ export enum FsdChapters { BackendJavascript = 'backend-javascript', Python = 'python' } + +export interface Chapter { + dashedName: string; + comingSoon?: boolean; + modules: Module[]; + chapterType?: string; +} diff --git a/shared/config/modules.ts b/shared/config/modules.ts new file mode 100644 index 00000000000..9deea867796 --- /dev/null +++ b/shared/config/modules.ts @@ -0,0 +1,8 @@ +export interface Module { + dashedName: string; + comingSoon?: boolean; + blocks: { + dashedName: string; + }[]; + moduleType?: string; +} diff --git a/tools/scripts/build/build-curriculum.ts b/tools/scripts/build/build-curriculum.ts index 0a473859129..6d7267e5065 100644 --- a/tools/scripts/build/build-curriculum.ts +++ b/tools/scripts/build/build-curriculum.ts @@ -4,9 +4,14 @@ import path from 'path'; import { getChallengesForLang } from '../../../curriculum/get-challenges'; import { buildExtCurriculumDataV1, - Curriculum, - CurriculumProps + type Curriculum as CurriculumV1, + type CurriculumProps as CurriculumPropsV1 } from './build-external-curricula-data-v1'; +import { + buildExtCurriculumDataV2, + type Curriculum as CurriculumV2, + type CurriculumProps as CurriculumPropsV2 +} from './build-external-curricula-data-v2'; const globalConfigPath = path.resolve(__dirname, '../../../shared/config'); @@ -14,7 +19,8 @@ const globalConfigPath = path.resolve(__dirname, '../../../shared/config'); // across all languages. void getChallengesForLang('english') .then((result: Record) => { - buildExtCurriculumDataV1(result as Curriculum); + buildExtCurriculumDataV1(result as CurriculumV1); + buildExtCurriculumDataV2(result as CurriculumV2); return result; }) .then(JSON.stringify) diff --git a/tools/scripts/build/build-external-curricula-data-v1.test.ts b/tools/scripts/build/build-external-curricula-data-v1.test.ts index 74f42e47095..44a6eae744d 100644 --- a/tools/scripts/build/build-external-curricula-data-v1.test.ts +++ b/tools/scripts/build/build-external-curricula-data-v1.test.ts @@ -7,7 +7,7 @@ import { SuperBlocks } from '../../../shared/config/curriculum'; import { superblockSchemaValidator, availableSuperBlocksValidator -} from './external-data-schema'; +} from './external-data-schema-v1'; import { type Curriculum, type CurriculumIntros, diff --git a/tools/scripts/build/build-external-curricula-data-v2.test.ts b/tools/scripts/build/build-external-curricula-data-v2.test.ts new file mode 100644 index 00000000000..6633f1f6e5d --- /dev/null +++ b/tools/scripts/build/build-external-curricula-data-v2.test.ts @@ -0,0 +1,235 @@ +import path from 'path'; +import fs, { readFileSync } from 'fs'; + +import readdirp from 'readdirp'; + +import { + chapterBasedSuperBlocks, + SuperBlocks +} from '../../../shared/config/curriculum'; +import { + superblockSchemaValidator, + availableSuperBlocksValidator +} from './external-data-schema-v2'; +import { + type CurriculumIntros, + type Curriculum, + type GeneratedCurriculumProps, + type GeneratedBlockBasedCurriculumProps, + type GeneratedChapterBasedCurriculumProps, + orderedSuperBlockInfo +} from './build-external-curricula-data-v2'; + +const VERSION = 'v2'; +const intros = JSON.parse( + readFileSync( + path.resolve(__dirname, '../../../client/i18n/locales/english/intro.json'), + 'utf-8' + ) +) as CurriculumIntros; + +describe('external curriculum data build', () => { + const clientStaticPath = path.resolve(__dirname, '../../../client/static'); + + const validateSuperBlock = superblockSchemaValidator(); + + test("the external curriculum data should be in the client's static directory", () => { + expect( + fs.existsSync(`${clientStaticPath}/curriculum-data/${VERSION}`) + ).toBe(true); + + expect( + fs.readdirSync(`${clientStaticPath}/curriculum-data/${VERSION}`).length + ).toBeGreaterThan(0); + }); + + test('there should be an endpoint to request submit types from', () => { + expect( + fs.existsSync( + `${clientStaticPath}/curriculum-data/${VERSION}/submit-types.json` + ) + ).toBe(true); + }); + + test('the available-superblocks file should have the correct structure', async () => { + const validateAvailableSuperBlocks = availableSuperBlocksValidator(); + const availableSuperblocks: unknown = JSON.parse( + await fs.promises.readFile( + `${clientStaticPath}/curriculum-data/${VERSION}/available-superblocks.json`, + 'utf-8' + ) + ); + + const result = validateAvailableSuperBlocks(availableSuperblocks); + + if (result.error) { + throw Error( + `file: available-superblocks.json +${result.error.message}` + ); + } + }); + + test('the super block files generated should have the correct schema', async () => { + const fileArray = ( + await readdirp.promise(`${clientStaticPath}/curriculum-data/${VERSION}`, { + directoryFilter: ['!challenges'], + fileFilter: entry => { + // The directory contains super block files and other curriculum-related files. + // We're only interested in super block ones. + const superBlocks = Object.values(SuperBlocks); + return superBlocks.includes(entry.basename); + } + }) + ).map(file => file.path); + + fileArray.forEach(fileInArray => { + const fileContent = fs.readFileSync( + `${clientStaticPath}/curriculum-data/${VERSION}/${fileInArray}`, + 'utf-8' + ); + + const result = validateSuperBlock(JSON.parse(fileContent)); + + if (result.error) { + throw Error(`file: ${fileInArray} +${result.error.message}`); + } + }); + }); + + test('block-based super blocks and blocks should have the correct data', async () => { + const superBlockFiles = ( + await readdirp.promise(`${clientStaticPath}/curriculum-data/${VERSION}`, { + directoryFilter: ['!challenges'], + fileFilter: entry => { + // The directory contains super block files and other curriculum-related files. + // We're only interested in super block ones. + const superBlocks = Object.values(SuperBlocks); + return ( + superBlocks.includes(entry.basename) && + !chapterBasedSuperBlocks.includes(entry.basename) + ); + } + }) + ).map(file => file.path); + + superBlockFiles.forEach(file => { + const fileContentJson = fs.readFileSync( + `${clientStaticPath}/curriculum-data/${VERSION}/${file}`, + 'utf-8' + ); + + const fileContent = JSON.parse( + fileContentJson + ) as Curriculum; + + const superBlock = Object.keys(fileContent)[0] as SuperBlocks; + const superBlockData = fileContent[ + superBlock + ] as GeneratedBlockBasedCurriculumProps; + + // Randomly pick a block to check its data. + const blocks = superBlockData.blocks; + const randomBlockIndex = Math.floor(Math.random() * blocks.length); + + expect(superBlockData.intro).toEqual(intros[superBlock].intro); + expect(superBlockData.blocks[randomBlockIndex].intro).toEqual( + intros[superBlock].blocks[randomBlockIndex].intro + ); + expect(superBlockData.blocks[randomBlockIndex].meta.name).toEqual( + intros[superBlock].blocks[randomBlockIndex].title + ); + }); + }); + + test('chapter-based super blocks and blocks should have the correct data', async () => { + const superBlockFiles = ( + await readdirp.promise(`${clientStaticPath}/curriculum-data/${VERSION}`, { + directoryFilter: ['!challenges'], + fileFilter: entry => { + // The directory contains super block files and other curriculum-related files. + // We're only interested in super block ones. + const superBlocks = Object.values(SuperBlocks); + return ( + superBlocks.includes(entry.basename) && + chapterBasedSuperBlocks.includes(entry.basename) + ); + } + }) + ).map(file => file.path); + + superBlockFiles.forEach(file => { + const fileContentJson = fs.readFileSync( + `${clientStaticPath}/curriculum-data/${VERSION}/${file}`, + 'utf-8' + ); + + const fileContent = JSON.parse( + fileContentJson + ) as Curriculum; + + const superBlock = Object.keys(fileContent)[0] as SuperBlocks; + const superBlockData = fileContent[ + superBlock + ] as GeneratedChapterBasedCurriculumProps; + + // Randomly pick a chapter. + const chapters = superBlockData.chapters; + const randomChapterIndex = Math.floor(Math.random() * chapters.length); + const randomChapter = chapters[randomChapterIndex]; + + // Randomly pick a module. + const modules = randomChapter.modules; + const randomModuleIndex = Math.floor(Math.random() * modules.length); + const randomModule = modules[randomModuleIndex]; + + // Randomly pick a block. + const blocks = randomModule.blocks; + const randomBlockIndex = Math.floor(Math.random() * blocks.length); + + expect(superBlockData.intro).toEqual(intros[superBlock].intro); + expect( + superBlockData.chapters[randomChapterIndex].modules[randomModuleIndex] + .blocks[randomBlockIndex].intro + ).toEqual(intros[superBlock].blocks[randomBlockIndex].intro); + expect( + superBlockData.chapters[randomChapterIndex].modules[randomModuleIndex] + .blocks[randomBlockIndex].meta.name + ).toEqual(intros[superBlock].blocks[randomBlockIndex].title); + }); + }); + + test('All public SuperBlocks should be present in the SuperBlock object', () => { + const publicSuperBlockNames = Object.values(SuperBlocks); + + const superBlockDashedNames = Object.keys(orderedSuperBlockInfo).reduce( + (acc, superBlockStage) => { + const dashedNames = orderedSuperBlockInfo[superBlockStage].map( + superBlock => superBlock.dashedName + ); + acc.push(...dashedNames); + + return acc; + }, + [] as SuperBlocks[] + ); + + expect(superBlockDashedNames).toEqual( + expect.arrayContaining(publicSuperBlockNames) + ); + expect(superBlockDashedNames).toHaveLength(publicSuperBlockNames.length); + }); + + test('challenge files should be created and in the correct directory', () => { + expect( + fs.existsSync(`${clientStaticPath}/curriculum-data/${VERSION}/challenges`) + ).toBe(true); + + expect( + fs.readdirSync( + `${clientStaticPath}/curriculum-data/${VERSION}/challenges` + ).length + ).toBeGreaterThan(0); + }); +}); diff --git a/tools/scripts/build/build-external-curricula-data-v2.ts b/tools/scripts/build/build-external-curricula-data-v2.ts new file mode 100644 index 00000000000..cc676f8b872 --- /dev/null +++ b/tools/scripts/build/build-external-curricula-data-v2.ts @@ -0,0 +1,346 @@ +import { mkdirSync, writeFileSync, readFileSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { submitTypes } from '../../../shared/config/challenge-types'; +import { type ChallengeNode } from '../../../client/src/redux/prop-types'; +import { + SuperBlocks, + SuperBlockStage +} from '../../../shared/config/curriculum'; +import fullStackSuperBlockStructure from '../../../curriculum/superblock-structure/full-stack.json'; +import type { Chapter } from '../../../shared/config/chapters'; + +export type CurriculumIntros = { + [keyValue in SuperBlocks]: { + title: string; + intro: string[]; + blocks: Record; + }; +}; + +export type Curriculum = { + [keyValue in SuperBlocks]: T extends CurriculumProps + ? CurriculumProps + : GeneratedCurriculumProps; +}; + +export interface CurriculumProps { + intro: string[]; + blocks: Record>; +} + +interface Block { + desc: string[]; + intro: string[]; + challenges: T; + meta: Record; +} + +export type GeneratedCurriculumProps = + | GeneratedBlockBasedCurriculumProps + | GeneratedChapterBasedCurriculumProps; + +export interface GeneratedBlockBasedCurriculumProps { + intro: string[]; + blocks: GeneratedBlock[]; +} + +export interface GeneratedChapterBasedCurriculumProps { + intro: string[]; + chapters: GeneratedChapter[]; +} + +interface GeneratedChapter { + dashedName: string; + comingSoon?: boolean; + modules: GeneratedModule[]; + chapterType?: string; +} + +interface GeneratedModule { + dashedName: string; + comingSoon?: boolean; + blocks: GeneratedBlock[]; + moduleType?: string; +} + +interface GeneratedBlock { + dashedName: string; + intro: string; + meta: Record; +} + +const ver = 'v2'; + +const staticFolderPath = resolve(__dirname, '../../../client/static'); +const dataPath = `${staticFolderPath}/curriculum-data/`; +const blockIntroPath = resolve( + __dirname, + '../../../client/i18n/locales/english/intro.json' +); +const intros = JSON.parse( + readFileSync(blockIntroPath, 'utf-8') +) as CurriculumIntros; + +export const orderedSuperBlockInfo: Record< + string, + Array<{ dashedName: SuperBlocks; public: boolean; title: string }> +> = { + [SuperBlockStage.Core]: [ + { + dashedName: SuperBlocks.FullStackDeveloper, + public: false, + title: intros[SuperBlocks.FullStackDeveloper].title + } + ], + + [SuperBlockStage.English]: [ + { + dashedName: SuperBlocks.A2English, + public: true, + title: intros[SuperBlocks.A2English].title + }, + { + dashedName: SuperBlocks.B1English, + public: true, + title: intros[SuperBlocks.B1English].title + } + ], + + [SuperBlockStage.Extra]: [ + { + dashedName: SuperBlocks.TheOdinProject, + public: true, + title: intros[SuperBlocks.TheOdinProject].title + }, + { + dashedName: SuperBlocks.CodingInterviewPrep, + public: false, + title: intros[SuperBlocks.CodingInterviewPrep].title + }, + { + dashedName: SuperBlocks.ProjectEuler, + public: false, + title: intros[SuperBlocks.ProjectEuler].title + }, + { + dashedName: SuperBlocks.RosettaCode, + public: false, + title: intros[SuperBlocks.RosettaCode].title + } + ], + + [SuperBlockStage.Legacy]: [ + { + dashedName: SuperBlocks.RespWebDesignNew, + public: true, + title: intros[SuperBlocks.RespWebDesignNew].title + }, + { + dashedName: SuperBlocks.JsAlgoDataStructNew, + public: false, + title: intros[SuperBlocks.JsAlgoDataStructNew].title + }, + { + dashedName: SuperBlocks.FrontEndDevLibs, + public: false, + title: intros[SuperBlocks.FrontEndDevLibs].title + }, + { + dashedName: SuperBlocks.DataVis, + public: false, + title: intros[SuperBlocks.DataVis].title + }, + { + dashedName: SuperBlocks.RelationalDb, + public: false, + title: intros[SuperBlocks.RelationalDb].title + }, + { + dashedName: SuperBlocks.BackEndDevApis, + public: false, + title: intros[SuperBlocks.BackEndDevApis].title + }, + { + dashedName: SuperBlocks.QualityAssurance, + public: false, + title: intros[SuperBlocks.QualityAssurance].title + }, + { + dashedName: SuperBlocks.SciCompPy, + public: false, + title: intros[SuperBlocks.SciCompPy].title + }, + { + dashedName: SuperBlocks.DataAnalysisPy, + public: true, + title: intros[SuperBlocks.DataAnalysisPy].title + }, + { + dashedName: SuperBlocks.InfoSec, + public: false, + title: intros[SuperBlocks.InfoSec].title + }, + { + dashedName: SuperBlocks.MachineLearningPy, + public: true, + title: intros[SuperBlocks.MachineLearningPy].title + }, + { + dashedName: SuperBlocks.CollegeAlgebraPy, + public: true, + title: intros[SuperBlocks.CollegeAlgebraPy].title + }, + { + dashedName: SuperBlocks.RespWebDesign, + public: true, + title: intros[SuperBlocks.RespWebDesign].title + }, + { + dashedName: SuperBlocks.JsAlgoDataStruct, + public: false, + title: intros[SuperBlocks.JsAlgoDataStruct].title + }, + { + dashedName: SuperBlocks.PythonForEverybody, + public: true, + title: intros[SuperBlocks.PythonForEverybody].title + } + ], + + [SuperBlockStage.Professional]: [ + { + dashedName: SuperBlocks.FoundationalCSharp, + public: false, + title: intros[SuperBlocks.FoundationalCSharp].title + } + ] +}; + +export const superBlockDashedNames = Object.keys(orderedSuperBlockInfo).reduce( + (acc, superBlockStage) => { + const dashedNames = orderedSuperBlockInfo[superBlockStage].map( + superBlock => superBlock.dashedName + ); + acc.push(...dashedNames); + + return acc; + }, + [] as SuperBlocks[] +); + +export function buildExtCurriculumDataV2( + curriculum: Curriculum +): void { + mkdirSync(dataPath, { recursive: true }); + + parseCurriculumData(); + getSubmitTypes(); + + function parseCurriculumData() { + const superBlockKeys = Object.values(SuperBlocks).filter(x => + superBlockDashedNames.includes(x) + ); + + writeToFile('available-superblocks', { + superblocks: orderedSuperBlockInfo + }); + + for (const superBlockKey of superBlockKeys) { + if (superBlockKey === SuperBlocks.FullStackDeveloper) { + buildChapterBasedCurriculum(superBlockKey); + } else { + buildBlockBasedCurriculum(superBlockKey); + } + + buildChallengeFiles(superBlockKey); + } + } + + function buildChapterBasedCurriculum(superBlockKey: SuperBlocks) { + const chapters: Chapter[] = fullStackSuperBlockStructure.chapters; + const blocksWithData = curriculum[superBlockKey].blocks; + + // Skip upcoming chapter/module as the metadata of their blocks + // is not included in the `curriculum` object. + const allChapters = chapters.map(chapter => ({ + dashedName: chapter.dashedName, + comingSoon: chapter.comingSoon, + chapterType: chapter.chapterType, + modules: chapter.comingSoon + ? [] + : chapter.modules.map(module => ({ + dashedName: module.dashedName, + comingSoon: module.comingSoon, + moduleType: module.moduleType, + blocks: module.comingSoon + ? [] + : module.blocks.map(block => { + const blockData = blocksWithData[block.dashedName]; + + return { + intro: intros[superBlockKey].blocks[block.dashedName].intro, + meta: blockData.meta + }; + }) + })) + })); + + const superBlock = { + [superBlockKey]: { + intro: intros[superBlockKey].intro, + chapters: allChapters + } + }; + + writeToFile(superBlockKey, superBlock); + } + + function buildBlockBasedCurriculum(superBlockKey: SuperBlocks) { + const blockNames = Object.keys(curriculum[superBlockKey].blocks); + const blocks = blockNames.map(blockName => { + const blockData = curriculum[superBlockKey].blocks[blockName]; + + return { + intro: intros[superBlockKey].blocks[blockName].intro, + meta: blockData.meta + }; + }); + + const superBlock = { + [superBlockKey]: { + intro: intros[superBlockKey].intro, + blocks + } + }; + + writeToFile(superBlockKey, superBlock); + } + + function buildChallengeFiles(superBlockKey: SuperBlocks) { + const blocks = Object.keys(curriculum[superBlockKey].blocks); + + for (const block of blocks) { + const challenges = curriculum[superBlockKey]['blocks'][block].challenges; + + for (const challenge of challenges) { + const challengeId = challenge.id; + const challengePath = `challenges/${superBlockKey}/${block}/${challengeId}`; + + writeToFile(challengePath, challenge); + } + } + } + + function writeToFile(fileName: string, data: Record): void { + const filePath = `${dataPath}/${ver}/${fileName}.json`; + mkdirSync(dirname(filePath), { recursive: true }); + writeFileSync(filePath, JSON.stringify(data, null, 2)); + } + + function getSubmitTypes() { + writeFileSync( + `${dataPath}/${ver}/submit-types.json`, + JSON.stringify(submitTypes, null, 2) + ); + } +} diff --git a/tools/scripts/build/external-data-schema.js b/tools/scripts/build/external-data-schema-v1.js similarity index 100% rename from tools/scripts/build/external-data-schema.js rename to tools/scripts/build/external-data-schema-v1.js diff --git a/tools/scripts/build/external-data-schema-v2.js b/tools/scripts/build/external-data-schema-v2.js new file mode 100644 index 00000000000..365997a7408 --- /dev/null +++ b/tools/scripts/build/external-data-schema-v2.js @@ -0,0 +1,125 @@ +const Joi = require('joi'); +const { + chapterBasedSuperBlocks +} = require('../../../shared/config/curriculum'); + +const slugRE = new RegExp('^[a-z0-9-]+$'); + +const blockSchema = Joi.object().keys({ + intro: Joi.array().min(1), + meta: Joi.object({}).keys({ + name: Joi.string().required(), + isUpcomingChange: Joi.bool().required(), + usesMultifileEditor: Joi.bool().optional(), + hasEditableBoundaries: Joi.bool().optional(), + dashedName: Joi.string().required(), + helpCategory: Joi.valid( + 'JavaScript', + 'HTML-CSS', + 'Python', + 'Backend Development', + 'C-Sharp', + 'English', + 'Odin', + 'Euler', + 'Rosetta' + ).required(), + order: Joi.number().when('superBlock', { + is: chapterBasedSuperBlocks, + then: Joi.forbidden(), + otherwise: Joi.required() + }), + template: Joi.string().allow(''), + required: Joi.array(), + superBlock: Joi.string().required(), + blockLayout: Joi.valid( + 'challenge-list', + 'challenge-grid', + 'dialogue-grid', + 'link', + 'project-list', + 'legacy-challenge-list', + 'legacy-link', + 'legacy-challenge-grid' + ).required(), + blockType: Joi.valid( + 'lecture', + 'workshop', + 'lab', + 'review', + 'quiz', + 'exam' + ).when('superBlock', { + is: chapterBasedSuperBlocks, + then: Joi.required(), + otherwise: Joi.optional() + }), + challengeOrder: Joi.array().items( + Joi.object({}).keys({ + id: Joi.string(), + title: Joi.string() + }) + ), + disableLoopProtectTests: Joi.boolean(), + disableLoopProtectPreview: Joi.boolean(), + superOrder: Joi.number() + }) +}); + +const blockBasedCurriculumSchema = Joi.object().pattern( + Joi.string(), + Joi.object().keys({ + intro: Joi.array(), + blocks: Joi.array().items(blockSchema) + }) +); + +const chapterBasedCurriculumSchema = Joi.object().pattern( + Joi.string(), + Joi.object().keys({ + intro: Joi.array(), + chapters: Joi.array().items( + Joi.object().keys({ + dashedName: Joi.string().regex(slugRE).required(), + comingSoon: Joi.boolean().optional(), + chapterType: Joi.valid('exam').optional(), + modules: Joi.array() + .items( + Joi.object().keys({ + moduleType: Joi.valid('review', 'exam').optional(), + comingSoon: Joi.boolean().optional(), + dashedName: Joi.string().regex(slugRE).required(), + blocks: Joi.array().items(blockSchema) + }) + ) + .required() + }) + ) + }) +); + +const availableSuperBlocksSchema = Joi.object({ + superblocks: Joi.object().pattern( + Joi.string(), + Joi.array().items( + Joi.object({ + dashedName: Joi.string().required(), + title: Joi.string().required(), + public: Joi.bool().required() + }) + ) + ) +}); + +exports.superblockSchemaValidator = () => superBlock => { + const superBlockName = Object.keys(superBlock)[0]; + + if (chapterBasedSuperBlocks.includes(superBlockName)) { + return chapterBasedCurriculumSchema.validate(superBlock); + } + + return blockBasedCurriculumSchema.validate(superBlock); +}; + +exports.availableSuperBlocksValidator = () => data => + availableSuperBlocksSchema.validate(data);