@iblai/data-layer 1.20.0 → 1.22.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/dist/index.js CHANGED
@@ -42473,7 +42473,7 @@ const MONETIZATION_ENDPOINTS = {
42473
42473
  path: (key, type, id, priceId) => `${BILLING_BASE}/${key}/items/${type}/${id}/paywall/prices/${priceId}/`,
42474
42474
  service: exports.SERVICES.DM,
42475
42475
  },
42476
- // Flow 4 — Public Pricing (No Auth)
42476
+ // Flow 4 — Public Pricing (No Authentication)
42477
42477
  PUBLIC_PRICING: {
42478
42478
  path: (key, type, id) => `${BILLING_BASE}/${key}/items/${type}/${id}/pricing/`,
42479
42479
  service: exports.SERVICES.DM,
@@ -43735,6 +43735,187 @@ const perLearnerApiSlice = react.createApi({
43735
43735
  });
43736
43736
  const { useGetPerLearnerActivityQuery, useLazyGetPerLearnerActivityQuery, useGetPerLearnerInfoQuery, useLazyGetPerLearnerInfoQuery, useGetOverTimeActivityQuery, useLazyGetOverTimeActivityQuery, } = perLearnerApiSlice;
43737
43737
 
43738
+ const LEARNER_COMPLETION_PATH = '/api/catalog/enrollment/courses/completion/';
43739
+ const LEARNER_IDENTIFIER_KEYS = ['username', 'user_id', 'email'];
43740
+ const isPresent = (value) => value !== undefined && value !== null && String(value).trim() !== '';
43741
+ /**
43742
+ * Checks the args carry exactly one learner identifier plus `course_id` and
43743
+ * `platform_key`. Returns a reason string when they don't.
43744
+ */
43745
+ function validateLearnerCompletionArgs(args) {
43746
+ const identifiers = LEARNER_IDENTIFIER_KEYS.filter((key) => isPresent(args[key]));
43747
+ if (identifiers.length !== 1) {
43748
+ return 'Exactly one of username, user_id or email is required.';
43749
+ }
43750
+ if (!isPresent(args.course_id))
43751
+ return 'course_id is required.';
43752
+ if (!isPresent(args.platform_key))
43753
+ return 'platform_key is required.';
43754
+ return null;
43755
+ }
43756
+ /**
43757
+ * Builds the completion URL for one `endpoint` selector. Only the learner
43758
+ * identifier, `course_id` and `platform_key` are sent; the DM forwards
43759
+ * nothing else to edX.
43760
+ */
43761
+ function buildLearnerCompletionUrl(args, endpoint) {
43762
+ const params = new URLSearchParams();
43763
+ const record = args;
43764
+ for (const key of [...LEARNER_IDENTIFIER_KEYS, 'platform_key', 'course_id']) {
43765
+ if (isPresent(record[key]))
43766
+ params.set(key, String(record[key]));
43767
+ }
43768
+ params.set('endpoint', endpoint);
43769
+ return `${LEARNER_COMPLETION_PATH}?${params.toString()}`;
43770
+ }
43771
+ // 404 means "course not on tenant / learner not enrolled"; retrying won't
43772
+ // change that, and the outline call is heavy. 5xx keeps the default retries.
43773
+ const LEARNER_COMPLETION_MAX_RETRIES = 3;
43774
+ const learnerCompletionExtraOptions = {
43775
+ retryCondition: (error, _args, { attempt }) => attempt <= LEARNER_COMPLETION_MAX_RETRIES && (error === null || error === void 0 ? void 0 : error.status) !== 404,
43776
+ };
43777
+ /**
43778
+ * One completion request through `iblFetchBaseQuery` (DM token, token
43779
+ * refresh, registered error handlers). `iblFetchBaseQuery` throws on HTTP
43780
+ * errors, which RTK would turn into a SerializedError without the status;
43781
+ * catching here keeps `{ status, data }` so the UI can tell 403/404/502 apart.
43782
+ */
43783
+ async function fetchLearnerCompletion(args, endpoint, api, extraOptions = {}) {
43784
+ const invalid = validateLearnerCompletionArgs(args);
43785
+ if (invalid) {
43786
+ return {
43787
+ error: {
43788
+ status: 'CUSTOM_ERROR',
43789
+ error: invalid,
43790
+ data: { kind: 'invalid_request', error: invalid },
43791
+ },
43792
+ };
43793
+ }
43794
+ try {
43795
+ const result = await iblFetchBaseQuery({
43796
+ url: buildLearnerCompletionUrl(args, endpoint),
43797
+ service: exports.SERVICES.DM,
43798
+ method: 'GET',
43799
+ }, api, { ...learnerCompletionExtraOptions, ...extraOptions });
43800
+ if ('error' in result && result.error) {
43801
+ return { error: result.error };
43802
+ }
43803
+ return { data: result.data };
43804
+ }
43805
+ catch (error) {
43806
+ return { error: error };
43807
+ }
43808
+ }
43809
+ const completionError = (kind, status) => ({ kind, status, retryable: kind === 'unavailable' });
43810
+ /**
43811
+ * Maps a failed completion query onto the states the UI shows. 403 and 404
43812
+ * are deliberately coarse on the backend (one body for every hidden case),
43813
+ * so don't try to read more into them.
43814
+ */
43815
+ function normalizeLearnerCompletionError(error) {
43816
+ if (error === undefined || error === null)
43817
+ return null;
43818
+ if (typeof error !== 'object')
43819
+ return completionError('unknown');
43820
+ const { status, data } = error;
43821
+ if (status === 'CUSTOM_ERROR') {
43822
+ const kind = data === null || data === void 0 ? void 0 : data.kind;
43823
+ return completionError(kind === 'invalid_request' ? 'invalid_request' : 'unknown', status);
43824
+ }
43825
+ if (status === 'FETCH_ERROR' || status === 'TIMEOUT_ERROR') {
43826
+ return completionError('unavailable', status);
43827
+ }
43828
+ if (typeof status === 'number') {
43829
+ if (status === 400)
43830
+ return completionError('invalid_request', status);
43831
+ if (status === 401)
43832
+ return completionError('unauthorized', status);
43833
+ if (status === 403)
43834
+ return completionError('forbidden', status);
43835
+ if (status === 404)
43836
+ return completionError('not_enrolled', status);
43837
+ if (status === 502 || status === 503 || status === 504) {
43838
+ return completionError('unavailable', status);
43839
+ }
43840
+ return completionError('unknown', status);
43841
+ }
43842
+ return completionError('unknown');
43843
+ }
43844
+ /* ------------------------------------------------------------------------ */
43845
+ /* Outline helpers */
43846
+ /* ------------------------------------------------------------------------ */
43847
+ /** Fills the fields edX omits: `children ?? []`, `complete`/`resume_block ?? false`. */
43848
+ function normalizeOutline(block) {
43849
+ var _a, _b, _c;
43850
+ return {
43851
+ ...block,
43852
+ children: ((_a = block.children) !== null && _a !== void 0 ? _a : []).map(normalizeOutline),
43853
+ complete: (_b = block.complete) !== null && _b !== void 0 ? _b : false,
43854
+ resume_block: (_c = block.resume_block) !== null && _c !== void 0 ? _c : false,
43855
+ };
43856
+ }
43857
+ /** Depth-first, course order. The root has depth 0 and `parentId` null. */
43858
+ function flattenOutline(root) {
43859
+ const entries = [];
43860
+ const visit = (block, depth, parentId, parentPath) => {
43861
+ var _a;
43862
+ const path = [...parentPath, block.display_name];
43863
+ entries.push({ block, depth, parentId, path });
43864
+ for (const child of (_a = block.children) !== null && _a !== void 0 ? _a : [])
43865
+ visit(child, depth + 1, block.id, path);
43866
+ };
43867
+ visit(root, 0, null, []);
43868
+ return entries;
43869
+ }
43870
+ /**
43871
+ * Follows `resume_block: true` from the root to the deepest marked node (the
43872
+ * last-visited leaf). Returns null when the learner hasn't visited anything.
43873
+ */
43874
+ function findResumeBlock(root) {
43875
+ var _a, _b;
43876
+ // The root may or may not carry the flag itself; either way descend from it.
43877
+ let current = root.resume_block ? root : null;
43878
+ let candidates = (_a = root.children) !== null && _a !== void 0 ? _a : [];
43879
+ for (;;) {
43880
+ const next = candidates.find((block) => block.resume_block);
43881
+ if (!next)
43882
+ return current;
43883
+ current = next;
43884
+ candidates = (_b = next.children) !== null && _b !== void 0 ? _b : [];
43885
+ }
43886
+ }
43887
+ /** Counts leaves (blocks without children) by `type`. */
43888
+ function countByType(root) {
43889
+ var _a;
43890
+ const counts = {};
43891
+ for (const { block, depth } of flattenOutline(root)) {
43892
+ if (depth === 0 || block.children !== undefined)
43893
+ continue;
43894
+ // Childless containers (an empty vertical) aren't content.
43895
+ if (block.type === 'chapter' || block.type === 'sequential' || block.type === 'vertical') {
43896
+ continue;
43897
+ }
43898
+ counts[block.type] = ((_a = counts[block.type]) !== null && _a !== void 0 ? _a : 0) + 1;
43899
+ }
43900
+ return counts;
43901
+ }
43902
+ /** Top-level sections (chapters) for a summary view, in course order. */
43903
+ function getSections(root) {
43904
+ var _a;
43905
+ return ((_a = root.children) !== null && _a !== void 0 ? _a : [])
43906
+ .filter((block) => block.type === 'chapter')
43907
+ .map((block) => {
43908
+ var _a;
43909
+ return ({
43910
+ id: block.id,
43911
+ display_name: block.display_name,
43912
+ completion_percentage: block.completion_percentage,
43913
+ complete: (_a = block.complete) !== null && _a !== void 0 ? _a : false,
43914
+ block,
43915
+ });
43916
+ });
43917
+ }
43918
+
43738
43919
  const coursesApiSlice = react.createApi({
43739
43920
  reducerPath: 'coursesApiSlice',
43740
43921
  baseQuery: iblFetchBaseQuery,
@@ -43776,9 +43957,20 @@ const coursesApiSlice = react.createApi({
43776
43957
  },
43777
43958
  providesTags: ['user-assigned-courses'],
43778
43959
  }),
43960
+ /**
43961
+ * Per-learner course outline with block-level completion. Name the learner
43962
+ * with exactly one of `username` / `user_id` / `email`, even for one's own
43963
+ * data. Errors keep their HTTP status; map them with
43964
+ * `normalizeLearnerCompletionError`.
43965
+ */
43966
+ getLearnerCourseOutline: builder.query({
43967
+ queryFn: (args, api, extraOptions) => fetchLearnerCompletion(args, 'outline', api, extraOptions),
43968
+ // The outline is heavy on large courses; keep it around briefly.
43969
+ keepUnusedDataFor: 120,
43970
+ }),
43779
43971
  }),
43780
43972
  });
43781
- const { useGetUserEnrolledCoursesQuery, useLazyGetUserEnrolledCoursesQuery, useGetUserAssignedCoursesQuery, useLazyGetUserAssignedCoursesQuery, } = coursesApiSlice;
43973
+ const { useGetUserEnrolledCoursesQuery, useLazyGetUserEnrolledCoursesQuery, useGetUserAssignedCoursesQuery, useLazyGetUserAssignedCoursesQuery, useGetLearnerCourseOutlineQuery, useLazyGetLearnerCourseOutlineQuery, } = coursesApiSlice;
43782
43974
 
43783
43975
  const courseMetadataApiSlice = react.createApi({
43784
43976
  reducerPath: 'courseMetadataApiSlice',
@@ -49211,6 +49403,7 @@ exports.HOSTING_ENDPOINTS = HOSTING_ENDPOINTS;
49211
49403
  exports.HOSTING_QUERY_KEYS = HOSTING_QUERY_KEYS;
49212
49404
  exports.HOSTING_REDUCER_PATH = HOSTING_REDUCER_PATH;
49213
49405
  exports.IblDataLayer = IblDataLayer;
49406
+ exports.LEARNER_COMPLETION_PATH = LEARNER_COMPLETION_PATH;
49214
49407
  exports.LOGO_ENDPOINTS = LOGO_ENDPOINTS;
49215
49408
  exports.MCP_ENDPOINTS = MCP_ENDPOINTS;
49216
49409
  exports.MCP_REDUCER_PATH = MCP_REDUCER_PATH;
@@ -49293,6 +49486,7 @@ exports.authApiSlice = authApiSlice;
49293
49486
  exports.billingApiSlice = billingApiSlice;
49294
49487
  exports.billingCustomSlice = billingCustomSlice;
49295
49488
  exports.buildGlobalSearchUrl = buildGlobalSearchUrl;
49489
+ exports.buildLearnerCompletionUrl = buildLearnerCompletionUrl;
49296
49490
  exports.buildRecentMessagesUrl = buildRecentMessagesUrl;
49297
49491
  exports.buildSessionRetrieveUrl = buildSessionRetrieveUrl;
49298
49492
  exports.buildSharedSessionRetrieveUrl = buildSharedSessionRetrieveUrl;
@@ -49309,6 +49503,7 @@ exports.clawApiSlice = clawApiSlice;
49309
49503
  exports.coreApiSlice = coreApiSlice;
49310
49504
  exports.coreCustomApiSlice = coreCustomApiSlice;
49311
49505
  exports.coreFakeCustomPublicImageAssetApiSlice = coreFakeCustomPublicImageAssetApiSlice;
49506
+ exports.countByType = countByType;
49312
49507
  exports.courseMetadataApiSlice = courseMetadataApiSlice;
49313
49508
  exports.coursesApiSlice = coursesApiSlice;
49314
49509
  exports.credentialsApiSlice = credentialsApiSlice;
@@ -49323,12 +49518,16 @@ exports.evalTotalPages = evalTotalPages;
49323
49518
  exports.evaluationsApiSlice = evaluationsApiSlice;
49324
49519
  exports.fakeBaseQueryWithLogging = fakeBaseQueryWithLogging;
49325
49520
  exports.featureTags = featureTags;
49521
+ exports.fetchLearnerCompletion = fetchLearnerCompletion;
49326
49522
  exports.fetchUserLlmUsage = fetchUserLlmUsage;
49327
49523
  exports.filterCourseUserRoles = filterCourseUserRoles;
49328
49524
  exports.filterGlobalUserRoles = filterGlobalUserRoles;
49329
49525
  exports.filterOrgUserRoles = filterOrgUserRoles;
49330
49526
  exports.filterSlashSkills = filterSlashSkills;
49527
+ exports.findResumeBlock = findResumeBlock;
49528
+ exports.flattenOutline = flattenOutline;
49331
49529
  exports.getNextRecentMessagesPageParam = getNextRecentMessagesPageParam;
49530
+ exports.getSections = getSections;
49332
49531
  exports.getSpendCapExceededError = getSpendCapExceededError;
49333
49532
  exports.getUserRoleScope = getUserRoleScope;
49334
49533
  exports.gradingApiSlice = gradingApiSlice;
@@ -49352,6 +49551,8 @@ exports.mentorReducer = mentorReducer;
49352
49551
  exports.moderationLogsApiReducer = moderationLogsApiReducer;
49353
49552
  exports.moderationLogsApiSlice = moderationLogsApiSlice;
49354
49553
  exports.monetizationApiSlice = monetizationApiSlice;
49554
+ exports.normalizeLearnerCompletionError = normalizeLearnerCompletionError;
49555
+ exports.normalizeOutline = normalizeOutline;
49355
49556
  exports.normalizeUserLlmUsageError = normalizeUserLlmUsageError;
49356
49557
  exports.notificationsApiSlice = notificationsApiSlice;
49357
49558
  exports.notificationsCustomApiSlice = notificationsCustomApiSlice;
@@ -49646,6 +49847,7 @@ exports.useGetIntegrationCredentialsSchemaQuery = useGetIntegrationCredentialsSc
49646
49847
  exports.useGetIssuersQuery = useGetIssuersQuery;
49647
49848
  exports.useGetItemSubscriptionQuery = useGetItemSubscriptionQuery;
49648
49849
  exports.useGetLLMCredentialsQuery = useGetLLMCredentialsQuery;
49850
+ exports.useGetLearnerCourseOutlineQuery = useGetLearnerCourseOutlineQuery;
49649
49851
  exports.useGetLearnerDetailsQuery = useGetLearnerDetailsQuery;
49650
49852
  exports.useGetLlmUsageMetricsQuery = useGetLlmUsageMetricsQuery;
49651
49853
  exports.useGetLlmUsageObservationsQuery = useGetLlmUsageObservationsQuery;
@@ -49911,6 +50113,7 @@ exports.useLazyGetIntegrationCredentialsSchemaQuery = useLazyGetIntegrationCrede
49911
50113
  exports.useLazyGetIssuersQuery = useLazyGetIssuersQuery;
49912
50114
  exports.useLazyGetItemSubscriptionQuery = useLazyGetItemSubscriptionQuery;
49913
50115
  exports.useLazyGetLLMCredentialsQuery = useLazyGetLLMCredentialsQuery;
50116
+ exports.useLazyGetLearnerCourseOutlineQuery = useLazyGetLearnerCourseOutlineQuery;
49914
50117
  exports.useLazyGetLlmsQuery = useLazyGetLlmsQuery;
49915
50118
  exports.useLazyGetLtiKeyDetailQuery = useLazyGetLtiKeyDetailQuery;
49916
50119
  exports.useLazyGetMCPServerQuery = useLazyGetMCPServerQuery;
@@ -50234,6 +50437,7 @@ exports.userApiSlice = userApiSlice;
50234
50437
  exports.userChatHistoryApiSlice = userChatHistoryApiSlice;
50235
50438
  exports.userInvitationsApiSlice = userInvitationsApiSlice;
50236
50439
  exports.userLlmUsageApiSlice = userLlmUsageApiSlice;
50440
+ exports.validateLearnerCompletionArgs = validateLearnerCompletionArgs;
50237
50441
  exports.validateUserLlmUsageParams = validateUserLlmUsageParams;
50238
50442
  exports.watchedGroupsApiSlice = watchedGroupsApiSlice;
50239
50443
  exports.workflowsApiSlice = workflowsApiSlice;