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)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue