diff --git a/backend/src/data/content/learning-object-repository.ts b/backend/src/data/content/learning-object-repository.ts index d69ae075..8fdf18fe 100644 --- a/backend/src/data/content/learning-object-repository.ts +++ b/backend/src/data/content/learning-object-repository.ts @@ -7,21 +7,30 @@ export class LearningObjectRepository extends DwengoEntityRepository { - return this.findOne({ - hruid: identifier.hruid, - language: identifier.language, - version: identifier.version, - }); + return this.findOne( + { + hruid: identifier.hruid, + language: identifier.language, + version: identifier.version, + }, + { + populate: ["keywords"] + } + ); } public findLatestByHruidAndLanguage(hruid: string, language: Language) { - return this.findOne({ - hruid: hruid, - language: language - }, { - orderBy: { - version: "DESC" + return this.findOne( + { + hruid: hruid, + language: language + }, + { + populate: ["keywords"], + orderBy: { + version: "DESC" + } } - }); + ); } } diff --git a/backend/src/data/content/learning-path-repository.ts b/backend/src/data/content/learning-path-repository.ts index 66ef54d0..347bc023 100644 --- a/backend/src/data/content/learning-path-repository.ts +++ b/backend/src/data/content/learning-path-repository.ts @@ -7,7 +7,10 @@ export class LearningPathRepository extends DwengoEntityRepository hruid: string, language: Language ): Promise { - return this.findOne({ hruid: hruid, language: language }); + return this.findOne( + { hruid: hruid, language: language }, + { populate: ["nodes", "nodes.transitions"] } + ); } /** @@ -25,8 +28,8 @@ export class LearningPathRepository extends DwengoEntityRepository { title: { $like: `%${query}%`} }, { description: { $like: `%${query}%`} } ] - } + }, + populate: ["nodes", "nodes.transitions"] }); } - // This repository is read-only for now since creating own learning object is an extension feature. } diff --git a/backend/src/data/repositories.ts b/backend/src/data/repositories.ts index 800d5485..6218bafc 100644 --- a/backend/src/data/repositories.ts +++ b/backend/src/data/repositories.ts @@ -33,6 +33,8 @@ import { LearningPath } from '../entities/content/learning-path.entity.js'; import { LearningPathRepository } from './content/learning-path-repository.js'; import { AttachmentRepository } from './content/attachment-repository.js'; import { Attachment } from '../entities/content/attachment.entity.js'; +import {LearningPathNode} from "../entities/content/learning-path-node.entity"; +import {LearningPathTransition} from "../entities/content/learning-path-transition.entity"; let entityManager: EntityManager | undefined; @@ -113,6 +115,8 @@ export const getLearningPathRepository = repositoryGetter< LearningPath, LearningPathRepository >(LearningPath); +export const getLearningPathNodeRepository = repositoryGetter(LearningPathNode); +export const getLearningPathTransitionRepository = repositoryGetter(LearningPathTransition); export const getAttachmentRepository = repositoryGetter< Attachment, AttachmentRepository diff --git a/backend/src/entities/content/learning-path-node.entity.ts b/backend/src/entities/content/learning-path-node.entity.ts index e15b033a..e2fbdbb3 100644 --- a/backend/src/entities/content/learning-path-node.entity.ts +++ b/backend/src/entities/content/learning-path-node.entity.ts @@ -5,10 +5,11 @@ import {LearningPathTransition} from "./learning-path-transition.entity"; @Entity() export class LearningPathNode { + @ManyToOne({ entity: () => LearningPath, primary: true }) learningPath!: LearningPath; - @PrimaryKey({ type: "numeric", autoincrement: true }) + @PrimaryKey({ type: "integer", autoincrement: true }) nodeNumber!: number; @Property({ type: 'string' }) diff --git a/backend/src/entities/content/learning-path-transition.entity.ts b/backend/src/entities/content/learning-path-transition.entity.ts index 6122b758..dfbe110e 100644 --- a/backend/src/entities/content/learning-path-transition.entity.ts +++ b/backend/src/entities/content/learning-path-transition.entity.ts @@ -3,7 +3,7 @@ import {LearningPathNode} from "./learning-path-node.entity"; @Entity() export class LearningPathTransition { - @ManyToOne({entity: () => LearningPathNode }) + @ManyToOne({entity: () => LearningPathNode, primary: true }) node!: LearningPathNode; @PrimaryKey({ type: 'numeric' }) diff --git a/backend/src/interfaces/learning-content.ts b/backend/src/interfaces/learning-content.ts index 861996f5..89a1340c 100644 --- a/backend/src/interfaces/learning-content.ts +++ b/backend/src/interfaces/learning-content.ts @@ -21,7 +21,7 @@ export interface LearningObjectNode { _id: string; learningobject_hruid: string; version: number; - language: string; + language: Language; start_node?: boolean; transitions: Transition[]; created_at: string; @@ -88,7 +88,7 @@ export interface FilteredLearningObject { version: number; title: string; htmlUrl: string; - language: string; + language: Language; difficulty: number; estimatedTime: number; available: boolean; diff --git a/backend/tests/services/learning-objects/database-learning-path-provider.test.ts b/backend/tests/services/learning-objects/database-learning-path-provider.test.ts index dab40a69..321fe213 100644 --- a/backend/tests/services/learning-objects/database-learning-path-provider.test.ts +++ b/backend/tests/services/learning-objects/database-learning-path-provider.test.ts @@ -6,6 +6,10 @@ import {getLearningObjectRepository, getLearningPathRepository} from "../../../s import learningObjectExample from "../../test-assets/learning-objects/pn_werkingnotebooks/pn-werkingnotebooks-example"; import learningPathExample from "../../test-assets/learning-paths/pn-werking-example" import databaseLearningPathProvider from "../../../src/services/learning-paths/database-learning-path-provider"; +import {expectToBeCorrectLearningPath} from "../../test-utils/expectations"; +import {LearningObjectRepository} from "../../../src/data/content/learning-object-repository"; +import learningObjectService from "../../../src/services/learning-objects/learning-object-service"; +import {Language} from "../../../src/entities/content/language"; async function initExampleData(): Promise<{ learningObject: LearningObject, learningPath: LearningPath }> { const learningObjectRepo = getLearningObjectRepository(); @@ -18,15 +22,17 @@ async function initExampleData(): Promise<{ learningObject: LearningObject, lear } describe("DatabaseLearningPathProvider", () => { + let learningObjectRepo: LearningObjectRepository; let example: {learningObject: LearningObject, learningPath: LearningPath}; beforeAll(async () => { await setupTestApp(); example = await initExampleData(); + learningObjectRepo = getLearningObjectRepository(); }); describe("fetchLearningPaths", () => { - it("returns the learning path correctly", () => { + it("returns the learning path correctly", async () => { const result = await databaseLearningPathProvider.fetchLearningPaths( [example.learningPath.hruid], example.learningPath.language, @@ -34,7 +40,26 @@ describe("DatabaseLearningPathProvider", () => { ); expect(result.success).toBe(true); expect(result.data?.length).toBe(1); - expect(result.data) - }) + + const learningObjectsOnPath = (await Promise.all( + example.learningPath.nodes.map(node => + learningObjectService.getLearningObjectById({ + hruid: node.learningObjectHruid, + version: node.version, + language: node.language + })) + )).filter(it => it !== null); + + expectToBeCorrectLearningPath(result.data![0], example.learningPath, learningObjectsOnPath) + }); + it("returns a non-successful response if a non-existing learning path is queried", async () => { + const result = await databaseLearningPathProvider.fetchLearningPaths( + [example.learningPath.hruid], + Language.Abkhazian, // wrong language + "the source" + ); + + expect(result.success).toBe(false); + }); }); }); diff --git a/backend/tests/test-assets/learning-paths/learning-path-utils.ts b/backend/tests/test-assets/learning-paths/learning-path-utils.ts index 31374869..68c49412 100644 --- a/backend/tests/test-assets/learning-paths/learning-path-utils.ts +++ b/backend/tests/test-assets/learning-paths/learning-path-utils.ts @@ -1,21 +1,28 @@ import {Language} from "../../../src/entities/content/language"; import {LearningPathTransition} from "../../../src/entities/content/learning-path-transition.entity"; import {LearningPathNode} from "../../../src/entities/content/learning-path-node.entity"; +import {LearningPath} from "../../../src/entities/content/learning-path.entity"; -export function createLearningPathTransition(condition: string | null, to: LearningPathNode) { +export function createLearningPathTransition(node: LearningPathNode, transitionNumber: number, condition: string | null, to: LearningPathNode) { let trans = new LearningPathTransition(); + trans.node = node; + trans.transitionNumber = transitionNumber; trans.condition = condition || "true"; trans.next = to; return trans; } export function createLearningPathNode( + learningPath: LearningPath, + nodeNumber: number, learningObjectHruid: string, version: number, language: Language, startNode: boolean ) { let node = new LearningPathNode(); + node.learningPath = learningPath; + node.nodeNumber = nodeNumber; node.learningObjectHruid = learningObjectHruid; node.version = version; node.language = language; diff --git a/backend/tests/test-assets/learning-paths/pn-werking-example.ts b/backend/tests/test-assets/learning-paths/pn-werking-example.ts index dbf86052..a96de552 100644 --- a/backend/tests/test-assets/learning-paths/pn-werking-example.ts +++ b/backend/tests/test-assets/learning-paths/pn-werking-example.ts @@ -1,16 +1,17 @@ -import {LearningPath, LearningPathNode} from "../../../src/entities/content/learning-path.entity"; +import {LearningPath} from "../../../src/entities/content/learning-path.entity"; import {Language} from "../../../src/entities/content/language"; import {EnvVars, getEnvVar} from "../../../src/util/envvars"; import {createLearningPathNode, createLearningPathTransition} from "./learning-path-utils"; +import {LearningPathNode} from "../../../src/entities/content/learning-path-node.entity"; -function createNodes(): LearningPathNode[] { +function createNodes(learningPath: LearningPath): LearningPathNode[] { let nodes = [ - createLearningPathNode("u_pn_werkingnotebooks", 3, Language.Dutch, true), - createLearningPathNode("pn_werkingnotebooks2", 3, Language.Dutch, false), - createLearningPathNode("pn_werkingnotebooks3", 3, Language.Dutch, false), + createLearningPathNode(learningPath, 0, "u_pn_werkingnotebooks", 3, Language.Dutch, true), + createLearningPathNode(learningPath, 1, "pn_werkingnotebooks2", 3, Language.Dutch, false), + createLearningPathNode(learningPath, 2, "pn_werkingnotebooks3", 3, Language.Dutch, false), ]; - nodes[0].transitions.push(createLearningPathTransition("true", nodes[1])); - nodes[1].transitions.push(createLearningPathTransition("true", nodes[2])); + nodes[0].transitions.push(createLearningPathTransition(nodes[0], 0, "true", nodes[1])); + nodes[1].transitions.push(createLearningPathTransition(nodes[1], 0, "true", nodes[2])); return nodes; } @@ -21,7 +22,7 @@ const example: LearningPathExample = { path.hruid = `${getEnvVar(EnvVars.UserContentPrefix)}pn_werking`; path.title = "Werken met notebooks"; path.description = "Een korte inleiding tot Python notebooks. Hoe ga je gemakkelijk en efficiƫnt met de notebooks aan de slag?"; - path.nodes = createNodes(); + path.nodes = createNodes(path); return path; } } diff --git a/backend/tests/test-utils/expectations.ts b/backend/tests/test-utils/expectations.ts index 285c001a..19c2408d 100644 --- a/backend/tests/test-utils/expectations.ts +++ b/backend/tests/test-utils/expectations.ts @@ -99,9 +99,66 @@ export function expectToBeCorrectFilteredLearningObject(filtered: FilteredLearni * * @param learningPath The learning path returned by the retriever, service or endpoint * @param expectedEntity The expected entity + * @param learningObjectsOnPath The learning objects on LearningPath. Necessary since some information in + * the learning path returned from the API endpoint */ -export function expectToBeCorrectLearningPath(learningPath: LearningPath, expectedEntity: LearningPathEntity) { +export function expectToBeCorrectLearningPath( + learningPath: LearningPath, + expectedEntity: LearningPathEntity, + learningObjectsOnPath: FilteredLearningObject[] +) { expect(learningPath.hruid).toEqual(expectedEntity.hruid); expect(learningPath.language).toEqual(expectedEntity.language); expect(learningPath.description).toEqual(expectedEntity.description); + expect(learningPath.title).toEqual(expectedEntity.title); + + const keywords = new Set(learningObjectsOnPath.flatMap(it => it.keywords || [])); + expect(new Set(learningPath.keywords.split(' '))).toEqual(keywords) + + const targetAges = new Set(learningObjectsOnPath.flatMap(it => it.targetAges || [])); + expect(new Set(learningPath.target_ages)).toEqual(targetAges); + expect(learningPath.min_age).toEqual(Math.min(...targetAges)); + expect(learningPath.max_age).toEqual(Math.max(...targetAges)); + + expect(learningPath.num_nodes).toEqual(expectedEntity.nodes.length); + expect(learningPath.image || null).toEqual(expectedEntity.image); + + let expectedLearningPathNodes = new Map( + expectedEntity.nodes.map(node => [ + {learningObjectHruid: node.learningObjectHruid, language: node.language, version: node.version}, + {startNode: node.startNode, transitions: node.transitions} + ]) + ); + + for (let node of learningPath.nodes) { + const nodeKey = { + learningObjectHruid: node.learningobject_hruid, + language: node.language, + version: node.version + }; + expect(expectedLearningPathNodes.keys()).toContainEqual(nodeKey); + let expectedNode = [...expectedLearningPathNodes.entries()] + .filter(([key, _]) => + key.learningObjectHruid === nodeKey.learningObjectHruid + && key.language === node.language + && key.version === node.version + )[0][1] + expect(node.start_node).toEqual(expectedNode?.startNode); + + expect( + new Set(node.transitions.map(it => it.next.hruid)) + ).toEqual( + new Set(expectedNode.transitions.map(it => it.next.learningObjectHruid)) + ); + expect( + new Set(node.transitions.map(it => it.next.language)) + ).toEqual( + new Set(expectedNode.transitions.map(it => it.next.language)) + ); + expect( + new Set(node.transitions.map(it => it.next.version)) + ).toEqual( + new Set(expectedNode.transitions.map(it => it.next.version)) + ); + } }