@instructure/platform-modules 0.1.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 +75 -0
- package/dist/adapters/canvasGraphql/index.d.ts +54 -0
- package/dist/adapters/canvasGraphql/index.d.ts.map +1 -0
- package/dist/adapters/canvasGraphql/normalize.d.ts +345 -0
- package/dist/adapters/canvasGraphql/normalize.d.ts.map +1 -0
- package/dist/adapters/canvasGraphql/queries.d.ts +69 -0
- package/dist/adapters/canvasGraphql/queries.d.ts.map +1 -0
- package/dist/adapters/inMemory.d.ts +63 -0
- package/dist/adapters/inMemory.d.ts.map +1 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/limits.d.ts +86 -0
- package/dist/adapters/limits.d.ts.map +1 -0
- package/dist/adapters/types.d.ts +87 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/cache/index.d.ts +3 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/invalidate.d.ts +52 -0
- package/dist/cache/invalidate.d.ts.map +1 -0
- package/dist/cache/keys.d.ts +86 -0
- package/dist/cache/keys.d.ts.map +1 -0
- package/dist/components/FilterChips.d.ts +19 -0
- package/dist/components/FilterChips.d.ts.map +1 -0
- package/dist/components/ModuleCard.d.ts +32 -0
- package/dist/components/ModuleCard.d.ts.map +1 -0
- package/dist/components/ModuleItemRow.d.ts +17 -0
- package/dist/components/ModuleItemRow.d.ts.map +1 -0
- package/dist/components/ModulesList.d.ts +69 -0
- package/dist/components/ModulesList.d.ts.map +1 -0
- package/dist/components/ModulesList.stories.d.ts +138 -0
- package/dist/components/ModulesList.stories.d.ts.map +1 -0
- package/dist/components/StatusPill.d.ts +18 -0
- package/dist/components/StatusPill.d.ts.map +1 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/labels.d.ts +89 -0
- package/dist/components/labels.d.ts.map +1 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/registry.d.ts +38 -0
- package/dist/config/registry.d.ts.map +1 -0
- package/dist/config/schemas.d.ts +16 -0
- package/dist/config/schemas.d.ts.map +1 -0
- package/dist/fixtures/generate.d.ts +48 -0
- package/dist/fixtures/generate.d.ts.map +1 -0
- package/dist/fixtures/index.d.ts +3 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/scenarios.d.ts +42 -0
- package/dist/fixtures/scenarios.d.ts.map +1 -0
- package/dist/hooks/constants.d.ts +49 -0
- package/dist/hooks/constants.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/queryOptions.d.ts +58 -0
- package/dist/hooks/queryOptions.d.ts.map +1 -0
- package/dist/hooks/useItemsMode.d.ts +114 -0
- package/dist/hooks/useItemsMode.d.ts.map +1 -0
- package/dist/hooks/useModuleItemQueries.d.ts +56 -0
- package/dist/hooks/useModuleItemQueries.d.ts.map +1 -0
- package/dist/hooks/useModuleItems.d.ts +41 -0
- package/dist/hooks/useModuleItems.d.ts.map +1 -0
- package/dist/hooks/useModules.d.ts +50 -0
- package/dist/hooks/useModules.d.ts.map +1 -0
- package/dist/hooks/useModulesPageData.d.ts +78 -0
- package/dist/hooks/useModulesPageData.d.ts.map +1 -0
- package/dist/i18n.d.ts +92 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1966 -0
- package/dist/types/domain.d.ts +147 -0
- package/dist/types/domain.d.ts.map +1 -0
- package/dist/types/enums.d.ts +93 -0
- package/dist/types/enums.d.ts.map +1 -0
- package/dist/types/extensions.d.ts +48 -0
- package/dist/types/extensions.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/schemas.d.ts +38 -0
- package/dist/types/schemas.d.ts.map +1 -0
- package/package.json +80 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @instructure/platform-modules
|
|
2
|
+
|
|
3
|
+
The platform modules experience. One data layer and one set of components, driven by a
|
|
4
|
+
host-supplied adapter.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
Add the workspace dependency:
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@instructure/platform-modules": "workspace:*"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Peer dependencies
|
|
19
|
+
|
|
20
|
+
The host provides React 18, `@tanstack/react-query` 5, `graphql` 16, `zod` 3, and the InstUI 11
|
|
21
|
+
packages listed under `peerDependencies` in `package.json`.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Build an adapter once, outside render. Wrap the page in the i18n provider. Call the hook and pass
|
|
26
|
+
its whole result to the list.
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import {
|
|
30
|
+
createCanvasGraphqlAdapter,
|
|
31
|
+
ModulesI18nProvider,
|
|
32
|
+
ModulesList,
|
|
33
|
+
useModulesPageData,
|
|
34
|
+
} from '@instructure/platform-modules'
|
|
35
|
+
|
|
36
|
+
const adapter = createCanvasGraphqlAdapter({ executeQuery })
|
|
37
|
+
|
|
38
|
+
function ModulesPage({ courseId }: { courseId: string }) {
|
|
39
|
+
const data = useModulesPageData({ courseId, adapter, view: 'student' })
|
|
40
|
+
return (
|
|
41
|
+
<ModulesI18nProvider t={t} locale={locale}>
|
|
42
|
+
<ModulesList data={data} courseName={courseName} />
|
|
43
|
+
</ModulesI18nProvider>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Keep the adapter referentially stable. A new adapter identity on every render rebuilds every item
|
|
49
|
+
observer's options on every render.
|
|
50
|
+
|
|
51
|
+
## Adapters
|
|
52
|
+
|
|
53
|
+
| Adapter | Use |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `createCanvasGraphqlAdapter({ executeQuery })` | Canvas GraphQL. The host injects `executeQuery` |
|
|
56
|
+
| `createInMemoryAdapter({ fixture })` | Tests, stories, and the demo app |
|
|
57
|
+
|
|
58
|
+
`executeQuery` is a constructor argument, not a context read, so each host supplies its own
|
|
59
|
+
endpoint and auth for the same documents.
|
|
60
|
+
|
|
61
|
+
## Development
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pnpm --filter @instructure/platform-modules test
|
|
65
|
+
pnpm --filter @instructure/platform-modules type-check
|
|
66
|
+
pnpm --filter @instructure/platform-modules build
|
|
67
|
+
pnpm storybook
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Documentation
|
|
71
|
+
|
|
72
|
+
- `CLAUDE.md` describes what is true in `src/` right now.
|
|
73
|
+
- `docs/adr/` holds the decision records. They are append-only: supersede a record, never edit it.
|
|
74
|
+
- Reasoning local to one file stays in a doc comment there. Start with `adapters/types.ts`,
|
|
75
|
+
`adapters/limits.ts`, `hooks/constants.ts`, and `cache/invalidate.ts`.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ModulesDataAdapter } from '../types';
|
|
2
|
+
import { UnsupportedModuleItem } from './normalize';
|
|
3
|
+
/**
|
|
4
|
+
* The Canvas GraphQL implementation of `ModulesDataAdapter`.
|
|
5
|
+
*
|
|
6
|
+
* `executeQuery` is injected rather than pulled from `usePlatformUi()`. Two
|
|
7
|
+
* reasons, both load-bearing: the adapter stays a plain function that a test
|
|
8
|
+
* can drive with `vi.fn()` and no React, and it stays host-agnostic — Canvas
|
|
9
|
+
* Career supplies a different `executeQuery` (different endpoint, different
|
|
10
|
+
* auth) for the identical documents.
|
|
11
|
+
*/
|
|
12
|
+
export type ExecuteQuery = <TResult, TVariables>(query: unknown, variables: TVariables) => Promise<TResult>;
|
|
13
|
+
export { INLINE_ITEM_LIMIT, INLINE_QUERY_NODE_BUDGET } from '../limits';
|
|
14
|
+
export interface CanvasGraphqlAdapterOptions {
|
|
15
|
+
executeQuery: ExecuteQuery;
|
|
16
|
+
/** @see INLINE_ITEM_LIMIT */
|
|
17
|
+
inlineItemLimit?: number;
|
|
18
|
+
/** @see INLINE_QUERY_NODE_BUDGET */
|
|
19
|
+
nodeBudget?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Notified when an item is dropped for having a content type outside the
|
|
22
|
+
* closed domain enum. Unset by default: dropping is the deliberate policy,
|
|
23
|
+
* this is the seam for making it visible.
|
|
24
|
+
*/
|
|
25
|
+
onUnsupportedItem?: (item: UnsupportedModuleItem) => void;
|
|
26
|
+
/**
|
|
27
|
+
* The student whose submissions to select, when there is one.
|
|
28
|
+
*
|
|
29
|
+
* Omit for a teacher, an admin, an observer, or any viewer without a single
|
|
30
|
+
* subject. Omitting it is *safe by construction*: the submission field set is
|
|
31
|
+
* then not requested at all, so `ModuleItem.status` comes back undefined —
|
|
32
|
+
* already the contract's "not fetched" — rather than filled with somebody
|
|
33
|
+
* else's data.
|
|
34
|
+
*
|
|
35
|
+
* Supplying it is what makes per-item status available, and it must be the
|
|
36
|
+
* viewer's own id. Canvas's `submissionsConnection` returns *every* visible
|
|
37
|
+
* student's submissions to a viewer holding `manage_grades` or
|
|
38
|
+
* `view_all_grades`, ordered by `user_id`. @see the `SUBMISSION_FIELDS`
|
|
39
|
+
* docstring in `./queries`.
|
|
40
|
+
*/
|
|
41
|
+
studentId?: string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Validate normalised output against the Zod schemas before returning.
|
|
44
|
+
*
|
|
45
|
+
* On by default. This is the check that keeps the GraphQL and REST adapters
|
|
46
|
+
* from drifting, and it is the only place the contract is enforced at
|
|
47
|
+
* runtime — turn it off only if a profile says it costs something real.
|
|
48
|
+
*/
|
|
49
|
+
validate?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export declare function createCanvasGraphqlAdapter({ executeQuery, inlineItemLimit, nodeBudget, onUnsupportedItem, studentId, validate, }: CanvasGraphqlAdapterOptions): ModulesDataAdapter;
|
|
52
|
+
export { GET_MODULE_ITEMS, GET_MODULES, type GetModuleItemsVariables, type GetModulesVariables, } from './queries';
|
|
53
|
+
export { buildStudentContext, deriveModuleItemStatus, deriveModuleProgress, deriveModuleStatus, firstSubmission, type GetModuleItemsResult, type GetModulesResult, type GraphqlCompletionRequirement, type GraphqlModuleConnection, type GraphqlModuleItemConnection, type GraphqlModuleItemContent, type GraphqlModuleItemNode, type GraphqlModuleNode, type GraphqlModuleProgression, type GraphqlModuleStatistics, type GraphqlPageInfo, type GraphqlRequirement, type GraphqlSubmission, type GraphqlSubmissionConnection, type ModuleStudentContext, normalizeItemType, normalizeModule, normalizeModuleItem, normalizeModuleItemsPage, normalizeModulesPage, normalizePageInfo, type NormalizeOptions, PartialCanvasResponseError, type PartialResponseSubject, parseDurationMinutes, type UnsupportedModuleItem, } from './normalize';
|
|
54
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/canvasGraphql/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAwC,kBAAkB,EAAE,MAAM,UAAU,CAAA;AACxF,OAAO,EAKL,KAAK,qBAAqB,EAC3B,MAAM,aAAa,CAAA;AAQpB;;;;;;;;GAQG;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,UAAU,EAC7C,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,UAAU,KAClB,OAAO,CAAC,OAAO,CAAC,CAAA;AAUrB,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAA;AAEvE,MAAM,WAAW,2BAA2B;IAC1C,YAAY,EAAE,YAAY,CAAA;IAC1B,6BAA6B;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,qBAAqB,KAAK,IAAI,CAAA;IACzD;;;;;;;;;;;;;;OAcG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AA6BD,wBAAgB,0BAA0B,CAAC,EACzC,YAAY,EACZ,eAAmC,EACnC,UAAqC,EACrC,iBAAiB,EACjB,SAAS,EACT,QAAe,GAChB,EAAE,2BAA2B,GAAG,kBAAkB,CAgFlD;AAED,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,GACzB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,4BAA4B,EACjC,KAAK,uBAAuB,EAC5B,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,2BAA2B,EAChC,KAAK,oBAAoB,EACzB,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,0BAA0B,EAC1B,KAAK,sBAAsB,EAC3B,oBAAoB,EACpB,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { CompletionRequirement, CompletionRequirementType, Module, ModuleItem, ModuleItemsPage, ModuleItemStatus, ModuleItemType, ModuleProgress, ModulesPage, ModuleStatus, PageInfo } from '../../types/domain';
|
|
2
|
+
/**
|
|
3
|
+
* GraphQL node → domain model.
|
|
4
|
+
*
|
|
5
|
+
* Everything transport-shaped stops here. The three normalisation rules from
|
|
6
|
+
* `types/domain.ts` are enforced in this file and nowhere else:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Ids are strings.** Canvas hands back `ID`, which the JSON transport
|
|
9
|
+
* may render as a number; every id goes through `String()`.
|
|
10
|
+
* 2. **`published` is always populated.** On this transport Canvas's resolver
|
|
11
|
+
* cannot return null for any viewer, so a null is a partial response and
|
|
12
|
+
* throws — see `resolvePublished`, which explains why the REST rule about
|
|
13
|
+
* absence meaning "published" does not apply here.
|
|
14
|
+
* 3. **One closed item enum.** `__typename` is the discriminator, with
|
|
15
|
+
* `Assignment { isNewQuiz: true }` collapsing to `'quiz'`.
|
|
16
|
+
*/
|
|
17
|
+
export interface GraphqlPageInfo {
|
|
18
|
+
hasNextPage: boolean;
|
|
19
|
+
endCursor: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface GraphqlCompletionRequirement {
|
|
22
|
+
id: string | number;
|
|
23
|
+
type: string;
|
|
24
|
+
minScore?: number | null;
|
|
25
|
+
minPercentage?: number | null;
|
|
26
|
+
}
|
|
27
|
+
/** One row of `submissionsConnection`. Present only for a student viewer. */
|
|
28
|
+
export interface GraphqlSubmission {
|
|
29
|
+
_id?: string | number | null;
|
|
30
|
+
/** `SubmissionState`: unsubmitted | submitted | pending_review | graded | ungraded | deleted. */
|
|
31
|
+
state?: string | null;
|
|
32
|
+
/** `SubmissionGradingStatus`: needs_grading | needs_review | graded | excused. */
|
|
33
|
+
gradingStatus?: string | null;
|
|
34
|
+
submittedAt?: string | null;
|
|
35
|
+
attempt?: number | null;
|
|
36
|
+
missing?: boolean | null;
|
|
37
|
+
/** Per-student, override-applied due date, denormalised onto the submission. */
|
|
38
|
+
cachedDueDate?: string | null;
|
|
39
|
+
}
|
|
40
|
+
export interface GraphqlSubmissionConnection {
|
|
41
|
+
nodes?: (GraphqlSubmission | null)[] | null;
|
|
42
|
+
}
|
|
43
|
+
export interface GraphqlModuleItemContent {
|
|
44
|
+
__typename?: string | null;
|
|
45
|
+
isNewQuiz?: boolean | null;
|
|
46
|
+
dueAt?: string | null;
|
|
47
|
+
/** Declared on `ModuleItemInterface`; `null` for content that cannot be graded. */
|
|
48
|
+
pointsPossible?: number | null;
|
|
49
|
+
submissionsConnection?: GraphqlSubmissionConnection | null;
|
|
50
|
+
}
|
|
51
|
+
/** A `Requirement` as it appears in `ModuleProgression.requirementsMet`. */
|
|
52
|
+
export interface GraphqlRequirement {
|
|
53
|
+
id: string | number;
|
|
54
|
+
type: string;
|
|
55
|
+
minScore?: number | null;
|
|
56
|
+
minPercentage?: number | null;
|
|
57
|
+
}
|
|
58
|
+
export interface GraphqlModuleProgression {
|
|
59
|
+
/** locked | unlocked | started | completed. */
|
|
60
|
+
workflowState?: string | null;
|
|
61
|
+
completed?: boolean | null;
|
|
62
|
+
locked?: boolean | null;
|
|
63
|
+
started?: boolean | null;
|
|
64
|
+
requirementsMet?: (GraphqlRequirement | null)[] | null;
|
|
65
|
+
}
|
|
66
|
+
/** `Module.submissionStatistics`, i.e. Canvas's `ModuleStatistics`. */
|
|
67
|
+
export interface GraphqlModuleStatistics {
|
|
68
|
+
latestDueAt?: string | null;
|
|
69
|
+
missingAssignmentCount?: number | null;
|
|
70
|
+
}
|
|
71
|
+
export interface GraphqlModuleItemNode {
|
|
72
|
+
_id: string | number;
|
|
73
|
+
title?: string | null;
|
|
74
|
+
url?: string | null;
|
|
75
|
+
position?: number | null;
|
|
76
|
+
indent?: number | null;
|
|
77
|
+
published?: boolean | null;
|
|
78
|
+
/** ISO 8601 duration, e.g. `PT45M`. Canvas Career only. */
|
|
79
|
+
estimatedDuration?: string | null;
|
|
80
|
+
content?: GraphqlModuleItemContent | null;
|
|
81
|
+
}
|
|
82
|
+
export interface GraphqlModuleItemConnection {
|
|
83
|
+
nodes?: (GraphqlModuleItemNode | null)[] | null;
|
|
84
|
+
pageInfo: GraphqlPageInfo;
|
|
85
|
+
}
|
|
86
|
+
export interface GraphqlModuleNode {
|
|
87
|
+
_id: string | number;
|
|
88
|
+
name?: string | null;
|
|
89
|
+
position?: number | null;
|
|
90
|
+
published?: boolean | null;
|
|
91
|
+
unlockAt?: string | null;
|
|
92
|
+
/**
|
|
93
|
+
* `Int!` in the Canvas schema, so it is optional here only because a partial
|
|
94
|
+
* response can omit it. `normalizeModule` treats that as a hard error rather
|
|
95
|
+
* than a zero — see `PartialCanvasResponseError`.
|
|
96
|
+
*/
|
|
97
|
+
moduleItemsTotalCount?: number | null;
|
|
98
|
+
requirementCount?: number | null;
|
|
99
|
+
requireSequentialProgress?: boolean | null;
|
|
100
|
+
prerequisites?: ({
|
|
101
|
+
id: string | number;
|
|
102
|
+
} | null)[] | null;
|
|
103
|
+
completionRequirements?: (GraphqlCompletionRequirement | null)[] | null;
|
|
104
|
+
/** Absent for a teacher/anonymous viewer, and `null` when Canvas has none. */
|
|
105
|
+
progression?: GraphqlModuleProgression | null;
|
|
106
|
+
submissionStatistics?: GraphqlModuleStatistics | null;
|
|
107
|
+
moduleItemsConnection?: GraphqlModuleItemConnection | null;
|
|
108
|
+
}
|
|
109
|
+
export interface GraphqlModuleConnection {
|
|
110
|
+
nodes?: (GraphqlModuleNode | null)[] | null;
|
|
111
|
+
pageInfo: GraphqlPageInfo;
|
|
112
|
+
}
|
|
113
|
+
export interface GetModulesResult {
|
|
114
|
+
legacyNode?: {
|
|
115
|
+
modulesConnection?: GraphqlModuleConnection | null;
|
|
116
|
+
} | null;
|
|
117
|
+
}
|
|
118
|
+
export interface GetModuleItemsResult {
|
|
119
|
+
legacyNode?: {
|
|
120
|
+
completionRequirements?: (GraphqlCompletionRequirement | null)[] | null;
|
|
121
|
+
progression?: GraphqlModuleProgression | null;
|
|
122
|
+
moduleItemsConnection?: GraphqlModuleItemConnection | null;
|
|
123
|
+
} | null;
|
|
124
|
+
}
|
|
125
|
+
/** What a `PartialCanvasResponseError` is about. */
|
|
126
|
+
export type PartialResponseSubject = 'module' | 'moduleItem';
|
|
127
|
+
/**
|
|
128
|
+
* A row arrived without a field this adapter cannot responsibly guess.
|
|
129
|
+
*
|
|
130
|
+
* Thrown rather than defaulted, because in both current cases the
|
|
131
|
+
* sensible-looking default is the dangerous one:
|
|
132
|
+
*
|
|
133
|
+
* - `moduleItemsTotalCount` is `Int!`. Coercing a missing value to `0` makes
|
|
134
|
+
* `resolveItemsMode` see an empty course and answer `inline`, so the page
|
|
135
|
+
* asks Canvas to inline every item it has — the most expensive transport
|
|
136
|
+
* setting, chosen when the response is least trustworthy.
|
|
137
|
+
* - `published` is null when the viewer cannot manage the object, which for
|
|
138
|
+
* such a viewer means "published". For a viewer who *can* manage, Canvas
|
|
139
|
+
* always sends a boolean, so a null contradicts what the host told us and
|
|
140
|
+
* the wrong guess renders an unpublished thing as published — to the one
|
|
141
|
+
* audience that acts on that badge.
|
|
142
|
+
*
|
|
143
|
+
* Failing is recoverable in a way silence is not: the hook surfaces the error,
|
|
144
|
+
* the host can retry, and `useItemsMode` already falls through to `onDemand` on
|
|
145
|
+
* a failed probe rather than deadlocking the page.
|
|
146
|
+
*/
|
|
147
|
+
export declare class PartialCanvasResponseError extends Error {
|
|
148
|
+
readonly subject: PartialResponseSubject;
|
|
149
|
+
/** The module or item id, so a host can log which row came back incomplete. */
|
|
150
|
+
readonly id: string;
|
|
151
|
+
/** The field that was missing. */
|
|
152
|
+
readonly field: string;
|
|
153
|
+
constructor(subject: PartialResponseSubject, id: string, field: string);
|
|
154
|
+
}
|
|
155
|
+
/** Reported when a `__typename` has no place in the closed domain enum. */
|
|
156
|
+
export interface UnsupportedModuleItem {
|
|
157
|
+
/** The module item id, so a host can look the row up in Canvas. */
|
|
158
|
+
id: string;
|
|
159
|
+
/** The `content.__typename` we could not map, or `null` when content was absent. */
|
|
160
|
+
typename: string | null;
|
|
161
|
+
}
|
|
162
|
+
export interface NormalizeOptions {
|
|
163
|
+
/**
|
|
164
|
+
* Called for every item dropped because its content type is outside the
|
|
165
|
+
* closed enum (`AiExperience`, `PeerReviewSubAssignment`, or content that
|
|
166
|
+
* was deleted and came back `null`).
|
|
167
|
+
*
|
|
168
|
+
* The alternatives were worse: widening the enum makes the shared contract
|
|
169
|
+
* chase Canvas's release cadence, and throwing takes the whole page down for
|
|
170
|
+
* one unknown row.
|
|
171
|
+
*
|
|
172
|
+
* Dropping is silent data loss, so it must not *also* be unobservable. This
|
|
173
|
+
* defaults to a `console.warn` rather than to nothing — matching
|
|
174
|
+
* `platform-datetime` and ~30 other files in the repo — because "a host can log
|
|
175
|
+
* it" is only true if the host knows to opt in. Pass a no-op to silence it.
|
|
176
|
+
*/
|
|
177
|
+
onUnsupportedItem?: (item: UnsupportedModuleItem) => void;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* `__typename` (+ `isNewQuiz`) → domain type, or `null` when unmappable.
|
|
181
|
+
*
|
|
182
|
+
* A New Quiz is an `Assignment` with an LTI submission type in Canvas's data
|
|
183
|
+
* model; every UI that shows it calls it a quiz, so rule 3 collapses it here
|
|
184
|
+
* rather than leaving each caller to re-derive it.
|
|
185
|
+
*/
|
|
186
|
+
export declare function normalizeItemType(content: GraphqlModuleItemContent | null | undefined): ModuleItemType | null;
|
|
187
|
+
/**
|
|
188
|
+
* ISO 8601 duration → whole minutes.
|
|
189
|
+
*
|
|
190
|
+
* Canvas exposes `estimatedDuration` as an `ISO8601Duration` scalar (`PT45M`)
|
|
191
|
+
* while the domain model carries minutes, because that is the only unit either
|
|
192
|
+
* host's UI renders. Year and month designators are rejected rather than
|
|
193
|
+
* guessed at — they are not calendar-independent, and Canvas never emits them
|
|
194
|
+
* for this field.
|
|
195
|
+
*/
|
|
196
|
+
export declare function parseDurationMinutes(value: string | null | undefined): number | null;
|
|
197
|
+
/**
|
|
198
|
+
* Wire string → closed enum, with anything unrecognised becoming `'unknown'`.
|
|
199
|
+
*
|
|
200
|
+
* Unlike an unmappable item `__typename`, an unknown requirement type is not
|
|
201
|
+
* dropped: the requirement still exists, the student still has to satisfy it,
|
|
202
|
+
* and hiding the row would be a worse lie than labelling it vaguely.
|
|
203
|
+
*/
|
|
204
|
+
export declare function normalizeCompletionRequirementType(type: string): CompletionRequirementType;
|
|
205
|
+
export declare function normalizePageInfo(pageInfo: GraphqlPageInfo | null | undefined): PageInfo;
|
|
206
|
+
/**
|
|
207
|
+
* The module-level student state every item in that module is read against.
|
|
208
|
+
*
|
|
209
|
+
* Built once per module and threaded into the item normaliser so the inline and
|
|
210
|
+
* on-demand paths derive the identical status for the identical item — which is
|
|
211
|
+
* why `GET_MODULE_ITEMS` re-reads `progression` alongside its items.
|
|
212
|
+
*/
|
|
213
|
+
export interface ModuleStudentContext {
|
|
214
|
+
/** Canvas reports the module locked for this student. */
|
|
215
|
+
locked: boolean;
|
|
216
|
+
/** Raw `ModuleProgression.workflowState`: locked | unlocked | started | completed. */
|
|
217
|
+
workflowState: string | null;
|
|
218
|
+
completed: boolean;
|
|
219
|
+
started: boolean;
|
|
220
|
+
/**
|
|
221
|
+
* Module *item* ids whose completion requirement the student has satisfied.
|
|
222
|
+
*
|
|
223
|
+
* `ModuleProgression.requirementsMet` is a list of `Requirement`, and
|
|
224
|
+
* `Requirement.id` is the module item id — the same join `completionRequirements`
|
|
225
|
+
* uses. The domain model wants a count, so the list collapses here.
|
|
226
|
+
*/
|
|
227
|
+
requirementsMetItemIds: ReadonlySet<string>;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The student's submission for an item, or `null`.
|
|
231
|
+
*
|
|
232
|
+
* Canvas returns a connection because the field is shared with the teacher view
|
|
233
|
+
* (where it is every student's submission); for a student it holds at most one
|
|
234
|
+
* row, so the first non-null node is the answer.
|
|
235
|
+
*/
|
|
236
|
+
export declare function firstSubmission(content: GraphqlModuleItemContent | null | undefined): GraphqlSubmission | null;
|
|
237
|
+
/**
|
|
238
|
+
* `progression` → the per-module student context, or `undefined` when there is
|
|
239
|
+
* no student. This is the gate described at the top of this block.
|
|
240
|
+
*
|
|
241
|
+
* `requirementsMet` is filtered against the requirements the module currently
|
|
242
|
+
* defines, matching Canvas's own `filterRequirementsMet`: a progression can
|
|
243
|
+
* outlive the requirement that produced it, and counting a stale entry would
|
|
244
|
+
* show a student 4 of 3. Canvas matches on id, type, `minScore` *and*
|
|
245
|
+
* `minPercentage`; we match on id and type only, because the score bounds are
|
|
246
|
+
* the part a teacher edits in place and an edited bound should not silently
|
|
247
|
+
* un-meet a requirement the student genuinely satisfied.
|
|
248
|
+
*/
|
|
249
|
+
export declare function buildStudentContext(progression: GraphqlModuleProgression | null | undefined, completionRequirements: readonly CompletionRequirement[]): ModuleStudentContext | undefined;
|
|
250
|
+
/**
|
|
251
|
+
* Per-item status. First rule that matches wins:
|
|
252
|
+
*
|
|
253
|
+
* 1. `locked` — the module is locked for this student. Canvas exposes no
|
|
254
|
+
* per-item lock in this field set (`masteryPaths.locked` is
|
|
255
|
+
* a host-specific affordance we deliberately do not fetch),
|
|
256
|
+
* and a locked module is locked for every item in it.
|
|
257
|
+
* 2. `missing` — `submission.missing`. Ahead of `graded` on purpose: this
|
|
258
|
+
* is the precedence Canvas's own student pill uses, and a
|
|
259
|
+
* teacher entering a zero on a missing submission should
|
|
260
|
+
* not make the "Missing" chip stop finding it.
|
|
261
|
+
* 3. `graded` — `state === 'graded'`, or `gradingStatus` is `graded`/
|
|
262
|
+
* `excused`. Excused counts as graded because there is
|
|
263
|
+
* nothing left for the student to do.
|
|
264
|
+
* 4. `submitted` — handed in and awaiting a grade: `submittedAt` is set, or
|
|
265
|
+
* the state is `submitted`/`pending_review`.
|
|
266
|
+
* 5. `in_progress` — the student has engaged but produced nothing gradable:
|
|
267
|
+
* either an attempt is open (`attempt > 0` with no
|
|
268
|
+
* `submittedAt`), or the item's completion requirement is
|
|
269
|
+
* already met with no submission at all — a viewed page, a
|
|
270
|
+
* followed link, a contribution to an ungraded discussion.
|
|
271
|
+
* 6. `not_started` — everything else.
|
|
272
|
+
*
|
|
273
|
+
* Rule 5 is the one to argue with. `ModuleItemStatus` has no "complete" member,
|
|
274
|
+
* so a satisfied `must_view` requirement has nowhere else to land; the domain
|
|
275
|
+
* comment already flags the same gap at module level ("All items complete" is
|
|
276
|
+
* not in the designer's list). If the designer adds one, this is the rule to
|
|
277
|
+
* change and the only one.
|
|
278
|
+
*/
|
|
279
|
+
export declare function deriveModuleItemStatus(content: GraphqlModuleItemContent | null | undefined, itemId: string, student: ModuleStudentContext): ModuleItemStatus;
|
|
280
|
+
/**
|
|
281
|
+
* Per-module rollup. First rule that matches wins:
|
|
282
|
+
*
|
|
283
|
+
* 1. `locked` — `progression.locked` (or `workflowState === 'locked'`).
|
|
284
|
+
* 2. `missing` — `submissionStatistics.missingAssignmentCount > 0` and
|
|
285
|
+
* the module is not completed. That is the design's
|
|
286
|
+
* "1+ item past due", read from the module-level
|
|
287
|
+
* statistic rather than from items, so the filter row
|
|
288
|
+
* works with nothing expanded.
|
|
289
|
+
* 3. `all_graded` — completed, *and* items were fetched, *and* every
|
|
290
|
+
* gradable one is `graded`.
|
|
291
|
+
* 4. `all_submitted` — completed. The honest floor: `progression.completed`
|
|
292
|
+
* means every requirement is met, which is submission,
|
|
293
|
+
* not grading. See the caveat below.
|
|
294
|
+
* 5. `in_progress` — started, or at least one requirement met.
|
|
295
|
+
* 6. `not_started` — everything else.
|
|
296
|
+
*
|
|
297
|
+
* Caveat on rule 3: Canvas has no module-level "everything is graded" signal —
|
|
298
|
+
* `ModuleStatistics` is only `latestDueAt` + `missingAssignmentCount`. So
|
|
299
|
+
* `all_graded` is reachable **only in inline mode**, where the items ride along
|
|
300
|
+
* with the module in the same response. Upgrading on the evidence we have beats
|
|
301
|
+
* claiming a grade we cannot see.
|
|
302
|
+
*
|
|
303
|
+
* In `onDemand` mode a completed module reports `all_submitted` and stays there,
|
|
304
|
+
* even after its items are fetched and every one of them is `graded`. That is a
|
|
305
|
+
* real limitation, not a temporary state: `progress` is derived once, from the
|
|
306
|
+
* list response, and nothing merges items back into it —
|
|
307
|
+
* `normalizeModuleItemsPage` returns `{ items, pageInfo }` and `useModulesPageData`
|
|
308
|
+
* never revisits the module. No upgrade to `all_graded` ever happens.
|
|
309
|
+
*
|
|
310
|
+
* Closing it means either recomputing the module's status in the UI from the items
|
|
311
|
+
* it holds — which reintroduces the inline/onDemand asymmetry the module-level
|
|
312
|
+
* design exists to avoid — or Canvas exposing the signal. Left open deliberately
|
|
313
|
+
* rather than papered over.
|
|
314
|
+
*/
|
|
315
|
+
export declare function deriveModuleStatus(student: ModuleStudentContext, missingCount: number, requirementsMet: number, items: ModuleItem[] | undefined): ModuleStatus;
|
|
316
|
+
/**
|
|
317
|
+
* The module's `ModuleProgress`, or `undefined` when there is no student.
|
|
318
|
+
*
|
|
319
|
+
* `requirementCount` is the module's own when Canvas set one and the number of
|
|
320
|
+
* completion requirements otherwise — Canvas uses `requirement_count` for
|
|
321
|
+
* "complete any N of these", so it is the denominator whenever it is present,
|
|
322
|
+
* and the full list is the denominator when it is not. `requirementsMet` is
|
|
323
|
+
* clamped to it so the pair can never read "3 of 1", and `percentComplete` is
|
|
324
|
+
* derived from that same clamped pair rather than computed independently, so a
|
|
325
|
+
* host can never render a bar that disagrees with the fraction beside it.
|
|
326
|
+
*/
|
|
327
|
+
export declare function deriveModuleProgress(node: GraphqlModuleNode, completionRequirements: readonly CompletionRequirement[], student: ModuleStudentContext | undefined, items: ModuleItem[] | undefined): ModuleProgress | undefined;
|
|
328
|
+
/**
|
|
329
|
+
* A single item, or `null` when its content type is outside the closed enum.
|
|
330
|
+
*
|
|
331
|
+
* `dueAt` prefers `submission.cachedDueDate` — the per-student, override-applied
|
|
332
|
+
* date Canvas denormalises onto the submission row — and falls back to
|
|
333
|
+
* `content.dueAt`. That ordering is what closes the gap a live-Canvas probe
|
|
334
|
+
* found: `dueAt` exists on `Assignment` only, `Quiz` has none, and a graded
|
|
335
|
+
* `Discussion` keeps its date on the backing assignment, but all three carry a
|
|
336
|
+
* submission. The fallback still matters, because a teacher viewer gets an
|
|
337
|
+
* assignment's `dueAt` and no submission at all.
|
|
338
|
+
*
|
|
339
|
+
* `status` is present only when `student` is — see the student-state block.
|
|
340
|
+
*/
|
|
341
|
+
export declare function normalizeModuleItem(node: GraphqlModuleItemNode, moduleId: string, completionRequirementsByItemId: ReadonlyMap<string, CompletionRequirement>, options?: NormalizeOptions, student?: ModuleStudentContext): ModuleItem | null;
|
|
342
|
+
export declare function normalizeModule(node: GraphqlModuleNode, options?: NormalizeOptions): Module;
|
|
343
|
+
export declare function normalizeModulesPage(result: GetModulesResult | null | undefined, options?: NormalizeOptions): ModulesPage;
|
|
344
|
+
export declare function normalizeModuleItemsPage(result: GetModuleItemsResult | null | undefined, moduleId: string, options?: NormalizeOptions): ModuleItemsPage;
|
|
345
|
+
//# sourceMappingURL=normalize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../../src/adapters/canvasGraphql/normalize.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { DocumentNode } from 'graphql';
|
|
2
|
+
/**
|
|
3
|
+
* Module list, optionally with each module's items inlined.
|
|
4
|
+
*
|
|
5
|
+
* `includeItems` is expressed as an `@include` directive on the nested
|
|
6
|
+
* `moduleItemsConnection` rather than as a second document. Reasoning:
|
|
7
|
+
*
|
|
8
|
+
* 1. One document means one operation name, which means one persisted-query
|
|
9
|
+
* hash to register with Canvas. Two documents would double that
|
|
10
|
+
* registration surface for what is a single conceptual read.
|
|
11
|
+
* 2. The directive is evaluated server-side, so `includeItems: false` skips
|
|
12
|
+
* resolving every per-item field — the title, content type, points and
|
|
13
|
+
* submissions for each row.
|
|
14
|
+
*
|
|
15
|
+
* What it does *not* skip is the
|
|
16
|
+
* `ModuleItemsVisibleLoader`. `moduleItemsTotalCount` and
|
|
17
|
+
* `completionRequirements` are selected unconditionally and
|
|
18
|
+
* `module_type.rb:137-141,166-169` resolves both through that same loader,
|
|
19
|
+
* so it runs whatever `includeItems` is. The saving is per-item field
|
|
20
|
+
* resolution, not the visibility query.
|
|
21
|
+
* 3. The response type is a single shape with an optional key, which is
|
|
22
|
+
* exactly how the domain model already treats it (`items?: ModuleItem[]`,
|
|
23
|
+
* where `undefined` means "not fetched"). A second document would force
|
|
24
|
+
* the normaliser to branch on which document produced the payload.
|
|
25
|
+
*
|
|
26
|
+
* `itemsFirst` bounds the inline item fetch. When a module has more items than
|
|
27
|
+
* that, the normaliser drops the partial list rather than reporting a
|
|
28
|
+
* truncated one — see `normalize.ts`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const GET_MODULES: DocumentNode;
|
|
31
|
+
/**
|
|
32
|
+
* A single module's items, paged on demand.
|
|
33
|
+
*
|
|
34
|
+
* `completionRequirements` is re-read at the module level here because a
|
|
35
|
+
* requirement is stored on the module, keyed by item id — without it, items
|
|
36
|
+
* fetched on demand would lose the requirement that items fetched inline keep,
|
|
37
|
+
* and the two paths would produce different domain objects for the same item.
|
|
38
|
+
*
|
|
39
|
+
* `progression` is re-read for the same reason: `ModuleItem.status` depends on
|
|
40
|
+
* module-level state (is the module locked for me, which of its requirements
|
|
41
|
+
* have I met), so an item fetched on demand would otherwise carry a different
|
|
42
|
+
* status from the identical item fetched inline.
|
|
43
|
+
*/
|
|
44
|
+
export declare const GET_MODULE_ITEMS: DocumentNode;
|
|
45
|
+
export interface GetModulesVariables {
|
|
46
|
+
courseId: string;
|
|
47
|
+
first: number;
|
|
48
|
+
after?: string | null;
|
|
49
|
+
includeItems: boolean;
|
|
50
|
+
itemsFirst: number;
|
|
51
|
+
/**
|
|
52
|
+
* Whether to select per-item submissions at all.
|
|
53
|
+
*
|
|
54
|
+
* False unless a `studentId` is known. Canvas's `submissionsConnection` is
|
|
55
|
+
* *not* self-scoping for a privileged viewer — see the fragment docstring — so
|
|
56
|
+
* the safe default is to ask for nothing.
|
|
57
|
+
*/
|
|
58
|
+
includeSubmissions: boolean;
|
|
59
|
+
/** The student whose submissions to select. Required whenever `includeSubmissions`. */
|
|
60
|
+
studentId?: string | null;
|
|
61
|
+
}
|
|
62
|
+
export interface GetModuleItemsVariables {
|
|
63
|
+
moduleId: string;
|
|
64
|
+
includeSubmissions: boolean;
|
|
65
|
+
studentId?: string | null;
|
|
66
|
+
first: number;
|
|
67
|
+
after?: string | null;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=queries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../../../src/adapters/canvasGraphql/queries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAiL3C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,WAAW,EAAE,YA0DzB,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,EAAE,YAiC9B,CAAA;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,YAAY,EAAE,OAAO,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;;OAMG;IACH,kBAAkB,EAAE,OAAO,CAAA;IAC3B,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAA;IAChB,kBAAkB,EAAE,OAAO,CAAA;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Module, ModuleItem } from '../types/domain';
|
|
2
|
+
import { ModulesDataAdapter } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* An adapter backed by a plain object instead of a network.
|
|
5
|
+
*
|
|
6
|
+
* It exists so the hooks can be built and tested without waiting on a real
|
|
7
|
+
* adapter, but it is not scaffolding — it stays as the test double for hook
|
|
8
|
+
* tests and as the fastest way to render a modules UI in Storybook with no
|
|
9
|
+
* server at all.
|
|
10
|
+
*
|
|
11
|
+
* It is a real implementation of the contract, deliberately: it paginates with
|
|
12
|
+
* opaque cursors, honours `AbortSignal`, and declines to inline items for modules
|
|
13
|
+
* over `INLINE_ITEM_LIMIT` — so hooks written against it meet the same edges
|
|
14
|
+
* Canvas will hand them, including the one Canvas Career gets wrong.
|
|
15
|
+
*/
|
|
16
|
+
export { INLINE_ITEM_LIMIT } from './limits';
|
|
17
|
+
export interface InMemoryFixture {
|
|
18
|
+
/**
|
|
19
|
+
* The course these modules belong to.
|
|
20
|
+
*
|
|
21
|
+
* Present so the adapter can *check* it. Without it, `courseId` was
|
|
22
|
+
* destructured and dropped, which meant one adapter instance served identical
|
|
23
|
+
* modules for every course id in existence — and since `cache/keys.ts` keys the
|
|
24
|
+
* query cache by `courseId`, the entire class of bug where it is mis-threaded,
|
|
25
|
+
* defaulted or collided across courses was unfalsifiable against the package's
|
|
26
|
+
* primary test double. A hook that hardcoded `courseId: '1'` internally would
|
|
27
|
+
* have passed every test and every story.
|
|
28
|
+
*/
|
|
29
|
+
courseId: string;
|
|
30
|
+
modules: Module[];
|
|
31
|
+
itemsByModuleId: Record<string, ModuleItem[]>;
|
|
32
|
+
}
|
|
33
|
+
/** What a served request was for. Handed to `onRequest`. */
|
|
34
|
+
export interface InMemoryRequest {
|
|
35
|
+
operation: 'listModules' | 'listModuleItems';
|
|
36
|
+
courseId: string;
|
|
37
|
+
/** Only set for `listModuleItems`. */
|
|
38
|
+
moduleId?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface InMemoryAdapterOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Invoked once per served request. Test hook for asserting exact request
|
|
43
|
+
* counts, which is how the scaling behaviour is locked in.
|
|
44
|
+
*
|
|
45
|
+
* Receives the operation name for the common case, and the full request for
|
|
46
|
+
* tests that care *which* course or module was asked for.
|
|
47
|
+
*/
|
|
48
|
+
onRequest?: (operation: 'listModules' | 'listModuleItems', request: InMemoryRequest) => void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Thrown when a caller asks this adapter for a course it was not built for.
|
|
52
|
+
*
|
|
53
|
+
* A real transport would return that other course's data, or 404. Silently
|
|
54
|
+
* serving the fixture regardless is the one behaviour that could not happen in
|
|
55
|
+
* production, so it is the one behaviour the test double must not have.
|
|
56
|
+
*/
|
|
57
|
+
export declare class InMemoryCourseMismatchError extends Error {
|
|
58
|
+
readonly expected: string;
|
|
59
|
+
readonly received: string;
|
|
60
|
+
constructor(expected: string, received: string);
|
|
61
|
+
}
|
|
62
|
+
export declare function createInMemoryAdapter(fixture: InMemoryFixture, options?: InMemoryAdapterOptions): ModulesDataAdapter;
|
|
63
|
+
//# sourceMappingURL=inMemory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inMemory.d.ts","sourceRoot":"","sources":["../../src/adapters/inMemory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAgC,MAAM,iBAAiB,CAAA;AAEvF,OAAO,KAAK,EAAwC,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAEvF;;;;;;;;;;;;GAYG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAE5C,MAAM,WAAW,eAAe;IAC9B;;;;;;;;;;OAUG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAAA;CAC9C;AAED,4DAA4D;AAC5D,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,aAAa,GAAG,iBAAiB,CAAA;IAC5C,QAAQ,EAAE,MAAM,CAAA;IAChB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE,aAAa,GAAG,iBAAiB,EAAE,OAAO,EAAE,eAAe,KAAK,IAAI,CAAA;CAC7F;AAED;;;;;;GAMG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;gBAEb,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAS/C;AA4BD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,sBAA2B,GACnC,kBAAkB,CAiEpB"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { ListModuleItemsArgs, ListModulesArgs, ModulesDataAdapter } from './types';
|
|
2
|
+
export { createInMemoryAdapter, InMemoryCourseMismatchError, INLINE_ITEM_LIMIT, type InMemoryAdapterOptions, type InMemoryFixture, type InMemoryRequest, } from './inMemory';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AACvF,OAAO,EACL,qBAAqB,EACrB,2BAA2B,EAC3B,iBAAiB,EACjB,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,YAAY,CAAA"}
|