• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Lumieducation / H5P-Nodejs-library / a4a7bd59-ecff-4636-8085-80e9e005f68d

pending completion
a4a7bd59-ecff-4636-8085-80e9e005f68d

push

circleci

renovate[bot]
fix(deps): update dependency react-bootstrap to v2.7.2

4507 of 8185 branches covered (55.06%)

Branch coverage included in aggregate %.

8333 of 11382 relevant lines covered (73.21%)

2278.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.74
/packages/h5p-server/src/ContentTypeCache.ts
1
import { AxiosInstance } from 'axios';
2
import * as merge from 'merge';
8✔
3
import * as qs from 'qs';
8✔
4
import { machineIdSync } from 'node-machine-id';
8✔
5

6
import H5pError from './helpers/H5pError';
8✔
7
import Logger from './helpers/Logger';
8✔
8
import {
9
    IH5PConfig,
10
    IHubContentType,
11
    IKeyValueStorage,
12
    IRegistrationData,
13
    IUsageStatistics
14
} from './types';
15
import HttpClient from './helpers/HttpClient';
8✔
16

17
const log = new Logger('ContentTypeCache');
8✔
18

19
/**
20
 * This class caches the information about the content types on the H5P Hub.
21
 *
22
 * IT DOES NOT exactly correspond to the ContentTypeCache of the original PHP implementation,
23
 * as it only caches the data (and converts it to a local format). It DOES NOT add information
24
 * about locally installed libraries and user rights. ContentTypeInformationRepository is meant to do this.
25
 *
26
 * Usage:
27
 * - Get the content type information by calling get().
28
 * - The method updateIfNecessary() should be called regularly, e.g. through a cron-job.
29
 * - Use contentTypeCacheRefreshInterval in the IH5PConfig object to set how often
30
 *   the update should be performed. You can also use forceUpdate() if you want to bypass the
31
 *   interval.
32
 */
33
export default class ContentTypeCache {
8✔
34
    /**
35
     *
36
     * @param config The configuration to use.
37
     * @param storage The storage object.
38
     */
39
    constructor(
40
        private config: IH5PConfig,
51✔
41
        private storage: IKeyValueStorage,
51✔
42
        private getLocalIdOverride?: () => string
51✔
43
    ) {
44
        log.info('initialize');
51✔
45
        this.httpClient = HttpClient(config);
51✔
46
    }
47

48
    private httpClient: AxiosInstance;
49

50
    /**
51
     * Converts an entry from the H5P Hub into a format with flattened versions and integer date values.
52
     * @param entry the entry as received from H5P Hub
53
     * @returns the local content type object
54
     */
55
    private static convertCacheEntryToLocalFormat(entry: any): IHubContentType {
8✔
56
        log.debug(`converting Cache Entry to local format`);
472✔
57
        return {
472✔
58
            categories: entry.categories || [],
476✔
59
            createdAt: Date.parse(entry.createdAt),
60
            description: entry.description,
61
            example: entry.example,
62
            h5pMajorVersion: entry.coreApiVersionNeeded.major,
63
            h5pMinorVersion: entry.coreApiVersionNeeded.minor,
64
            icon: entry.icon,
65
            isRecommended: entry.isRecommended,
66
            keywords: entry.keywords || [],
562✔
67
            license: entry.license,
68
            machineName: entry.id,
69
            majorVersion: entry.version.major,
70
            minorVersion: entry.version.minor,
71
            owner: entry.owner,
72
            patchVersion: entry.version.patch,
73
            popularity: entry.popularity,
74
            screenshots: entry.screenshots,
75
            summary: entry.summary,
76
            title: entry.title,
77
            tutorial: entry.tutorial || '',
665✔
78
            updatedAt: Date.parse(entry.updatedAt)
79
        };
80
    }
81

82
    /**
83
     * Downloads information about available content types from the H5P Hub. This method will
84
     * create a UUID to identify this site if required.
85
     * @returns content types
86
     */
87
    public async downloadContentTypesFromHub(): Promise<any[]> {
8✔
88
        log.info(
17✔
89
            `downloading content types from hub ${this.config.hubContentTypesEndpoint}`
90
        );
91
        await this.registerOrGetUuid();
34✔
92
        let formData = this.compileRegistrationData();
16✔
93
        if (this.config.sendUsageStatistics) {
16!
94
            formData = merge.recursive(
×
95
                true,
96
                formData,
97
                this.compileUsageStatistics()
98
            );
99
        }
100
        const response = await this.httpClient.post(
16✔
101
            this.config.hubContentTypesEndpoint,
102
            qs.stringify(formData)
103
        );
104
        if (response.status !== 200) {
14!
105
            throw new H5pError(
×
106
                'error-communicating-with-hub',
107
                {
108
                    statusCode: response.status.toString(),
109
                    statusText: response.statusText
110
                },
111
                504
112
            );
113
        }
114
        if (!response.data) {
14!
115
            throw new H5pError(
×
116
                'error-communicating-with-hub-no-status',
117
                {},
118
                504
119
            );
120
        }
121

122
        return response.data.contentTypes;
14✔
123
    }
124

125
    /**
126
     * Downloads the content type information from the H5P Hub and stores it in the storage object.
127
     * @returns the downloaded (and saved) cache; undefined if it failed (e.g. because Hub was unreachable)
128
     */
129
    public async forceUpdate(): Promise<any> {
8✔
130
        log.info(`forcing update`);
17✔
131
        let cacheInHubFormat;
132
        try {
133
            cacheInHubFormat = await this.downloadContentTypesFromHub();
17✔
134
            if (!cacheInHubFormat) {
14!
135
                return undefined;
×
136
            }
137
        } catch (error) {
138
            log.error(error);
3✔
139
            return undefined;
3✔
140
        }
141
        const cacheInInternalFormat = cacheInHubFormat.map(
14✔
142
            ContentTypeCache.convertCacheEntryToLocalFormat
143
        );
144
        await this.storage.save('contentTypeCache', cacheInInternalFormat);
28✔
145
        await this.storage.save('contentTypeCacheUpdate', Date.now());
28✔
146
        return cacheInInternalFormat;
14✔
147
    }
148

149
    /**
150
     * Returns the cache data.
151
     * @param machineNames (optional) The method only returns content type cache data for these machine names.
152
     * @returns Cached hub data in a format in which the version objects are flattened into the main object,
153
     */
154
    public async get(...machineNames: string[]): Promise<IHubContentType[]> {
43✔
155
        log.info(`getting content types`);
18✔
156

157
        let cache = await this.storage.load('contentTypeCache');
18✔
158
        if (!cache) {
28✔
159
            log.info(
8✔
160
                'ContentTypeCache was never updated before. Downloading it from the H5P Hub...'
161
            );
162
            // try updating cache if it is empty for some reason
163
            cache = await this.forceUpdate();
8✔
164
            // if the cache is still empty (e.g. because no connection to the H5P Hub can be established, return an empty array)
165
            if (!cache) {
8✔
166
                log.info(
3✔
167
                    'ContentTypeCache could not be retrieved from H5P Hub.'
168
                );
169
                return [];
3✔
170
            }
171
        }
172

173
        if (!machineNames || machineNames.length === 0) {
15✔
174
            return cache;
9✔
175
        }
176
        return cache.filter((contentType: IHubContentType) =>
6✔
177
            machineNames.some(
210✔
178
                (machineName) => machineName === contentType.machineName
210✔
179
            )
180
        );
181
    }
182

183
    /**
184
     * Returns the date and time of the last update of the cache.
185
     * @returns the date and time; undefined if the cache was never updated before.
186
     */
187
    public async getLastUpdate(): Promise<Date> {
8✔
188
        const lastUpdate = await this.storage.load('contentTypeCacheUpdate');
×
189
        return lastUpdate;
×
190
    }
191

192
    /**
193
     * Checks if the cache is not up to date anymore (update interval exceeded).
194
     * @returns true if cache is outdated, false if not
195
     */
196
    public async isOutdated(): Promise<boolean> {
8✔
197
        log.info(`checking if content type cache is up to date`);
13✔
198
        const lastUpdate = await this.storage.load('contentTypeCacheUpdate');
13✔
199
        return (
13✔
200
            !lastUpdate ||
23✔
201
            Date.now() - lastUpdate >
202
                this.config.contentTypeCacheRefreshInterval
203
        );
204
    }
205

206
    /**
207
     * If the running site has already been registered at the H5P hub, this method will
208
     * return the UUID of it. If it hasn't been registered yet, it will do so and store
209
     * the UUID in the storage object.
210
     * @returns uuid
211
     */
212
    public async registerOrGetUuid(): Promise<string> {
8✔
213
        log.info(
20✔
214
            `registering or getting uuid from hub ${this.config.hubRegistrationEndpoint}`
215
        );
216
        if (this.config.uuid && this.config.uuid !== '') {
20!
217
            return this.config.uuid;
×
218
        }
219
        const response = await this.httpClient.post(
20✔
220
            this.config.hubRegistrationEndpoint,
221
            this.compileRegistrationData()
222
        );
223
        if (response.status !== 200) {
18!
224
            throw new H5pError(
×
225
                'error-registering-at-hub',
226
                {
227
                    statusCode: response.status.toString(),
228
                    statusText: response.statusText
229
                },
230
                500
231
            );
232
        }
233
        if (!response.data || !response.data.uuid) {
18!
234
            throw new H5pError('error-registering-at-hub-no-status', {}, 500);
×
235
        }
236
        log.debug(`setting uuid to ${response.data.uuid}`);
18✔
237
        this.config.uuid = response.data.uuid;
18✔
238
        await this.config.save();
36✔
239
        return this.config.uuid;
18✔
240
    }
241

242
    /**
243
     * Checks if the interval between updates has been exceeded and updates the cache if necessary.
244
     * @returns true if cache was updated, false if not
245
     */
246
    public async updateIfNecessary(): Promise<boolean> {
8✔
247
        log.info(`checking if update is necessary`);
11✔
248
        const oldCache = await this.storage.load('contentTypeCache');
11✔
249
        if (!oldCache || (await this.isOutdated())) {
33✔
250
            log.info(`update is necessary`);
9✔
251
            return (await this.forceUpdate()) !== undefined;
9✔
252
        }
253
        log.info(`no update necessary`);
2✔
254
        return false;
2✔
255
    }
256

257
    /**
258
     * @returns An object with the registration data as required by the H5P Hub
259
     */
260
    private compileRegistrationData(): IRegistrationData {
8✔
261
        log.debug(
36✔
262
            `compiling registration data for hub ${this.config.hubRegistrationEndpoint}`
263
        );
264
        return {
36✔
265
            core_api_version: `${this.config.coreApiVersion.major}.${this.config.coreApiVersion.minor}`,
266
            disabled: this.config.fetchingDisabled,
267
            h5p_version: this.config.h5pVersion,
268
            local_id: this.getLocalId(),
269
            platform_name: this.config.platformName,
270
            platform_version: this.config.platformVersion,
271
            type: this.config.siteType,
272
            uuid: this.config.uuid
273
        };
274
    }
275

276
    /**
277
     * @returns An object with usage statistics as required by the H5P Hub
278
     */
279
    private compileUsageStatistics(): IUsageStatistics {
8✔
280
        log.info(`compiling usage statistics`);
×
281
        return {
×
282
            libraries: {}, // TODO: add library information here
283
            num_authors: 0 // number of active authors
284
        };
285
    }
286

287
    /**
288
     * Creates an identifier for the running instance.
289
     * @returns id
290
     */
291
    private getLocalId(): string {
8✔
292
        if (this.getLocalIdOverride) {
36✔
293
            log.debug('Getting local ID from override');
1✔
294
            return this.getLocalIdOverride();
1✔
295
        } else {
296
            log.debug('Generating local id with node-machine-id');
35✔
297
            return machineIdSync();
35✔
298
        }
299
    }
300
}
8✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc