@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.
Files changed (57) hide show
  1. package/README.md +118 -0
  2. package/dist/README.md +118 -0
  3. package/dist/assertions.d.ts +9 -0
  4. package/dist/assertions.js +99 -0
  5. package/dist/aws/securityTokenService.d.ts +2 -0
  6. package/dist/aws/securityTokenService.js +6 -0
  7. package/dist/aws/ssmService.d.ts +2 -0
  8. package/dist/aws/ssmService.js +6 -0
  9. package/dist/config.d.ts +26 -0
  10. package/dist/config.js +110 -0
  11. package/dist/errors.d.ts +12 -0
  12. package/dist/errors.js +32 -0
  13. package/dist/fetch.d.ts +59 -0
  14. package/dist/fetch.js +47 -0
  15. package/dist/guards.d.ts +5 -0
  16. package/dist/guards.js +34 -0
  17. package/dist/index.d.ts +16 -0
  18. package/dist/index.js +120 -0
  19. package/dist/middleware.d.ts +9 -0
  20. package/dist/middleware.js +38 -0
  21. package/dist/package.json +65 -0
  22. package/dist/pagination.d.ts +65 -0
  23. package/dist/pagination.js +83 -0
  24. package/dist/routing.d.ts +100 -0
  25. package/dist/routing.js +237 -0
  26. package/dist/services/apiGateway/index.d.ts +61 -0
  27. package/dist/services/apiGateway/index.js +70 -0
  28. package/dist/services/authProvider/browser.d.ts +17 -0
  29. package/dist/services/authProvider/browser.js +30 -0
  30. package/dist/services/authProvider/decryption.d.ts +16 -0
  31. package/dist/services/authProvider/decryption.js +56 -0
  32. package/dist/services/authProvider/index.d.ts +37 -0
  33. package/dist/services/authProvider/index.js +17 -0
  34. package/dist/services/authProvider/subrequest.d.ts +16 -0
  35. package/dist/services/authProvider/subrequest.js +39 -0
  36. package/dist/services/exercisesGateway/index.d.ts +80 -0
  37. package/dist/services/exercisesGateway/index.js +94 -0
  38. package/dist/services/lrsGateway/attempt-utils.d.ts +60 -0
  39. package/dist/services/lrsGateway/attempt-utils.js +270 -0
  40. package/dist/services/lrsGateway/file-system.d.ts +15 -0
  41. package/dist/services/lrsGateway/file-system.js +126 -0
  42. package/dist/services/lrsGateway/index.d.ts +110 -0
  43. package/dist/services/lrsGateway/index.js +116 -0
  44. package/dist/services/searchProvider/index.d.ts +20 -0
  45. package/dist/services/searchProvider/index.js +2 -0
  46. package/dist/services/searchProvider/memorySearchTheBadWay.d.ts +12 -0
  47. package/dist/services/searchProvider/memorySearchTheBadWay.js +51 -0
  48. package/dist/services/versionedDocumentStore/dynamodb.d.ts +20 -0
  49. package/dist/services/versionedDocumentStore/dynamodb.js +151 -0
  50. package/dist/services/versionedDocumentStore/file-system.d.ts +22 -0
  51. package/dist/services/versionedDocumentStore/file-system.js +112 -0
  52. package/dist/services/versionedDocumentStore/index.d.ts +23 -0
  53. package/dist/services/versionedDocumentStore/index.js +2 -0
  54. package/dist/tsconfig.tsbuildinfo +1 -0
  55. package/dist/types.d.ts +6 -0
  56. package/dist/types.js +2 -0
  57. 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;