feat(backend): databaseLearningPathProvider.fetchLearningPaths geïmplementeerd
This commit is contained in:
		
							parent
							
								
									0fe42f73b2
								
							
						
					
					
						commit
						02be44fe53
					
				
					 6 changed files with 161 additions and 11 deletions
				
			
		|  | @ -4,6 +4,6 @@ export class LearningObjectIdentifier { | |||
|     constructor( | ||||
|         public hruid: string, | ||||
|         public language: Language, | ||||
|         public version: string | ||||
|         public version: number | ||||
|     ) {} | ||||
| } | ||||
|  |  | |||
|  | @ -54,6 +54,12 @@ export class LearningPathNode { | |||
| 
 | ||||
|     @Embedded({ entity: () => LearningPathTransition, array: true }) | ||||
|     transitions!: LearningPathTransition[]; | ||||
| 
 | ||||
|     @Property({ length: 3 }) | ||||
|     createdAt: Date = new Date(); | ||||
| 
 | ||||
|     @Property({ length: 3, onUpdate: () => new Date() }) | ||||
|     updatedAt: Date = new Date(); | ||||
| } | ||||
| 
 | ||||
| @Embeddable() | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ import {NotFoundError} from "@mikro-orm/core"; | |||
| const learningObjectRepo = getLearningObjectRepository(); | ||||
| const learningPathRepo = getLearningPathRepository(); | ||||
| 
 | ||||
| function filter(learningObject: LearningObject | null): FilteredLearningObject | null { | ||||
| function convertLearningObject(learningObject: LearningObject | null): FilteredLearningObject | null { | ||||
|     if (!learningObject) { | ||||
|         return null; | ||||
|     } | ||||
|  | @ -59,7 +59,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = { | |||
|      */ | ||||
|     async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { | ||||
|         const learningObject = await findLearningObjectEntityById(id); | ||||
|         return filter(learningObject); | ||||
|         return convertLearningObject(learningObject); | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|  | @ -110,7 +110,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = { | |||
|                 return learningObject; | ||||
|             }) | ||||
|         ); | ||||
|         return learningObjects.filter(it => it !== null); | ||||
|         return learningObjects.filter(it => it !== null); // TODO: Determine this based on the submissions of the user.
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,134 @@ | |||
| import {LearningPathProvider} from "./learning-path-provider"; | ||||
| import {LearningPath, LearningPathResponse} from "../../interfaces/learning-content"; | ||||
| import { | ||||
|     FilteredLearningObject, | ||||
|     LearningObjectNode, | ||||
|     LearningPath, | ||||
|     LearningPathResponse, | ||||
|     Transition | ||||
| } from "../../interfaces/learning-content"; | ||||
| import { | ||||
|     LearningPath as LearningPathEntity, | ||||
|     LearningPathNode, | ||||
|     LearningPathTransition | ||||
| } from "../../entities/content/learning-path.entity" | ||||
| import {getLearningPathRepository} from "../../data/repositories"; | ||||
| import {Language} from "../../entities/content/language"; | ||||
| import learningObjectService from "../learning-objects/learning-object-service"; | ||||
| 
 | ||||
| const learningPathRepo = getLearningPathRepository(); | ||||
| 
 | ||||
| /** | ||||
|  * Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its | ||||
|  * corresponding learning object. | ||||
|  * @param nodes The nodes to find the learning object for. | ||||
|  */ | ||||
| async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Map<LearningPathNode, FilteredLearningObject>> { | ||||
|     // Fetching the corresponding learning object for each of the nodes and creating a map that maps each node to
 | ||||
|     // its corresponding learning object.
 | ||||
|     const nullableNodesToLearningObjects = new Map<LearningPathNode, FilteredLearningObject | null>( | ||||
|         await Promise.all( | ||||
|             nodes.map(node => | ||||
|                 learningObjectService.getLearningObjectById({ | ||||
|                     hruid: node.learningObjectHruid, | ||||
|                     version: node.version, | ||||
|                     language: node.language | ||||
|                 }).then(learningObject => | ||||
|                     <[LearningPathNode, FilteredLearningObject | null]>[node, learningObject] | ||||
|                 ) | ||||
|             ) | ||||
|         ) | ||||
|     ); | ||||
|     if (nullableNodesToLearningObjects.values().some(it => it === null)) { | ||||
|         throw new Error("At least one of the learning objects on this path could not be found.") | ||||
|     } | ||||
|     return nullableNodesToLearningObjects as Map<LearningPathNode, FilteredLearningObject>; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Convert the given learning path entity to an object which conforms to the learning path content. | ||||
|  */ | ||||
| async function convertLearningPath(learningPath: LearningPathEntity, order: number): Promise<LearningPath> { | ||||
|     const nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> = | ||||
|         await getLearningObjectsForNodes(learningPath.nodes); | ||||
| 
 | ||||
|     const targetAges = | ||||
|         nodesToLearningObjects.values().flatMap(it => it.targetAges || []).toArray(); | ||||
| 
 | ||||
|     const keywords = | ||||
|         nodesToLearningObjects.values().flatMap(it => it.keywords || []).toArray(); | ||||
| 
 | ||||
|     return { | ||||
|         _id: `${learningPath.hruid}/${learningPath.language}`, // for backwards compatibility with the original Dwengo API.
 | ||||
|         __order: order, | ||||
|         hruid: learningPath.hruid, | ||||
|         language: learningPath.language, | ||||
|         description: learningPath.description, | ||||
|         image: learningPath.image, | ||||
|         title: learningPath.title, | ||||
|         nodes: convertNodes(nodesToLearningObjects), | ||||
|         num_nodes: learningPath.nodes.length, | ||||
|         num_nodes_left: learningPath.nodes.length, // TODO: Adjust when submissions are added.
 | ||||
|         keywords: keywords.join(' '), | ||||
|         target_ages: targetAges, | ||||
|         max_age: Math.max(...targetAges), | ||||
|         min_age: Math.min(...targetAges) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Helper function converting pairs of learning path nodes (as represented in the database) and the corresponding | ||||
|  * learning objects into a list of learning path nodes as they should be represented in the API. | ||||
|  * @param nodesToLearningObjects | ||||
|  */ | ||||
| function convertNodes( | ||||
|     nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> | ||||
| ): LearningObjectNode[]  { | ||||
|     return nodesToLearningObjects.entries().map((entry) => { | ||||
|         const [node, learningObject] = entry; | ||||
|         return { | ||||
|             _id: learningObject.uuid, | ||||
|             language: learningObject.language, | ||||
|             start_node: node.startNode, | ||||
|             created_at: node.createdAt.toISOString(), | ||||
|             updatedAt: node.updatedAt.toISOString(), | ||||
|             learningobject_hruid: node.learningObjectHruid, | ||||
|             version: learningObject.version, | ||||
|             transitions: node.transitions.map((trans, i) => | ||||
|                 convertTransition(trans, i, nodesToLearningObjects) | ||||
|             ) | ||||
|         } | ||||
|     }).toArray(); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Helper function which converts a transition in the database representation to a transition in the representation | ||||
|  * the Dwengo API uses. | ||||
|  * | ||||
|  * @param transition | ||||
|  * @param index | ||||
|  * @param nodesToLearningObjects | ||||
|  */ | ||||
| function convertTransition( | ||||
|     transition: LearningPathTransition, | ||||
|     index: number, | ||||
|     nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> | ||||
| ): Transition { | ||||
|     const nextNode = nodesToLearningObjects.get(transition.next); | ||||
|     if (!nextNode) { | ||||
|         throw new Error(`Learning object ${transition.next.learningObjectHruid}/${transition.next.language}/${transition.next.version} not found!`) | ||||
|     } else { | ||||
|         return { | ||||
|             _id: "" + index, // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path.
 | ||||
|             default: false, // We don't work with default transitions but retain this for backwards compatibility.
 | ||||
|             next: { | ||||
|                 _id: nextNode._id + index, // Construct a unique ID for the transition for backwards compatibility.
 | ||||
|                 hruid: transition.next.learningObjectHruid, | ||||
|                 language: nextNode.language, | ||||
|                 version: nextNode.version | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Service providing access to data about learning paths from the database. | ||||
|  | @ -8,8 +137,21 @@ const databaseLearningPathProvider: LearningPathProvider = { | |||
|     /** | ||||
|      * Fetch the learning paths with the given hruids from the database. | ||||
|      */ | ||||
|     fetchLearningPaths(hruids: string[], language: string, source: string): Promise<LearningPathResponse> { | ||||
|         throw new Error("Not yet implemented"); // TODO
 | ||||
|     async fetchLearningPaths(hruids: string[], language: Language, source: string): Promise<LearningPathResponse> { | ||||
|         const learningPaths = await Promise.all( | ||||
|             hruids.map(hruid => learningPathRepo.findByHruidAndLanguage(hruid, language)) | ||||
|         ); | ||||
|         const filteredLearningPaths = await Promise.all( | ||||
|             learningPaths | ||||
|                 .filter(learningPath => learningPath !== null) | ||||
|                 .map((learningPath, index) => convertLearningPath(learningPath, index)) | ||||
|         ); | ||||
| 
 | ||||
|         return { | ||||
|             success: filteredLearningPaths.length > 0, | ||||
|             data: await Promise.all(filteredLearningPaths), | ||||
|             source | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| import {LearningPath, LearningPathResponse} from "../../interfaces/learning-content"; | ||||
| import {Language} from "../../entities/content/language"; | ||||
| 
 | ||||
| /** | ||||
|  * Generic interface for a service which provides access to learning paths from a data source. | ||||
|  | @ -7,10 +8,10 @@ export interface LearningPathProvider { | |||
|     /** | ||||
|      * Fetch the learning paths with the given hruids from the data source. | ||||
|      */ | ||||
|     fetchLearningPaths(hruids: string[], language: string, source: string): Promise<LearningPathResponse>; | ||||
|     fetchLearningPaths(hruids: string[], language: Language, source: string): Promise<LearningPathResponse>; | ||||
| 
 | ||||
|     /** | ||||
|      * Search learning paths in the data source using the given search string. | ||||
|      */ | ||||
|     searchLearningPaths(query: string, language: string): Promise<LearningPath[]>; | ||||
|     searchLearningPaths(query: string, language: Language): Promise<LearningPath[]>; | ||||
| } | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import { | |||
| import dwengoApiLearningPathProvider from "./dwengo-api-learning-path-provider"; | ||||
| import databaseLearningPathProvider from "./database-learning-path-provider"; | ||||
| import {EnvVars, getEnvVar} from "../../util/envvars"; | ||||
| import {Language} from "../../entities/content/language"; | ||||
| 
 | ||||
| const userContentPrefix = getEnvVar(EnvVars.UserContentPrefix); | ||||
| const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider] | ||||
|  | @ -16,7 +17,7 @@ const learningPathService = { | |||
|     /** | ||||
|      * Fetch the learning paths with the given hruids from the data source. | ||||
|      */ | ||||
|     async fetchLearningPaths(hruids: string[], language: string, source: string): Promise<LearningPathResponse> { | ||||
|     async fetchLearningPaths(hruids: string[], language: Language, source: string): Promise<LearningPathResponse> { | ||||
|         const userContentHruids = hruids.filter(hruid => hruid.startsWith(userContentPrefix)); | ||||
|         const nonUserContentHruids = hruids.filter(hruid => !hruid.startsWith(userContentPrefix)); | ||||
| 
 | ||||
|  | @ -35,7 +36,7 @@ const learningPathService = { | |||
|     /** | ||||
|      * Search learning paths in the data source using the given search string. | ||||
|      */ | ||||
|     async searchLearningPaths(query: string, language: string): Promise<LearningPath[]> { | ||||
|     async searchLearningPaths(query: string, language: Language): Promise<LearningPath[]> { | ||||
|         const providerResponses = await Promise.all( | ||||
|             allProviders.map( | ||||
|                 provider => provider.searchLearningPaths(query, language) | ||||
|  |  | |||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger