@openstax/ts-utils 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -0
- package/dist/README.md +118 -0
- package/dist/assertions.d.ts +9 -0
- package/dist/assertions.js +99 -0
- package/dist/aws/securityTokenService.d.ts +2 -0
- package/dist/aws/securityTokenService.js +6 -0
- package/dist/aws/ssmService.d.ts +2 -0
- package/dist/aws/ssmService.js +6 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.js +110 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +32 -0
- package/dist/fetch.d.ts +59 -0
- package/dist/fetch.js +47 -0
- package/dist/guards.d.ts +5 -0
- package/dist/guards.js +34 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +120 -0
- package/dist/middleware.d.ts +9 -0
- package/dist/middleware.js +38 -0
- package/dist/package.json +65 -0
- package/dist/pagination.d.ts +65 -0
- package/dist/pagination.js +83 -0
- package/dist/routing.d.ts +100 -0
- package/dist/routing.js +237 -0
- package/dist/services/apiGateway/index.d.ts +61 -0
- package/dist/services/apiGateway/index.js +70 -0
- package/dist/services/authProvider/browser.d.ts +17 -0
- package/dist/services/authProvider/browser.js +30 -0
- package/dist/services/authProvider/decryption.d.ts +16 -0
- package/dist/services/authProvider/decryption.js +56 -0
- package/dist/services/authProvider/index.d.ts +37 -0
- package/dist/services/authProvider/index.js +17 -0
- package/dist/services/authProvider/subrequest.d.ts +16 -0
- package/dist/services/authProvider/subrequest.js +39 -0
- package/dist/services/exercisesGateway/index.d.ts +80 -0
- package/dist/services/exercisesGateway/index.js +94 -0
- package/dist/services/lrsGateway/attempt-utils.d.ts +60 -0
- package/dist/services/lrsGateway/attempt-utils.js +270 -0
- package/dist/services/lrsGateway/file-system.d.ts +15 -0
- package/dist/services/lrsGateway/file-system.js +126 -0
- package/dist/services/lrsGateway/index.d.ts +110 -0
- package/dist/services/lrsGateway/index.js +116 -0
- package/dist/services/searchProvider/index.d.ts +20 -0
- package/dist/services/searchProvider/index.js +2 -0
- package/dist/services/searchProvider/memorySearchTheBadWay.d.ts +12 -0
- package/dist/services/searchProvider/memorySearchTheBadWay.js +51 -0
- package/dist/services/versionedDocumentStore/dynamodb.d.ts +20 -0
- package/dist/services/versionedDocumentStore/dynamodb.js +151 -0
- package/dist/services/versionedDocumentStore/file-system.d.ts +22 -0
- package/dist/services/versionedDocumentStore/file-system.js +112 -0
- package/dist/services/versionedDocumentStore/index.d.ts +23 -0
- package/dist/services/versionedDocumentStore/index.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +2 -0
- package/package.json +65 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ConfigProviderForConfig } from '../../config';
|
|
2
|
+
import { GenericFetch } from '../../fetch';
|
|
3
|
+
import { METHOD } from '../../routing';
|
|
4
|
+
export declare type Config = {
|
|
5
|
+
defaultCorrectness?: string;
|
|
6
|
+
exercisesHost: string;
|
|
7
|
+
exercisesAuthToken: string;
|
|
8
|
+
};
|
|
9
|
+
interface Initializer<C> {
|
|
10
|
+
configSpace?: C;
|
|
11
|
+
fetch: GenericFetch;
|
|
12
|
+
}
|
|
13
|
+
export declare type Answer = {
|
|
14
|
+
id: number;
|
|
15
|
+
content_html: string;
|
|
16
|
+
correctness?: string;
|
|
17
|
+
feedback_html?: string;
|
|
18
|
+
};
|
|
19
|
+
export declare type Solution = {
|
|
20
|
+
images: any[];
|
|
21
|
+
solution_type: string;
|
|
22
|
+
content_html: string;
|
|
23
|
+
};
|
|
24
|
+
export declare type Question = {
|
|
25
|
+
id: number;
|
|
26
|
+
is_answer_order_important: boolean;
|
|
27
|
+
stimulus_html: string;
|
|
28
|
+
stem_html: string;
|
|
29
|
+
answers: Answer[];
|
|
30
|
+
hints: string[];
|
|
31
|
+
formats: string[];
|
|
32
|
+
combo_choices: any[];
|
|
33
|
+
collaborator_solutions?: Solution[];
|
|
34
|
+
community_solutions?: Solution[];
|
|
35
|
+
};
|
|
36
|
+
export declare type Exercise = {
|
|
37
|
+
images: any[];
|
|
38
|
+
tags: string[];
|
|
39
|
+
uuid: string;
|
|
40
|
+
group_uuid: string;
|
|
41
|
+
number: number;
|
|
42
|
+
version: number;
|
|
43
|
+
uid: string;
|
|
44
|
+
published_at: string;
|
|
45
|
+
solutions_are_public: boolean;
|
|
46
|
+
authors: any[];
|
|
47
|
+
copyright_holders: any[];
|
|
48
|
+
derived_from: any[];
|
|
49
|
+
is_vocab: boolean;
|
|
50
|
+
questions: Question[];
|
|
51
|
+
delegations: any[];
|
|
52
|
+
versions: number[];
|
|
53
|
+
stimulus_html: string;
|
|
54
|
+
};
|
|
55
|
+
export declare type ExercisesSearchResults = {
|
|
56
|
+
total_count: number;
|
|
57
|
+
items: Exercise[];
|
|
58
|
+
};
|
|
59
|
+
export declare type ExercisesSearchResultsWithDigest = ExercisesSearchResults & {
|
|
60
|
+
digest: string;
|
|
61
|
+
};
|
|
62
|
+
export declare const exercisesGateway: <C extends string = "exercises">(initializer: Initializer<C>) => (configProvider: { [key in C]: {
|
|
63
|
+
defaultCorrectness?: import("../../config").ConfigValueProvider<string> | undefined;
|
|
64
|
+
exercisesHost: import("../../config").ConfigValueProvider<string>;
|
|
65
|
+
exercisesAuthToken: import("../../config").ConfigValueProvider<string>;
|
|
66
|
+
}; }) => {
|
|
67
|
+
searchDigest: (query: string, page?: number, per_page?: number) => Promise<string>;
|
|
68
|
+
get: (uuid: string) => Promise<Exercise | undefined>;
|
|
69
|
+
request: (method: METHOD, path: string, query?: object | undefined) => Promise<{
|
|
70
|
+
status: number;
|
|
71
|
+
headers: {
|
|
72
|
+
get: (name: string) => string | null;
|
|
73
|
+
};
|
|
74
|
+
json: () => Promise<any>;
|
|
75
|
+
text: () => Promise<string>;
|
|
76
|
+
}>;
|
|
77
|
+
search: (query: string, page?: number, per_page?: number) => Promise<ExercisesSearchResultsWithDigest>;
|
|
78
|
+
};
|
|
79
|
+
export declare type ExercisesGateway = ReturnType<ReturnType<typeof exercisesGateway>>;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.exercisesGateway = void 0;
|
|
27
|
+
const queryString = __importStar(require("query-string"));
|
|
28
|
+
const assertions_1 = require("../../assertions");
|
|
29
|
+
const config_1 = require("../../config");
|
|
30
|
+
const guards_1 = require("../../guards");
|
|
31
|
+
const routing_1 = require("../../routing");
|
|
32
|
+
const exercisesGateway = (initializer) => (configProvider) => {
|
|
33
|
+
const config = configProvider[(0, guards_1.ifDefined)(initializer.configSpace, 'exercises')];
|
|
34
|
+
const exercisesHost = (0, config_1.resolveConfigValue)(config.exercisesHost);
|
|
35
|
+
const exercisesAuthToken = (0, config_1.resolveConfigValue)(config.exercisesAuthToken);
|
|
36
|
+
const defaultCorrectness = (0, config_1.resolveConfigValue)(config.defaultCorrectness || '');
|
|
37
|
+
const doDefaultCorrectness = async (exercise) => {
|
|
38
|
+
if (await defaultCorrectness !== 'true') {
|
|
39
|
+
return exercise;
|
|
40
|
+
}
|
|
41
|
+
for (const question of exercise.questions) {
|
|
42
|
+
const existingCorrect = question.answers.find(answer => answer.correctness !== undefined);
|
|
43
|
+
if (question.answers.length < 1 || existingCorrect) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const defaultCorrectIndex = question.id % question.answers.length;
|
|
47
|
+
const defaultCorrect = question.answers[defaultCorrectIndex];
|
|
48
|
+
const defaultHint = `<em>random default: the correct answer is ${defaultCorrect.id}: ${defaultCorrect.content_html.slice(0, 20)}</em>`;
|
|
49
|
+
question.stem_html += `\n<br>${defaultHint}`;
|
|
50
|
+
question.collaborator_solutions = [
|
|
51
|
+
{ solution_type: 'detailed', images: [], content_html: defaultHint }
|
|
52
|
+
];
|
|
53
|
+
for (const [index, answer] of question.answers.entries()) {
|
|
54
|
+
answer.correctness = defaultCorrectIndex === index ? '1.0' : '0.0';
|
|
55
|
+
answer.feedback_html = defaultCorrectIndex === index ? 'This is the good one!' : defaultHint;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return exercise;
|
|
59
|
+
};
|
|
60
|
+
const request = async (method, path, query = undefined) => {
|
|
61
|
+
const host = (await exercisesHost).replace(/\/+$/, '');
|
|
62
|
+
const baseUrl = `${host}/api/${path}`;
|
|
63
|
+
const url = query ? `${baseUrl}?${queryString.stringify(query)}` : baseUrl;
|
|
64
|
+
return initializer.fetch(url, {
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `Bearer ${await exercisesAuthToken}`,
|
|
67
|
+
},
|
|
68
|
+
method,
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
const searchDigest = async (query, page = 1, per_page = 100) => {
|
|
72
|
+
const response = await request(routing_1.METHOD.HEAD, 'exercises', { query, page, per_page });
|
|
73
|
+
return (0, assertions_1.assertString)(response.headers.get('X-Digest'), 'OpenStax Exercises search endpoint HEAD did not return an X-Digest header');
|
|
74
|
+
};
|
|
75
|
+
const search = async (query, page = 1, per_page = 100) => {
|
|
76
|
+
const response = await request(routing_1.METHOD.GET, 'exercises', { query, page, per_page });
|
|
77
|
+
const digest = (0, assertions_1.assertString)(response.headers.get('X-Digest'), 'OpenStax Exercises search endpoint GET did not return an X-Digest header');
|
|
78
|
+
const { items, total_count } = await response.json();
|
|
79
|
+
return { digest, items: await Promise.all(items.map(doDefaultCorrectness)), total_count };
|
|
80
|
+
};
|
|
81
|
+
const get = async (uuid) => {
|
|
82
|
+
const response = await request(routing_1.METHOD.GET, `exercises/${uuid}`);
|
|
83
|
+
return response.status === 404
|
|
84
|
+
? undefined
|
|
85
|
+
: response.json().then(doDefaultCorrectness);
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
searchDigest,
|
|
89
|
+
get,
|
|
90
|
+
request,
|
|
91
|
+
search,
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
exports.exercisesGateway = exercisesGateway;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { LrsGateway, XapiStatement } from '.';
|
|
2
|
+
export declare type ActivityState = {
|
|
3
|
+
attempts: number;
|
|
4
|
+
completedAttempts: number;
|
|
5
|
+
currentAttempt?: XapiStatement;
|
|
6
|
+
currentAttemptCompleted?: XapiStatement;
|
|
7
|
+
currentAttemptStatements: XapiStatement[];
|
|
8
|
+
mostRecentAttemptWithCompleted?: XapiStatement;
|
|
9
|
+
mostRecentAttemptWithCompletedCompleted?: XapiStatement;
|
|
10
|
+
};
|
|
11
|
+
export declare const matchAttempt: (statement: XapiStatement) => boolean;
|
|
12
|
+
export declare const matchAttemptCompleted: (attempt: XapiStatement) => (statement: XapiStatement) => boolean;
|
|
13
|
+
export declare const resolveActivityAttempts: (statements: XapiStatement[], activityIRI: string, parentActivityAttempt?: string | undefined) => XapiStatement[];
|
|
14
|
+
export declare const resolveCompletedForAttempt: (statements: XapiStatement[], activityIRI: string, attempt: XapiStatement) => XapiStatement | undefined;
|
|
15
|
+
export declare const mostRecentStatement: (statements: XapiStatement[]) => XapiStatement | undefined;
|
|
16
|
+
export declare const resolveActivityAttemptInfo: (statements: XapiStatement[], activityIRI: string, options?: {
|
|
17
|
+
currentAttempt?: string | undefined;
|
|
18
|
+
parentActivityAttempt?: string | undefined;
|
|
19
|
+
} | undefined) => ActivityState;
|
|
20
|
+
export declare const loadStatementsForActivityAndFirstChildren: (gateway: LrsGateway, activityIRI: string, attempt?: string | undefined) => Promise<XapiStatement[]>;
|
|
21
|
+
export declare const loadStatementsForAttempt: (gateway: LrsGateway, attempt: string) => Promise<XapiStatement[]>;
|
|
22
|
+
export declare const loadStatementsForActivity: (gateway: LrsGateway, activityIRI: string, attempt?: string | undefined) => Promise<XapiStatement[]>;
|
|
23
|
+
export declare const loadActivityAttemptInfo: (gateway: LrsGateway, activityIRI: string, options?: {
|
|
24
|
+
currentAttempt?: string | undefined;
|
|
25
|
+
parentActivityAttempt?: string | undefined;
|
|
26
|
+
} | undefined) => Promise<ActivityState>;
|
|
27
|
+
export declare const createStatement: (verb: XapiStatement['verb'], activity: {
|
|
28
|
+
iri: string;
|
|
29
|
+
type: string;
|
|
30
|
+
name: string;
|
|
31
|
+
extensions?: {
|
|
32
|
+
[key: string]: string;
|
|
33
|
+
} | undefined;
|
|
34
|
+
}, attempt: string, parentActivityIRI?: string | undefined) => Pick<XapiStatement, 'object' | 'verb' | 'context'>;
|
|
35
|
+
export declare const createAttemptStatement: (activity: {
|
|
36
|
+
iri: string;
|
|
37
|
+
type: string;
|
|
38
|
+
name: string;
|
|
39
|
+
extensions?: {
|
|
40
|
+
[key: string]: string;
|
|
41
|
+
} | undefined;
|
|
42
|
+
}, parentActivity?: {
|
|
43
|
+
iri?: string | undefined;
|
|
44
|
+
attempt?: string | undefined;
|
|
45
|
+
} | undefined) => Pick<XapiStatement, 'object' | 'verb' | 'context'>;
|
|
46
|
+
export declare const putAttemptStatement: (gateway: LrsGateway, activity: {
|
|
47
|
+
iri: string;
|
|
48
|
+
type: string;
|
|
49
|
+
name: string;
|
|
50
|
+
extensions?: {
|
|
51
|
+
[key: string]: string;
|
|
52
|
+
} | undefined;
|
|
53
|
+
}, parentActivity?: {
|
|
54
|
+
iri?: string | undefined;
|
|
55
|
+
attempt?: string | undefined;
|
|
56
|
+
} | undefined) => Promise<import(".").EagerXapiStatement>;
|
|
57
|
+
export declare const createAttemptActivityStatement: (attemptStatement: XapiStatement, verb: XapiStatement['verb'], result?: XapiStatement['result']) => Pick<XapiStatement, 'object' | 'verb' | 'context' | 'result'>;
|
|
58
|
+
export declare const putAttemptActivityStatement: (gateway: LrsGateway, attemptStatement: XapiStatement, verb: XapiStatement['verb'], result?: XapiStatement['result']) => Promise<import(".").EagerXapiStatement>;
|
|
59
|
+
export declare const createCompletedStatement: (attemptStatement: XapiStatement, result?: XapiStatement['result']) => Pick<XapiStatement, 'object' | 'verb' | 'context' | 'result'>;
|
|
60
|
+
export declare const putCompletedStatement: (gateway: LrsGateway, attemptStatement: XapiStatement, result: XapiStatement['result']) => Promise<import(".").EagerXapiStatement>;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.putCompletedStatement = exports.createCompletedStatement = exports.putAttemptActivityStatement = exports.createAttemptActivityStatement = exports.putAttemptStatement = exports.createAttemptStatement = exports.createStatement = exports.loadActivityAttemptInfo = exports.loadStatementsForActivity = exports.loadStatementsForAttempt = exports.loadStatementsForActivityAndFirstChildren = exports.resolveActivityAttemptInfo = exports.mostRecentStatement = exports.resolveCompletedForAttempt = exports.resolveActivityAttempts = exports.matchAttemptCompleted = exports.matchAttempt = void 0;
|
|
7
|
+
/*
|
|
8
|
+
* the structure of xapi statements for handling multiple attempts of an activity
|
|
9
|
+
* including the option of a parent activity/attempt such that a new attempt
|
|
10
|
+
* of a parent activity inherently creates a new attempt scope for sub-activities
|
|
11
|
+
* is done by convention using certain context and verb pieces of a statement.
|
|
12
|
+
* this module provides helpers for creating and retrieving statements according
|
|
13
|
+
* to this convention.
|
|
14
|
+
*/
|
|
15
|
+
const formatISODuration_1 = __importDefault(require("date-fns/formatISODuration"));
|
|
16
|
+
const intervalToDuration_1 = __importDefault(require("date-fns/intervalToDuration"));
|
|
17
|
+
const isAfter_1 = __importDefault(require("date-fns/isAfter"));
|
|
18
|
+
const parseISO_1 = __importDefault(require("date-fns/parseISO"));
|
|
19
|
+
var Verb;
|
|
20
|
+
(function (Verb) {
|
|
21
|
+
Verb["Attempted"] = "http://adlnet.gov/expapi/verbs/attempted";
|
|
22
|
+
Verb["Completed"] = "http://adlnet.gov/expapi/verbs/completed";
|
|
23
|
+
})(Verb || (Verb = {}));
|
|
24
|
+
const matchAttempt = (statement) => statement.verb.id === Verb.Attempted;
|
|
25
|
+
exports.matchAttempt = matchAttempt;
|
|
26
|
+
const matchAttemptCompleted = (attempt) => (statement) => {
|
|
27
|
+
var _a, _b;
|
|
28
|
+
return statement.verb.id === Verb.Completed
|
|
29
|
+
&& statement.context !== undefined
|
|
30
|
+
&& ((_a = statement.context.statement) === null || _a === void 0 ? void 0 : _a.id) === attempt.id
|
|
31
|
+
&& statement.context.registration === ((_b = attempt.context) === null || _b === void 0 ? void 0 : _b.registration);
|
|
32
|
+
};
|
|
33
|
+
exports.matchAttemptCompleted = matchAttemptCompleted;
|
|
34
|
+
const resolveActivityAttempts = (statements, activityIRI, parentActivityAttempt) => statements.filter(statement => {
|
|
35
|
+
var _a;
|
|
36
|
+
return (0, exports.matchAttempt)(statement)
|
|
37
|
+
&& statement.object.id === activityIRI
|
|
38
|
+
&& (!parentActivityAttempt || ((_a = statement.context) === null || _a === void 0 ? void 0 : _a.registration) === parentActivityAttempt);
|
|
39
|
+
});
|
|
40
|
+
exports.resolveActivityAttempts = resolveActivityAttempts;
|
|
41
|
+
const resolveCompletedForAttempt = (statements, activityIRI, attempt) => statements.find(statement => (0, exports.matchAttemptCompleted)(attempt)(statement)
|
|
42
|
+
&& statement.object.id === activityIRI);
|
|
43
|
+
exports.resolveCompletedForAttempt = resolveCompletedForAttempt;
|
|
44
|
+
const mostRecentStatement = (statements) => statements.reduce((result, statement) => result && (0, isAfter_1.default)((0, parseISO_1.default)(result.timestamp), (0, parseISO_1.default)(statement.timestamp)) ? result : statement, statements[0]);
|
|
45
|
+
exports.mostRecentStatement = mostRecentStatement;
|
|
46
|
+
const resolveActivityAttemptInfo = (statements, activityIRI, options) => {
|
|
47
|
+
// TODO optimize. i'm 100% that this could all be done in one iteration but i'm not messing around with that for now.
|
|
48
|
+
const attempts = (0, exports.resolveActivityAttempts)(statements, activityIRI, options === null || options === void 0 ? void 0 : options.parentActivityAttempt);
|
|
49
|
+
/* attempts that have a completed statement */
|
|
50
|
+
const completedAttempts = attempts.filter(attempt => !!(0, exports.resolveCompletedForAttempt)(statements, activityIRI, attempt));
|
|
51
|
+
/* the last attempt sorted by timestamp */
|
|
52
|
+
const currentAttempt = (options === null || options === void 0 ? void 0 : options.currentAttempt)
|
|
53
|
+
? attempts.find(attempt => attempt.id === options.currentAttempt)
|
|
54
|
+
: (0, exports.mostRecentStatement)(attempts);
|
|
55
|
+
/* all statements for the current attempt (doesn't include the attempt or completed statements) */
|
|
56
|
+
const currentAttemptStatements = currentAttempt ? statements.filter(statement => {
|
|
57
|
+
var _a;
|
|
58
|
+
return statement.object.id === activityIRI
|
|
59
|
+
&& ((_a = statement.context) === null || _a === void 0 ? void 0 : _a.registration) === currentAttempt.id;
|
|
60
|
+
}) : [];
|
|
61
|
+
const currentAttemptCompleted = currentAttempt && (0, exports.resolveCompletedForAttempt)(statements, activityIRI, currentAttempt);
|
|
62
|
+
const mostRecentAttemptWithCompleted = completedAttempts.reduce((current, attempt) => current && (0, isAfter_1.default)((0, parseISO_1.default)(current.timestamp), (0, parseISO_1.default)(attempt.timestamp)) ? current : attempt, completedAttempts[0]);
|
|
63
|
+
const mostRecentAttemptWithCompletedCompleted = mostRecentAttemptWithCompleted
|
|
64
|
+
&& (0, exports.resolveCompletedForAttempt)(statements, activityIRI, mostRecentAttemptWithCompleted);
|
|
65
|
+
/*
|
|
66
|
+
* the structure allows for the possibility of multiple incomplete attempts.
|
|
67
|
+
* the implementation can choose at its discretion to ignore the currentAttempt
|
|
68
|
+
* and instead make a new one, for instance if the implementation desires
|
|
69
|
+
* an attempt timeout feature
|
|
70
|
+
*/
|
|
71
|
+
return {
|
|
72
|
+
attempts: attempts.length,
|
|
73
|
+
completedAttempts: completedAttempts.length,
|
|
74
|
+
currentAttempt,
|
|
75
|
+
currentAttemptCompleted: currentAttemptCompleted,
|
|
76
|
+
currentAttemptStatements,
|
|
77
|
+
mostRecentAttemptWithCompleted,
|
|
78
|
+
mostRecentAttemptWithCompletedCompleted,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
exports.resolveActivityAttemptInfo = resolveActivityAttemptInfo;
|
|
82
|
+
/*
|
|
83
|
+
* loads all statements (for this actor) that have the given activityIRI as the object.id or the context.contextActivities.parent.id
|
|
84
|
+
*
|
|
85
|
+
* note: if you filter on attempt you're only gonna get the `Attempted` statements from the child activities, subsequent child activity
|
|
86
|
+
* statements would then have to be fetched using `loadStatementsForActivity(gateway, childActivityIRI, childAttemptStatementID)`. this
|
|
87
|
+
* is because child activities could have multiple attempts under one attempt on the parent activity.
|
|
88
|
+
*/
|
|
89
|
+
const loadStatementsForActivityAndFirstChildren = (gateway, activityIRI, attempt) => {
|
|
90
|
+
return gateway.getAllXapiStatements({
|
|
91
|
+
activity: activityIRI,
|
|
92
|
+
related_activities: true,
|
|
93
|
+
...(attempt ? { registration: attempt } : {})
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
exports.loadStatementsForActivityAndFirstChildren = loadStatementsForActivityAndFirstChildren;
|
|
97
|
+
/*
|
|
98
|
+
* loads all statements (for this actor) that have the given parent attempt (registration)
|
|
99
|
+
*/
|
|
100
|
+
const loadStatementsForAttempt = (gateway, attempt) => {
|
|
101
|
+
return gateway.getAllXapiStatements({
|
|
102
|
+
registration: attempt
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
exports.loadStatementsForAttempt = loadStatementsForAttempt;
|
|
106
|
+
/*
|
|
107
|
+
* loads all statements (for this actor) that have the given activityIRI as the object.id
|
|
108
|
+
*/
|
|
109
|
+
const loadStatementsForActivity = (gateway, activityIRI, attempt) => {
|
|
110
|
+
return gateway.getAllXapiStatements({
|
|
111
|
+
activity: activityIRI,
|
|
112
|
+
...(attempt ? { registration: attempt } : {})
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
exports.loadStatementsForActivity = loadStatementsForActivity;
|
|
116
|
+
const loadActivityAttemptInfo = async (gateway, activityIRI, options) => {
|
|
117
|
+
return (0, exports.resolveActivityAttemptInfo)(await (0, exports.loadStatementsForActivity)(gateway, activityIRI, options === null || options === void 0 ? void 0 : options.parentActivityAttempt), activityIRI, options);
|
|
118
|
+
};
|
|
119
|
+
exports.loadActivityAttemptInfo = loadActivityAttemptInfo;
|
|
120
|
+
const createStatement = (verb, activity, attempt, parentActivityIRI) => {
|
|
121
|
+
return {
|
|
122
|
+
context: {
|
|
123
|
+
...(parentActivityIRI ? {
|
|
124
|
+
contextActivities: {
|
|
125
|
+
parent: [
|
|
126
|
+
{
|
|
127
|
+
id: parentActivityIRI,
|
|
128
|
+
objectType: 'Activity',
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
} : {}),
|
|
133
|
+
registration: attempt,
|
|
134
|
+
},
|
|
135
|
+
object: {
|
|
136
|
+
definition: {
|
|
137
|
+
extensions: {
|
|
138
|
+
...activity.extensions
|
|
139
|
+
},
|
|
140
|
+
name: {
|
|
141
|
+
'en-US': activity.name,
|
|
142
|
+
},
|
|
143
|
+
type: activity.type,
|
|
144
|
+
},
|
|
145
|
+
id: activity.iri,
|
|
146
|
+
objectType: 'Activity'
|
|
147
|
+
},
|
|
148
|
+
verb,
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
exports.createStatement = createStatement;
|
|
152
|
+
/*
|
|
153
|
+
* activity:
|
|
154
|
+
* - iri: the IRI formatted id for this activity
|
|
155
|
+
* - type: the IRI formatted activity type (eg: http://id.tincanapi.com/activitytype/school-assignment)
|
|
156
|
+
* - name: the plaintext name of the activity, for reporting
|
|
157
|
+
*
|
|
158
|
+
* parentActivity:
|
|
159
|
+
* - iri: the IRI formatted id for the parent activity
|
|
160
|
+
* - attempt: the statement id for the parent attempt (the object of which should be the parentActivity.iri)
|
|
161
|
+
*/
|
|
162
|
+
const createAttemptStatement = (activity, parentActivity) => {
|
|
163
|
+
return {
|
|
164
|
+
...((parentActivity === null || parentActivity === void 0 ? void 0 : parentActivity.iri) || (parentActivity === null || parentActivity === void 0 ? void 0 : parentActivity.attempt) ? {
|
|
165
|
+
context: {
|
|
166
|
+
...(parentActivity.iri ? {
|
|
167
|
+
contextActivities: {
|
|
168
|
+
parent: [
|
|
169
|
+
{
|
|
170
|
+
id: parentActivity.iri,
|
|
171
|
+
objectType: 'Activity',
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
} : {}),
|
|
176
|
+
...(parentActivity.attempt ? {
|
|
177
|
+
registration: parentActivity.attempt,
|
|
178
|
+
} : {})
|
|
179
|
+
},
|
|
180
|
+
} : {}),
|
|
181
|
+
object: {
|
|
182
|
+
definition: {
|
|
183
|
+
extensions: {
|
|
184
|
+
...activity.extensions
|
|
185
|
+
},
|
|
186
|
+
name: {
|
|
187
|
+
'en-US': activity.name,
|
|
188
|
+
},
|
|
189
|
+
type: activity.type,
|
|
190
|
+
},
|
|
191
|
+
id: activity.iri,
|
|
192
|
+
objectType: 'Activity'
|
|
193
|
+
},
|
|
194
|
+
verb: {
|
|
195
|
+
display: { 'en-US': 'Attempted' },
|
|
196
|
+
id: Verb.Attempted,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
exports.createAttemptStatement = createAttemptStatement;
|
|
201
|
+
/* resolves with the statement id */
|
|
202
|
+
const putAttemptStatement = async (gateway, activity, parentActivity) => {
|
|
203
|
+
return await gateway.putXapiStatements([(0, exports.createAttemptStatement)(activity, parentActivity)])
|
|
204
|
+
.then(statements => statements[0]);
|
|
205
|
+
};
|
|
206
|
+
exports.putAttemptStatement = putAttemptStatement;
|
|
207
|
+
/*
|
|
208
|
+
* creates a statement under the given attempt.
|
|
209
|
+
*
|
|
210
|
+
* `result` optional context for the attempt result (score, selected answer, etc)
|
|
211
|
+
*/
|
|
212
|
+
const createAttemptActivityStatement = (attemptStatement, verb, result) => {
|
|
213
|
+
var _a;
|
|
214
|
+
return {
|
|
215
|
+
context: {
|
|
216
|
+
...(((_a = attemptStatement.context) === null || _a === void 0 ? void 0 : _a.contextActivities) ? {
|
|
217
|
+
contextActivities: attemptStatement.context.contextActivities,
|
|
218
|
+
} : {}),
|
|
219
|
+
registration: attemptStatement.id,
|
|
220
|
+
},
|
|
221
|
+
object: attemptStatement.object,
|
|
222
|
+
verb: verb,
|
|
223
|
+
...(result ? { result } : {}),
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
exports.createAttemptActivityStatement = createAttemptActivityStatement;
|
|
227
|
+
const putAttemptActivityStatement = async (gateway, attemptStatement, verb, result) => {
|
|
228
|
+
return await gateway.putXapiStatements([(0, exports.createAttemptActivityStatement)(attemptStatement, verb, result)])
|
|
229
|
+
.then(statements => statements[0]);
|
|
230
|
+
};
|
|
231
|
+
exports.putAttemptActivityStatement = putAttemptActivityStatement;
|
|
232
|
+
/*
|
|
233
|
+
* creates a statement that completes the given attempt.
|
|
234
|
+
*
|
|
235
|
+
* `result` optional context for the attempt result (score, selected answer, etc)
|
|
236
|
+
*/
|
|
237
|
+
const createCompletedStatement = (attemptStatement, result) => {
|
|
238
|
+
var _a, _b;
|
|
239
|
+
return {
|
|
240
|
+
context: {
|
|
241
|
+
...(((_a = attemptStatement.context) === null || _a === void 0 ? void 0 : _a.contextActivities) ? {
|
|
242
|
+
contextActivities: attemptStatement.context.contextActivities,
|
|
243
|
+
} : {}),
|
|
244
|
+
...(((_b = attemptStatement.context) === null || _b === void 0 ? void 0 : _b.registration) ? {
|
|
245
|
+
registration: attemptStatement.context.registration,
|
|
246
|
+
} : {}),
|
|
247
|
+
statement: {
|
|
248
|
+
objectType: 'StatementRef',
|
|
249
|
+
id: attemptStatement.id,
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
object: attemptStatement.object,
|
|
253
|
+
verb: {
|
|
254
|
+
display: { 'en-US': 'Completed' },
|
|
255
|
+
id: Verb.Completed,
|
|
256
|
+
},
|
|
257
|
+
result: {
|
|
258
|
+
duration: (0, formatISODuration_1.default)((0, intervalToDuration_1.default)({
|
|
259
|
+
start: (0, parseISO_1.default)(attemptStatement.timestamp), end: new Date()
|
|
260
|
+
})),
|
|
261
|
+
...result,
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
exports.createCompletedStatement = createCompletedStatement;
|
|
266
|
+
const putCompletedStatement = async (gateway, attemptStatement, result) => {
|
|
267
|
+
return await gateway.putXapiStatements([(0, exports.createCompletedStatement)(attemptStatement, result)])
|
|
268
|
+
.then(statements => statements[0]);
|
|
269
|
+
};
|
|
270
|
+
exports.putCompletedStatement = putCompletedStatement;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ConfigProviderForConfig } from '../../config';
|
|
2
|
+
import { AuthProvider } from '../authProvider';
|
|
3
|
+
import { LrsGateway } from '.';
|
|
4
|
+
declare type Config = {
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
interface Initializer<C> {
|
|
8
|
+
dataDir: string;
|
|
9
|
+
fs?: Pick<typeof import('fs'), 'readFile' | 'writeFile'>;
|
|
10
|
+
configSpace?: C;
|
|
11
|
+
}
|
|
12
|
+
export declare const fileSystemLrsGateway: <C extends string = "fileSystem">(initializer: Initializer<C>) => (configProvider: { [key in C]: {
|
|
13
|
+
name: import("../../config").ConfigValueProvider<string>;
|
|
14
|
+
}; }) => (authProvider: AuthProvider) => LrsGateway;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.fileSystemLrsGateway = void 0;
|
|
30
|
+
const fsModule = __importStar(require("fs"));
|
|
31
|
+
const path_1 = __importDefault(require("path"));
|
|
32
|
+
const formatISO_1 = __importDefault(require("date-fns/formatISO"));
|
|
33
|
+
const uuid_1 = require("uuid");
|
|
34
|
+
const assertions_1 = require("../../assertions");
|
|
35
|
+
const config_1 = require("../../config");
|
|
36
|
+
const errors_1 = require("../../errors");
|
|
37
|
+
const guards_1 = require("../../guards");
|
|
38
|
+
const pageSize = 5;
|
|
39
|
+
const fileSystemLrsGateway = (initializer) => (configProvider) => (authProvider) => {
|
|
40
|
+
const name = (0, config_1.resolveConfigValue)(configProvider[initializer.configSpace || 'fileSystem'].name);
|
|
41
|
+
const filePath = name.then((fileName) => path_1.default.join(initializer.dataDir, fileName));
|
|
42
|
+
const { readFile, writeFile } = (0, guards_1.ifDefined)(initializer.fs, fsModule);
|
|
43
|
+
let data;
|
|
44
|
+
const load = filePath.then(path => new Promise(resolve => {
|
|
45
|
+
readFile(path, (err, readData) => {
|
|
46
|
+
if (err) {
|
|
47
|
+
console.error(err);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
try {
|
|
51
|
+
data = JSON.parse(readData.toString());
|
|
52
|
+
if (typeof data !== 'object' || !(data instanceof Array)) {
|
|
53
|
+
data = undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.error(e);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
resolve();
|
|
61
|
+
});
|
|
62
|
+
}));
|
|
63
|
+
let previousSave;
|
|
64
|
+
const putXapiStatements = async (statements) => {
|
|
65
|
+
const user = (0, assertions_1.assertDefined)(await authProvider.getUser(), new errors_1.UnauthorizedError);
|
|
66
|
+
const statementsWithDefaults = statements.map(statement => ({
|
|
67
|
+
...statement,
|
|
68
|
+
id: (0, uuid_1.v4)(),
|
|
69
|
+
actor: {
|
|
70
|
+
account: {
|
|
71
|
+
homePage: 'https://openstax.org',
|
|
72
|
+
name: user.uuid,
|
|
73
|
+
},
|
|
74
|
+
objectType: 'Agent',
|
|
75
|
+
},
|
|
76
|
+
timestamp: (0, formatISO_1.default)(new Date()),
|
|
77
|
+
}));
|
|
78
|
+
await load;
|
|
79
|
+
await previousSave;
|
|
80
|
+
const path = await filePath;
|
|
81
|
+
const save = previousSave = new Promise(resolve => {
|
|
82
|
+
data = data || [];
|
|
83
|
+
data.push(...statementsWithDefaults.map(statement => ({ ...statement, stored: statement.timestamp })));
|
|
84
|
+
writeFile(path, JSON.stringify(data, null, 2), () => resolve());
|
|
85
|
+
});
|
|
86
|
+
await save;
|
|
87
|
+
return statementsWithDefaults;
|
|
88
|
+
};
|
|
89
|
+
const getAllXapiStatements = async ({ user, anyUser, ...options }) => {
|
|
90
|
+
const authUser = await authProvider.getUser();
|
|
91
|
+
await load;
|
|
92
|
+
return (data || []).filter(statement => {
|
|
93
|
+
var _a, _b, _c, _d;
|
|
94
|
+
return (anyUser === true || statement.actor.account.name === (user || (0, assertions_1.assertDefined)(authUser, new errors_1.UnauthorizedError()).uuid))
|
|
95
|
+
&& (!options.verb || statement.verb.id === options.verb)
|
|
96
|
+
&& (!options.registration || ((_a = statement.context) === null || _a === void 0 ? void 0 : _a.registration) === options.registration)
|
|
97
|
+
&& (!options.activity || (options.related_activities
|
|
98
|
+
? ((statement.object.id === options.activity && statement.object.objectType === 'Activity')
|
|
99
|
+
|| (!!((_d = (_c = (_b = statement.context) === null || _b === void 0 ? void 0 : _b.contextActivities) === null || _c === void 0 ? void 0 : _c.parent) === null || _d === void 0 ? void 0 : _d.find(parent => parent.id === options.activity && parent.objectType === 'Activity'))))
|
|
100
|
+
: (statement.object.id === options.activity && statement.object.objectType === 'Activity')));
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
const getMoreXapiStatements = async (more) => {
|
|
104
|
+
const { args, offset } = JSON.parse(more);
|
|
105
|
+
const allResults = await getAllXapiStatements(...args);
|
|
106
|
+
const end = offset + pageSize;
|
|
107
|
+
return {
|
|
108
|
+
more: allResults.length > end ? JSON.stringify({ args, offset: end }) : '',
|
|
109
|
+
statements: allResults.slice(offset, end)
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
const getXapiStatements = async (...args) => {
|
|
113
|
+
const allResults = await getAllXapiStatements(...args);
|
|
114
|
+
return {
|
|
115
|
+
more: allResults.length > pageSize ? JSON.stringify({ args, offset: pageSize }) : '',
|
|
116
|
+
statements: allResults.slice(0, pageSize)
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
putXapiStatements,
|
|
121
|
+
getAllXapiStatements,
|
|
122
|
+
getXapiStatements,
|
|
123
|
+
getMoreXapiStatements,
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
exports.fileSystemLrsGateway = fileSystemLrsGateway;
|