@openstax/ts-utils 1.30.4 → 1.31.1

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.
@@ -0,0 +1 @@
1
+ export declare const assertNoUndefined: (obj: any, path?: string[]) => void;
@@ -0,0 +1,10 @@
1
+ export const assertNoUndefined = (obj, path = []) => {
2
+ if (obj === undefined) {
3
+ throw new Error(`unknown attribute type ${typeof obj} with value ${obj} at ${path.join('.') || 'root'}.`);
4
+ }
5
+ if (obj && typeof obj === 'object') {
6
+ for (const [key, value] of Object.entries(obj)) {
7
+ assertNoUndefined(value, [...path, key]);
8
+ }
9
+ }
10
+ };
@@ -4,6 +4,7 @@ import { hashValue } from '../../..';
4
4
  import { resolveConfigValue } from '../../../config';
5
5
  import { ConflictError, NotFoundError } from '../../../errors';
6
6
  import { ifDefined, isDefined } from '../../../guards';
7
+ import { assertNoUndefined } from '../fileSystemAssert';
7
8
  export const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvider) => (_, hashKey, options) => {
8
9
  const tableName = resolveConfigValue(configProvider[initializer.configSpace || 'fileSystem'].tableName);
9
10
  const tablePath = tableName.then((table) => path.join(initializer.dataDir, table));
@@ -42,6 +43,7 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
42
43
  return {
43
44
  loadAllDocumentsTheBadWay,
44
45
  getItemsByField: async (key, value, pageKey) => {
46
+ assertNoUndefined(value, [key]);
45
47
  const pageSize = 10;
46
48
  const items = await loadAllDocumentsTheBadWay();
47
49
  const filteredItems = items.filter((item) => item[key] === value);
@@ -53,11 +55,18 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
53
55
  return { items: paginatedItems, nextPageToken };
54
56
  },
55
57
  batchGetItem: async (ids) => {
56
- const items = await Promise.all(ids.map((id) => load(hashFilename(id))));
58
+ const items = await Promise.all(ids.map((id) => {
59
+ assertNoUndefined(id, ['id']);
60
+ return load(hashFilename(id));
61
+ }));
57
62
  return items.filter(isDefined);
58
63
  },
59
- getItem: (id) => load(hashFilename(id)),
64
+ getItem: (id) => {
65
+ assertNoUndefined(id, ['id']);
66
+ return load(hashFilename(id));
67
+ },
60
68
  incrementItemAttribute: async (id, attribute) => {
69
+ assertNoUndefined(id, ['id']);
61
70
  const filename = hashFilename(id);
62
71
  const path = await filePath(filename);
63
72
  await mkTableDir;
@@ -88,11 +97,13 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
88
97
  throw new NotFoundError(`Item with ${hashKey.toString()} "${id}" does not exist`);
89
98
  }
90
99
  const newItem = { ...data, ...item };
100
+ assertNoUndefined(newItem);
91
101
  return new Promise((resolve, reject) => {
92
102
  writeFile(path, JSON.stringify(newItem, null, 2), (err) => err ? reject(err) : resolve(newItem));
93
103
  });
94
104
  },
95
105
  putItem: async (item) => {
106
+ assertNoUndefined(item);
96
107
  const path = await filePath(hashFilename(item[hashKey]));
97
108
  await mkTableDir;
98
109
  return new Promise((resolve, reject) => {
@@ -100,6 +111,7 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
100
111
  });
101
112
  },
102
113
  createItem: async (item) => {
114
+ assertNoUndefined(item);
103
115
  const hashed = hashFilename(item[hashKey]);
104
116
  const existingItem = await load(hashed);
105
117
  if (existingItem) {
@@ -120,6 +132,7 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
120
132
  // Process items sequentially to ensure consistent conflict detection
121
133
  // Note: concurrency parameter is ignored for filesystem to avoid race conditions
122
134
  for (const item of items) {
135
+ assertNoUndefined(item);
123
136
  try {
124
137
  const hashed = hashFilename(item[hashKey]);
125
138
  const existingItem = await load(hashed);
@@ -1,3 +1,4 @@
1
+ import { assertNoUndefined } from '../fileSystemAssert';
1
2
  import { fileSystemUnversionedDocumentStore } from '../unversioned/file-system';
2
3
  const PAGE_LIMIT = 5;
3
4
  export const fileSystemVersionedDocumentStore = (initializer) => () => (configProvider) => (_, hashKey, options) => {
@@ -9,6 +10,7 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
9
10
  }));
10
11
  },
11
12
  getVersions: async (id, startVersion) => {
13
+ assertNoUndefined(id, ['id']);
12
14
  const item = await unversionedDocuments.getItem(id);
13
15
  const versions = item === null || item === void 0 ? void 0 : item.items.reverse();
14
16
  if (!versions) {
@@ -23,6 +25,7 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
23
25
  };
24
26
  },
25
27
  getItem: async (id, timestamp) => {
28
+ assertNoUndefined(id, ['id']);
26
29
  const item = await unversionedDocuments.getItem(id);
27
30
  if (timestamp) {
28
31
  return item === null || item === void 0 ? void 0 : item.items.find(version => version.timestamp === timestamp);
@@ -38,6 +41,7 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
38
41
  save: async (changes) => {
39
42
  var _a;
40
43
  const document = { ...item, ...changes, timestamp, author };
44
+ assertNoUndefined(document);
41
45
  const container = (_a = await unversionedDocuments.getItem(document[hashKey])) !== null && _a !== void 0 ? _a : { id: document[hashKey], items: [] };
42
46
  const updated = { ...container, items: [...container.items, document] };
43
47
  await unversionedDocuments.putItem(updated);
@@ -49,6 +53,7 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
49
53
  var _a;
50
54
  const author = (options === null || options === void 0 ? void 0 : options.getAuthor) ? await options.getAuthor(...authorArgs) : authorArgs[0];
51
55
  const document = { ...item, timestamp: new Date().getTime(), author };
56
+ assertNoUndefined(document);
52
57
  const container = (_a = await unversionedDocuments.getItem(document[hashKey])) !== null && _a !== void 0 ? _a : { id: document[hashKey], items: [] };
53
58
  const updated = { ...container, items: [...container.items, document] };
54
59
  await unversionedDocuments.putItem(updated);
@@ -3,11 +3,15 @@ import { AuthProvider } from '../authProvider';
3
3
  import { ActivityState } from './attempt-utils';
4
4
  import { LrsGateway } from '.';
5
5
  export interface Grade {
6
- scoreGiven: number;
7
- scoreMaximum: number;
8
- comment?: string;
9
6
  activityProgress: 'Initialized' | 'Started' | 'inProgress' | 'Submitted' | 'Completed';
7
+ comment?: string;
10
8
  gradingProgress: 'FullyGraded' | 'Pending' | 'PendingManual' | 'Failed' | 'NotReady';
9
+ scoreGiven: number;
10
+ scoreMaximum: number;
11
+ submission?: {
12
+ startedAt?: string;
13
+ submittedAt?: string;
14
+ };
11
15
  userId?: string;
12
16
  }
13
17
  export declare const getRegistrationAttemptInfo: (lrs: LrsGateway, registration: string, options?: {
@@ -24,7 +28,12 @@ export declare const getScoreGrade: (score: {
24
28
  raw?: number;
25
29
  min?: number;
26
30
  max?: number;
27
- }, completed: boolean, userId?: string, maxScore?: number) => Grade;
31
+ }, options: {
32
+ maxScore?: number;
33
+ startedAt?: string;
34
+ submittedAt?: string;
35
+ userId?: string;
36
+ }) => Grade;
28
37
  export type Progress = {
29
38
  scaled: number;
30
39
  max?: number;
@@ -22,29 +22,44 @@ export const getRegistrationAttemptInfo = async (lrs, registration, options) =>
22
22
  // generates a payload that can be sent to the LMS, documentation here:
23
23
  // lti: http://www.imsglobal.org/spec/lti-ags/v2p0#score-publish-service
24
24
  // ltijs: https://cvmcosta.me/ltijs/#/grading
25
- export const getScoreGrade = (score, completed, userId, maxScore) => {
25
+ // Note: "min" is currently completely ignored
26
+ export const getScoreGrade = (score, options) => {
26
27
  const { raw, scaled, max } = score;
28
+ const { maxScore, startedAt, submittedAt, userId } = options;
27
29
  const scoreMaximum = maxScore !== null && maxScore !== void 0 ? maxScore : 100;
28
30
  const scoreGiven = raw && max
29
31
  ? scoreMaximum / max * raw
30
32
  : scaled
31
33
  ? scaled * scoreMaximum
32
34
  : 0;
35
+ const submission = {};
36
+ if (startedAt) {
37
+ submission.startedAt = startedAt;
38
+ }
39
+ if (submittedAt) {
40
+ submission.submittedAt = submittedAt;
41
+ }
33
42
  return {
34
43
  userId,
35
- activityProgress: completed ? 'Completed' : 'Started',
44
+ activityProgress: submittedAt ? 'Completed' : 'Started',
36
45
  // canvas assumes that anything that isn't 'FullyGraded' requires manual grading and displays a "needs grading" icon.
37
46
  // if you warp your mind you can consider the portion of the assignment which is completed to be fully graded.
38
47
  gradingProgress: 'FullyGraded',
39
48
  scoreMaximum,
40
49
  scoreGiven: roundToPrecision(scoreGiven, -2),
50
+ submission,
41
51
  };
42
52
  };
43
53
  // These methods assign 0's to incomplete activities
44
54
  const getCompletedActivityStateGradeAndProgress = ({ name, scoreMaximum, state, userId }) => {
45
- var _a, _b;
55
+ var _a, _b, _c, _d;
46
56
  return ({
47
- grade: getScoreGrade(((_b = (_a = state.currentAttemptCompleted) === null || _a === void 0 ? void 0 : _a.result) === null || _b === void 0 ? void 0 : _b.score) || {}, !!state.currentAttemptCompleted, userId, scoreMaximum),
57
+ grade: getScoreGrade(((_b = (_a = state.currentAttemptCompleted) === null || _a === void 0 ? void 0 : _a.result) === null || _b === void 0 ? void 0 : _b.score) || {}, {
58
+ maxScore: scoreMaximum,
59
+ startedAt: (_c = state.currentAttempt) === null || _c === void 0 ? void 0 : _c.timestamp,
60
+ submittedAt: (_d = state.currentAttemptCompleted) === null || _d === void 0 ? void 0 : _d.timestamp,
61
+ userId,
62
+ }),
48
63
  progress: {
49
64
  scaled: state.currentAttemptCompleted ? 1 : 0,
50
65
  },