refactor(backend): Preparation for learning content from multiple data sources.

Refactored the service layer so that it becomes possible to add another source for learning objects and learning paths.
This commit is contained in:
Gerald Schmittinger 2025-03-03 22:19:09 +01:00
parent 8c22b72b22
commit d7728ddd03
10 changed files with 219 additions and 117 deletions

View file

@ -3,9 +3,9 @@ import {
getLearningObjectById, getLearningObjectById,
getLearningObjectIdsFromPath, getLearningObjectIdsFromPath,
getLearningObjectsFromPath, getLearningObjectsFromPath,
} from '../services/learningObjects.js'; } from '../services/learning-content/dwengo-api/dwengo-api-learning-object-provider.js';
import { FALLBACK_LANG } from '../config.js'; import { FALLBACK_LANG } from '../config.js';
import { FilteredLearningObject } from '../interfaces/learningPath'; import { FilteredLearningObject } from '../interfaces/learningContent';
export async function getAllLearningObjects( export async function getAllLearningObjects(
req: Request, req: Request,

View file

@ -4,7 +4,7 @@ import { FALLBACK_LANG } from '../config.js';
import { import {
fetchLearningPaths, fetchLearningPaths,
searchLearningPaths, searchLearningPaths,
} from '../services/learningPaths.js'; } from '../services/learning-content/dwengo-api/dwengo-api-learning-path-provider.js';
/** /**
* Fetch learning paths based on query parameters. * Fetch learning paths based on query parameters.
*/ */

View file

@ -20,7 +20,7 @@ export interface LearningObjectNode {
updatedAt: string; updatedAt: string;
} }
export interface LearningPath { export interface LearningContent {
_id: string; _id: string;
language: string; language: string;
hruid: string; hruid: string;
@ -93,6 +93,6 @@ export interface FilteredLearningObject {
export interface LearningPathResponse { export interface LearningPathResponse {
success: boolean; success: boolean;
source: string; source: string;
data: LearningPath[] | null; data: LearningContent[] | null;
message?: string; message?: string;
} }

View file

@ -1,13 +1,20 @@
import { DWENGO_API_BASE } from '../config.js'; import { DWENGO_API_BASE } from '../../../config.js';
import { fetchWithLogging } from '../util/apiHelper.js'; import { fetchWithLogging } from '../../../util/apiHelper.js';
import { import {
FilteredLearningObject, FilteredLearningObject,
LearningObjectMetadata, LearningObjectMetadata,
LearningObjectNode, LearningObjectNode,
LearningPathResponse, LearningPathResponse,
} from '../interfaces/learningPath.js'; } from '../../../interfaces/learningContent.js';
import { fetchLearningPaths } from './learningPaths.js'; import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js';
import {LearningObjectProvider} from "../learning-object-provider";
/**
* Helper function to convert the learning object metadata retrieved from the API to a FilteredLearningObject which
* our API should return.
* @param data
* @param htmlUrl
*/
function filterData( function filterData(
data: LearningObjectMetadata, data: LearningObjectMetadata,
htmlUrl: string htmlUrl: string
@ -36,29 +43,7 @@ function filterData(
} }
/** /**
* Fetches a single learning object by its HRUID * Generic helper function to fetch learning objects (full data or just HRUIDs)
*/
export async function getLearningObjectById(
hruid: string,
language: string
): Promise<FilteredLearningObject | null> {
const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`;
const metadata = await fetchWithLogging<LearningObjectMetadata>(
metadataUrl,
`Metadata for Learning Object HRUID "${hruid}" (language ${language})`
);
if (!metadata) {
console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`);
return null;
}
const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw?hruid=${hruid}&language=${language}`;
return filterData(metadata, htmlUrl);
}
/**
* Generic function to fetch learning objects (full data or just HRUIDs)
*/ */
async function fetchLearningObjects( async function fetchLearningObjects(
hruid: string, hruid: string,
@ -67,7 +52,7 @@ async function fetchLearningObjects(
): Promise<FilteredLearningObject[] | string[]> { ): Promise<FilteredLearningObject[] | string[]> {
try { try {
const learningPathResponse: LearningPathResponse = const learningPathResponse: LearningPathResponse =
await fetchLearningPaths( await dwengoApiLearningPathProvider.fetchLearningPaths(
[hruid], [hruid],
language, language,
`Learning path for HRUID "${hruid}"` `Learning path for HRUID "${hruid}"`
@ -93,7 +78,7 @@ async function fetchLearningObjects(
return await Promise.all( return await Promise.all(
nodes.map(async (node) => { nodes.map(async (node) => {
return getLearningObjectById( return dwengoApiLearningObjectProvider.getLearningObjectById(
node.learningobject_hruid, node.learningobject_hruid,
language language
); );
@ -109,26 +94,52 @@ async function fetchLearningObjects(
} }
} }
/** const dwengoApiLearningObjectProvider: LearningObjectProvider = {
* Fetch full learning object data (metadata) /**
* Fetches a single learning object by its HRUID
*/ */
export async function getLearningObjectsFromPath( async getLearningObjectById(
hruid: string, hruid: string,
language: string language: string
): Promise<FilteredLearningObject[]> { ): Promise<FilteredLearningObject | null> {
const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`;
const metadata = await fetchWithLogging<LearningObjectMetadata>(
metadataUrl,
`Metadata for Learning Object HRUID "${hruid}" (language ${language})`
);
if (!metadata) {
console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`);
return null;
}
const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw?hruid=${hruid}&language=${language}`;
return filterData(metadata, htmlUrl);
},
/**
* Fetch full learning object data (metadata)
*/
async getLearningObjectsFromPath(
hruid: string,
language: string
): Promise<FilteredLearningObject[]> {
return (await fetchLearningObjects( return (await fetchLearningObjects(
hruid, hruid,
true, true,
language language
)) as FilteredLearningObject[]; )) as FilteredLearningObject[];
} },
/** /**
* Fetch only learning object HRUIDs * Fetch only learning object HRUIDs
*/ */
export async function getLearningObjectIdsFromPath( async getLearningObjectIdsFromPath(
hruid: string, hruid: string,
language: string language: string
): Promise<string[]> { ): Promise<string[]> {
return (await fetchLearningObjects(hruid, false, language)) as string[]; return (await fetchLearningObjects(hruid, false, language)) as string[];
} }
};
export default dwengoApiLearningObjectProvider;

View file

@ -0,0 +1,65 @@
import { fetchWithLogging } from '../../../util/apiHelper.js';
import { DWENGO_API_BASE } from '../../../config.js';
import {
LearningContent,
LearningPathResponse,
} from '../../../interfaces/learningContent.js';
import {LearningPathProvider} from "../learning-path-provider";
const dwengoApiLearningPathProvider: LearningPathProvider = {
async fetchLearningPaths(
hruids: string[],
language: string,
source: string
): Promise<LearningPathResponse> {
if (hruids.length === 0) {
return {
success: false,
source,
data: null,
message: `No HRUIDs provided for ${source}.`,
};
}
const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`;
const params = { pathIdList: JSON.stringify({ hruids }), language };
const learningPaths = await fetchWithLogging<LearningContent[]>(
apiUrl,
`Learning paths for ${source}`,
params
);
if (!learningPaths || learningPaths.length === 0) {
console.error(`⚠️ WARNING: No learning paths found for ${source}.`);
return {
success: false,
source,
data: [],
message: `No learning paths found for ${source}.`,
};
}
return {
success: true,
source,
data: learningPaths,
};
},
async searchLearningPaths(
query: string,
language: string
): Promise<LearningContent[]> {
const apiUrl = `${DWENGO_API_BASE}/learningPath/search`;
const params = { all: query, language };
const searchResults = await fetchWithLogging<LearningContent[]>(
apiUrl,
`Search learning paths with query "${query}"`,
params
);
return searchResults ?? [];
}
};
export default dwengoApiLearningPathProvider;

View file

@ -0,0 +1,18 @@
import {FilteredLearningObject} from "../../interfaces/learningContent";
export interface LearningObjectProvider {
/**
* Fetches a single learning object by its HRUID
*/
getLearningObjectById(hruid: string, language: string): Promise<FilteredLearningObject | null>;
/**
* Fetch full learning object data (metadata)
*/
getLearningObjectsFromPath(hruid: string, language: string): Promise<FilteredLearningObject[]>;
/**
* Fetch only learning object HRUIDs
*/
getLearningObjectIdsFromPath(hruid: string, language: string): Promise<string[]>;
}

View file

@ -0,0 +1,30 @@
import {FilteredLearningObject} from "../../interfaces/learningContent";
import dwengoApiLearningObjectProvider from "./dwengo-api/dwengo-api-learning-object-provider";
/**
* Service providing access to data about learning objects from the appropriate data source (database or Dwengo-api)
*/
const learningObjectService = {
/**
* Fetches a single learning object by its HRUID
*/
getLearningObjectById(hruid: string, language: string): Promise<FilteredLearningObject | null> {
return dwengoApiLearningObjectProvider.getLearningObjectById(hruid, language);
},
/**
* Fetch full learning object data (metadata)
*/
getLearningObjectsFromPath(hruid: string, language: string): Promise<FilteredLearningObject[]> {
return dwengoApiLearningObjectProvider.getLearningObjectsFromPath(hruid, language);
},
/**
* Fetch only learning object HRUIDs
*/
getLearningObjectIdsFromPath(hruid: string, language: string): Promise<string[]> {
return dwengoApiLearningObjectProvider.getLearningObjectIdsFromPath(hruid, language);
}
};
export default learningObjectService;

View file

@ -0,0 +1,16 @@
import {LearningContent, LearningPathResponse} from "../../interfaces/learningContent";
/**
* Generic interface for a service which provides access to learning paths from a data source.
*/
export interface LearningPathProvider {
/**
* Fetch the learning paths with the given hruids from the data source.
*/
fetchLearningPaths(hruids: string[], language: string, source: string): Promise<LearningPathResponse>;
/**
* Search learning paths in the data source using the given search string.
*/
searchLearningPaths(query: string, language: string): Promise<LearningContent[]>;
}

View file

@ -0,0 +1,23 @@
import {LearningContent, LearningPathResponse} from "../../interfaces/learningContent";
import dwengoApiLearningPathProvider from "./dwengo-api/dwengo-api-learning-path-provider";
/**
* Service providing access to data about learning paths from the appropriate data source (database or Dwengo-api)
*/
const learningPathService = {
/**
* Fetch the learning paths with the given hruids from the data source.
*/
fetchLearningPaths(hruids: string[], language: string, source: string): Promise<LearningPathResponse> {
return dwengoApiLearningPathProvider.fetchLearningPaths(hruids, language, source);
},
/**
* Search learning paths in the data source using the given search string.
*/
searchLearningPaths(query: string, language: string): Promise<LearningContent[]> {
return dwengoApiLearningPathProvider.searchLearningPaths(query, language);
}
}
export default learningPathService;

View file

@ -1,61 +0,0 @@
import { fetchWithLogging } from '../util/apiHelper.js';
import { DWENGO_API_BASE } from '../config.js';
import {
LearningPath,
LearningPathResponse,
} from '../interfaces/learningPath.js';
export async function fetchLearningPaths(
hruids: string[],
language: string,
source: string
): Promise<LearningPathResponse> {
if (hruids.length === 0) {
return {
success: false,
source,
data: null,
message: `No HRUIDs provided for ${source}.`,
};
}
const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`;
const params = { pathIdList: JSON.stringify({ hruids }), language };
const learningPaths = await fetchWithLogging<LearningPath[]>(
apiUrl,
`Learning paths for ${source}`,
params
);
if (!learningPaths || learningPaths.length === 0) {
console.error(`⚠️ WARNING: No learning paths found for ${source}.`);
return {
success: false,
source,
data: [],
message: `No learning paths found for ${source}.`,
};
}
return {
success: true,
source,
data: learningPaths,
};
}
export async function searchLearningPaths(
query: string,
language: string
): Promise<LearningPath[]> {
const apiUrl = `${DWENGO_API_BASE}/learningPath/search`;
const params = { all: query, language };
const searchResults = await fetchWithLogging<LearningPath[]>(
apiUrl,
`Search learning paths with query "${query}"`,
params
);
return searchResults ?? [];
}