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( |     constructor( | ||||||
|         public hruid: string, |         public hruid: string, | ||||||
|         public language: Language, |         public language: Language, | ||||||
|         public version: string |         public version: number | ||||||
|     ) {} |     ) {} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -54,6 +54,12 @@ export class LearningPathNode { | ||||||
| 
 | 
 | ||||||
|     @Embedded({ entity: () => LearningPathTransition, array: true }) |     @Embedded({ entity: () => LearningPathTransition, array: true }) | ||||||
|     transitions!: LearningPathTransition[]; |     transitions!: LearningPathTransition[]; | ||||||
|  | 
 | ||||||
|  |     @Property({ length: 3 }) | ||||||
|  |     createdAt: Date = new Date(); | ||||||
|  | 
 | ||||||
|  |     @Property({ length: 3, onUpdate: () => new Date() }) | ||||||
|  |     updatedAt: Date = new Date(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @Embeddable() | @Embeddable() | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ import {NotFoundError} from "@mikro-orm/core"; | ||||||
| const learningObjectRepo = getLearningObjectRepository(); | const learningObjectRepo = getLearningObjectRepository(); | ||||||
| const learningPathRepo = getLearningPathRepository(); | const learningPathRepo = getLearningPathRepository(); | ||||||
| 
 | 
 | ||||||
| function filter(learningObject: LearningObject | null): FilteredLearningObject | null { | function convertLearningObject(learningObject: LearningObject | null): FilteredLearningObject | null { | ||||||
|     if (!learningObject) { |     if (!learningObject) { | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
|  | @ -59,7 +59,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = { | ||||||
|      */ |      */ | ||||||
|     async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { |     async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { | ||||||
|         const learningObject = await findLearningObjectEntityById(id); |         const learningObject = await findLearningObjectEntityById(id); | ||||||
|         return filter(learningObject); |         return convertLearningObject(learningObject); | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -110,7 +110,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = { | ||||||
|                 return learningObject; |                 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 {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. |  * 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. |      * Fetch the learning paths with the given hruids from the database. | ||||||
|      */ |      */ | ||||||
|     fetchLearningPaths(hruids: string[], language: string, source: string): Promise<LearningPathResponse> { |     async fetchLearningPaths(hruids: string[], language: Language, source: string): Promise<LearningPathResponse> { | ||||||
|         throw new Error("Not yet implemented"); // TODO
 |         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 {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. |  * 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. |      * 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. |      * 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 dwengoApiLearningPathProvider from "./dwengo-api-learning-path-provider"; | ||||||
| import databaseLearningPathProvider from "./database-learning-path-provider"; | import databaseLearningPathProvider from "./database-learning-path-provider"; | ||||||
| import {EnvVars, getEnvVar} from "../../util/envvars"; | import {EnvVars, getEnvVar} from "../../util/envvars"; | ||||||
|  | import {Language} from "../../entities/content/language"; | ||||||
| 
 | 
 | ||||||
| const userContentPrefix = getEnvVar(EnvVars.UserContentPrefix); | const userContentPrefix = getEnvVar(EnvVars.UserContentPrefix); | ||||||
| const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider] | const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider] | ||||||
|  | @ -16,7 +17,7 @@ const learningPathService = { | ||||||
|     /** |     /** | ||||||
|      * Fetch the learning paths with the given hruids from the data source. |      * 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 userContentHruids = hruids.filter(hruid => hruid.startsWith(userContentPrefix)); | ||||||
|         const nonUserContentHruids = 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. |      * 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( |         const providerResponses = await Promise.all( | ||||||
|             allProviders.map( |             allProviders.map( | ||||||
|                 provider => provider.searchLearningPaths(query, language) |                 provider => provider.searchLearningPaths(query, language) | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger