├── .env.example
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ ├── codeql-analysis.yml
│ └── test.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .vscode
└── extensions.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE.md
├── README.md
├── SECURITY.md
├── babel.config.js
├── eslint.config.mjs
├── example
├── browser
│ ├── module
│ │ ├── index.html
│ │ └── tincan.xml
│ └── umd
│ │ ├── index.html
│ │ └── tincan.xml
└── node
│ ├── import.mjs
│ └── require.js
├── jest.config.edge.js
├── jest.config.js
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── XAPI.int.test.ts
├── XAPI.ts
├── XAPI.unit.test.ts
├── XAPIConfig.ts
├── adapters
│ ├── axiosAdapter.ts
│ ├── axiosAdapter.unit.test.ts
│ ├── fetchAdapter.ts
│ ├── fetchAdapter.unit.test.ts
│ ├── index.ts
│ ├── resolveAdapterFunction.ts
│ └── resolveAdapterFunction.unit.test.ts
├── constants
│ ├── AttachmentUsages.ts
│ ├── Resources.ts
│ ├── Verbs.ts
│ ├── Versions.ts
│ └── index.ts
├── helpers
│ ├── calculateISO8601Duration
│ │ ├── calculateISO8601Duration.ts
│ │ └── calculateISO8601Duration.unit.test.ts
│ ├── getSearchQueryParamsAsObject
│ │ ├── getSearchQueryParamsAsObject.ts
│ │ └── getSearchQueryParamsAsObject.unit.test.ts
│ ├── getTinCanLaunchData
│ │ ├── TinCanLaunchData.ts
│ │ ├── getTinCanLaunchData.ts
│ │ └── getTinCanLaunchData.unit.test.ts
│ ├── getXAPILaunchData
│ │ ├── XAPILaunchData.ts
│ │ ├── XAPILaunchParameters.ts
│ │ ├── getXAPILaunchData.ts
│ │ └── getXAPILaunchData.unit.test.ts
│ └── toBasicAuth
│ │ ├── toBasicAuth.ts
│ │ └── toBasicAuth.unit.test.ts
├── internal
│ ├── WithRequiredProperty.ts
│ ├── formatEndpoint.ts
│ ├── formatEndpoint.unit.test.ts
│ ├── multiPart.ts
│ └── multiPart.unit.test.ts
└── resources
│ ├── GetParamsBase.ts
│ ├── about
│ ├── About.ts
│ └── getAbout
│ │ ├── GetAboutParams.ts
│ │ ├── getAbout.int.test.ts
│ │ ├── getAbout.ts
│ │ └── getAbout.unit.test.ts
│ ├── activities
│ ├── Activity.ts
│ ├── ActivityDefinition.ts
│ └── getActivity
│ │ ├── GetActivityParams.ts
│ │ ├── getActivity.int.test.ts
│ │ ├── getActivity.ts
│ │ └── getActivity.unit.test.ts
│ ├── agents
│ ├── Person.ts
│ └── getAgent
│ │ ├── GetAgentParams.ts
│ │ ├── getAgent.int.test.ts
│ │ ├── getAgent.ts
│ │ └── getAgent.unit.test.ts
│ ├── document
│ ├── Document.ts
│ ├── activityProfile
│ │ ├── createActivityProfile
│ │ │ ├── CreateActivityProfileParams.ts
│ │ │ ├── createActivityProfile.int.test.ts
│ │ │ ├── createActivityProfile.ts
│ │ │ └── createActivityProfile.unit.test.ts
│ │ ├── deleteActivityProfile
│ │ │ ├── DeleteActivityProfileParams.ts
│ │ │ ├── deleteActivityProfile.int.test.ts
│ │ │ ├── deleteActivityProfile.ts
│ │ │ └── deleteActivityProfile.unit.test.ts
│ │ ├── getActivityProfile
│ │ │ ├── GetActivityProfileParams.ts
│ │ │ ├── getActivityProfile.int.test.ts
│ │ │ ├── getActivityProfile.ts
│ │ │ └── getActivityProfile.unit.test.ts
│ │ ├── getActivityProfiles
│ │ │ ├── GetActivityProfilesParams.ts
│ │ │ ├── getActivityProfiles.int.test.ts
│ │ │ ├── getActivityProfiles.ts
│ │ │ └── getActivityProfiles.unit.test.ts
│ │ └── setActivityProfile
│ │ │ ├── SetActivityProfileParams.ts
│ │ │ ├── setActivityProfile.int.test.ts
│ │ │ ├── setActivityProfile.ts
│ │ │ └── setActivityProfile.unit.test.ts
│ ├── agentProfile
│ │ ├── createAgentProfile
│ │ │ ├── CreateAgentProfileParams.ts
│ │ │ ├── createAgentProfile.int.test.ts
│ │ │ ├── createAgentProfile.ts
│ │ │ └── createAgentProfile.unit.test.ts
│ │ ├── deleteAgentProfile
│ │ │ ├── DeleteAgentProfileParams.ts
│ │ │ ├── deleteAgentProfile.int.test.ts
│ │ │ ├── deleteAgentProfile.ts
│ │ │ └── deleteAgentProfile.unit.test.ts
│ │ ├── getAgentProfile
│ │ │ ├── GetAgentProfileParams.ts
│ │ │ ├── getAgentProfile.int.test.ts
│ │ │ ├── getAgentProfile.ts
│ │ │ └── getAgentProfile.unit.test.ts
│ │ ├── getAgentProfiles
│ │ │ ├── GetAgentProfilesParams.ts
│ │ │ ├── getAgentProfiles.int.test.ts
│ │ │ ├── getAgentProfiles.ts
│ │ │ └── getAgentProfiles.unit.test.ts
│ │ └── setAgentProfile
│ │ │ ├── SetAgentProfileParams.ts
│ │ │ ├── setAgentProfile.int.test.ts
│ │ │ ├── setAgentProfile.ts
│ │ │ └── setAgentProfile.unit.test.ts
│ └── state
│ │ ├── createState
│ │ ├── CreateStateParams.ts
│ │ ├── createState.int.test.ts
│ │ ├── createState.ts
│ │ └── createState.unit.test.ts
│ │ ├── deleteState
│ │ ├── DeleteStateParams.ts
│ │ ├── deleteState.int.test.ts
│ │ ├── deleteState.ts
│ │ └── deleteState.unit.test.ts
│ │ ├── deleteStates
│ │ ├── DeleteStatesParams.ts
│ │ ├── deleteStates.int.test.ts
│ │ ├── deleteStates.ts
│ │ └── deleteStates.unit.test.ts
│ │ ├── getState
│ │ ├── GetStateParams.ts
│ │ ├── getState.int.test.ts
│ │ ├── getState.ts
│ │ └── getState.unit.test.ts
│ │ ├── getStates
│ │ ├── GetStatesParams.ts
│ │ ├── getStates.int.test.ts
│ │ ├── getStates.ts
│ │ └── getStates.unit.test.ts
│ │ └── setState
│ │ ├── SetStateParams.ts
│ │ ├── setState.int.test.ts
│ │ ├── setState.ts
│ │ └── setState.unit.test.ts
│ └── statement
│ ├── Account.ts
│ ├── Actor.ts
│ ├── Agent.ts
│ ├── AnonymousGroup.ts
│ ├── Attachment.ts
│ ├── AttachmentUsage.ts
│ ├── Context.ts
│ ├── ContextActivity.ts
│ ├── Extensions.ts
│ ├── Group.ts
│ ├── IdentifiedGroup.ts
│ ├── InteractionActivity.ts
│ ├── InteractionActivityDefinition.ts
│ ├── InverseFunctionalIdentifier.ts
│ ├── LanguageMap.ts
│ ├── ObjectiveActivity.ts
│ ├── ObjectiveActivityDefinition.ts
│ ├── Part.ts
│ ├── RFC5646LanguageCodes.ts
│ ├── Result.ts
│ ├── Statement.ts
│ ├── StatementObject.ts
│ ├── StatementParamsBase.ts
│ ├── StatementRef.ts
│ ├── StatementResponseWithAttachments.ts
│ ├── StatementsResponse.ts
│ ├── StatementsResponseWithAttachments.ts
│ ├── SubStatement.ts
│ ├── Timestamp.ts
│ ├── Verb.ts
│ ├── getMoreStatements
│ ├── GetMoreStatementsParams.ts
│ ├── getMoreStatements.int.test.ts
│ ├── getMoreStatements.ts
│ └── getMoreStatements.unit.test.ts
│ ├── getStatement
│ ├── GetStatementParams.ts
│ ├── getStatement.int.test.ts
│ ├── getStatement.ts
│ └── getStatement.unit.test.ts
│ ├── getStatements
│ ├── GetStatementsParams.ts
│ ├── getStatements.int.test.ts
│ ├── getStatements.ts
│ └── getStatements.unit.test.ts
│ ├── getVoidedStatement
│ ├── GetVoidedStatementParams.ts
│ ├── getVoidedStatement.int.test.ts
│ ├── getVoidedStatement.ts
│ └── getVoidedStatement.unit.test.ts
│ ├── index.ts
│ ├── sendStatement
│ ├── SendStatementParams.ts
│ ├── sendStatement.int.test.ts
│ ├── sendStatement.ts
│ └── sendStatement.unit.test.ts
│ ├── sendStatements
│ ├── SendStatementsParams.ts
│ ├── sendStatements.int.test.ts
│ ├── sendStatements.ts
│ └── sendStatements.unit.test.ts
│ ├── voidStatement
│ ├── VoidStatementParams.ts
│ ├── voidStatement.int.test.ts
│ ├── voidStatement.ts
│ └── voidStatement.unit.test.ts
│ └── voidStatements
│ ├── VoidStatementsParams.ts
│ ├── voidStatements.int.test.ts
│ ├── voidStatements.ts
│ └── voidStatements.unit.test.ts
├── test
├── arrayBufferToWordArray.ts
├── constants.ts
├── getCredentials.ts
├── jestUtils.ts
├── mockAxios.ts
├── mockFetch.ts
├── polyfillFetch.ts
├── setupAxios.ts
├── setupFetch.ts
└── setupInt.ts
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | LRS_CREDENTIALS_ARRAY="[{\"endpoint\":\"https://cloud.scorm.com/lrs/xxxxxxxxxx/sandbox/\",\"username\":\"\",\"password\":\"\"}]"
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [CookieCookson]
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | - package-ecosystem: "npm"
5 | directory: /
6 | target-branch: develop
7 | schedule:
8 | interval: "monthly"
9 | groups:
10 | all-deps:
11 | update-types:
12 | - "major"
13 | - "minor"
14 | - "patch"
15 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | name: "CodeQL"
7 |
8 | on:
9 | push:
10 | branches:
11 | - 'develop'
12 | pull_request:
13 | # The branches below must be a subset of the branches above
14 | branches:
15 | - 'develop'
16 | schedule:
17 | - cron: '0 3 * * 3'
18 |
19 | jobs:
20 | analyze:
21 | name: Analyze
22 | runs-on: ubuntu-latest
23 |
24 | strategy:
25 | fail-fast: false
26 | matrix:
27 | # Override automatic language detection by changing the below list
28 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
29 | language: ['javascript']
30 | # Learn more...
31 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
32 |
33 | steps:
34 | - name: Checkout repository
35 | uses: actions/checkout@v4
36 | with:
37 | # We must fetch at least the immediate parents so that if this is
38 | # a pull request then we can checkout the head.
39 | fetch-depth: 2
40 |
41 | # Initializes the CodeQL tools for scanning.
42 | - name: Initialize CodeQL
43 | uses: github/codeql-action/init@v2
44 | with:
45 | languages: ${{ matrix.language }}
46 | # If you wish to specify custom queries, you can do so here or in a config file.
47 | # By default, queries listed here will override any specified in a config file.
48 | # Prefix the list here with "+" to use these queries and those in the config file.
49 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
50 |
51 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
52 | # If this step fails, then you should remove it and run the build manually (see below)
53 | - name: Autobuild
54 | uses: github/codeql-action/autobuild@v2
55 |
56 | # ℹ️ Command-line programs to run using the OS shell.
57 | # 📚 https://git.io/JvXDl
58 |
59 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
60 | # and modify them (or add more) to build your code if your project
61 | # uses a compiled language
62 |
63 | #- run: |
64 | # make bootstrap
65 | # make release
66 |
67 | - name: Perform CodeQL Analysis
68 | uses: github/codeql-action/analyze@v2
69 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'develop'
7 | pull_request:
8 | branches:
9 | - 'develop'
10 |
11 | jobs:
12 | test:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v4
17 |
18 | - name: Use Node.js 18
19 | uses: actions/setup-node@v4
20 | with:
21 | node-version: 18
22 |
23 | - name: Install Dependencies
24 | run: npm ci
25 |
26 | - name: Prettier
27 | run: npm run test:format
28 |
29 | - name: ESLint
30 | run: npm run lint
31 |
32 | - name: Build
33 | run: npm run build --if-present
34 |
35 | - name: Test
36 | run: npm run test
37 | env:
38 | CI: true
39 | LRS_CREDENTIALS_ARRAY: ${{ secrets.LRS_CREDENTIALS_ARRAY }}
40 |
41 | - name: Coverage
42 | uses: coverallsapp/github-action@master
43 | with:
44 | github-token: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /dist
3 | .env
4 | /coverage
5 | .DS_Store
6 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .github
2 | node_modules
3 | dist
4 | example
5 | coverage
6 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "auto",
3 | "printWidth": 80,
4 | "semi": true,
5 | "singleQuote": false,
6 | "tabWidth": 2,
7 | "trailingComma": "es5"
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Christian Cook
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [](https://www.npmjs.com/package/@xapi/xapi)
3 | [](https://github.com/xapijs/xapi/actions/workflows/test.yml)
4 | [](https://coveralls.io/github/xapijs/xapi?branch=develop)
5 | [](https://codeclimate.com/github/xapijs/xapi/maintainability)
6 | 
7 | 
8 |
9 | [
](https://www.xapijs.dev)
10 |
11 | # xAPI.js - xAPI Wrapper Library
12 |
13 | The **xAPI.js** Wrapper Library is a strongly typed JavaScript library for enabling learning content and learning systems to speak to each other. It is a complete implementation and fully compliant against the [xAPI Specification](https://github.com/adlnet/xAPI-Spec) (v1.0.0 - v1.0.3).
14 |
15 | - [API Documentation](https://www.xapijs.dev/xapi-wrapper-library)
16 | - [Basic Examples](https://github.com/xapijs/xapi/tree/master/example)
17 | - [Demo](https://github.com/xapijs/xapi-demo)
18 |
19 | ## xAPI Profile Libraries
20 |
21 | - [cmi5](https://github.com/xapijs/cmi5)
22 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | ------- | ------------------ |
7 | | 2.x.x | :white_check_mark: |
8 | | 1.x.x | :x: |
9 |
10 | ## Reporting a Vulnerability
11 |
12 | Please report security vulnerabilities by raising an issue on this repository, and we will strive a best effort to get a patched package distributed as quickly as possible.
13 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | "@babel/preset-env",
5 | {
6 | targets: {
7 | node: "current",
8 | },
9 | },
10 | ],
11 | "@babel/preset-typescript",
12 | ],
13 | plugins: ["@babel/plugin-transform-optional-chaining"],
14 | };
15 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import eslint from "@eslint/js";
3 | import tseslint from "typescript-eslint";
4 | import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
5 |
6 | export default [
7 | ...tseslint.config(eslint.configs.recommended, tseslint.configs.recommended),
8 | eslintPluginPrettierRecommended,
9 | {
10 | ignores: ["dist/*", "example/*", "*.js"],
11 | },
12 | ];
13 |
--------------------------------------------------------------------------------
/example/browser/module/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
--------------------------------------------------------------------------------
/example/browser/module/tincan.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | xAPI.js xAPI demo
6 | An example course using the xAPI.js demo.
7 | index.html
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/example/browser/umd/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
--------------------------------------------------------------------------------
/example/browser/umd/tincan.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | xAPI.js xAPI demo
6 | An example course using the xAPI.js demo.
7 | index.html
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/example/node/import.mjs:
--------------------------------------------------------------------------------
1 | import XAPI from "../../dist/XAPI.cjs";
2 | import dotenv from "dotenv";
3 |
4 | dotenv.config();
5 |
6 | const credentials = JSON.parse(process.env.LRS_CREDENTIALS_ARRAY)[0];
7 | const endpoint = credentials.endpoint;
8 | const username = credentials.username;
9 | const password = credentials.password;
10 | const auth = XAPI.toBasicAuth(username, password);
11 | const xapi = new XAPI({
12 | endpoint: endpoint,
13 | auth: auth
14 | });
15 |
16 | xapi.getAbout().then((result) => {
17 | console.log(result.data);
18 | });
19 |
--------------------------------------------------------------------------------
/example/node/require.js:
--------------------------------------------------------------------------------
1 | const XAPI = require("../../dist/XAPI.cjs.js");
2 |
3 | require("dotenv").config();
4 |
5 | const credentials = JSON.parse(process.env.LRS_CREDENTIALS_ARRAY)[0];
6 | const endpoint = credentials.endpoint;
7 | const username = credentials.username;
8 | const password = credentials.password;
9 | const auth = XAPI.toBasicAuth(username, password);
10 | const xapi = new XAPI({
11 | endpoint: endpoint,
12 | auth: auth
13 | });
14 |
15 | xapi.getAbout().then((result) => {
16 | console.log(result.data);
17 | });
18 |
--------------------------------------------------------------------------------
/jest.config.edge.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 |
3 | module.exports = {
4 | preset: "ts-jest",
5 | projects: [
6 | {
7 | displayName: "edge-fetch-unit",
8 | testMatch: ["**/*.unit.test.ts"],
9 | testEnvironment: "@edge-runtime/jest-environment",
10 | setupFiles: ["./test/mockFetch.ts", "./test/setupFetch.ts"],
11 | },
12 | {
13 | displayName: "edge-fetch-int",
14 | testMatch: ["**/*.int.test.ts"],
15 | testEnvironment: "@edge-runtime/jest-environment",
16 | setupFilesAfterEnv: ["./test/setupInt.ts", "./test/setupFetch.ts"],
17 | },
18 | ],
19 | };
20 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 |
3 | module.exports = {
4 | preset: "ts-jest",
5 | collectCoverage: true,
6 | coverageThreshold: {
7 | global: {
8 | branches: 0,
9 | functions: 0,
10 | lines: 0,
11 | statements: 0,
12 | },
13 | },
14 | projects: [
15 | {
16 | displayName: "dom-axios-unit",
17 | testMatch: ["**/*.unit.test.ts"],
18 | testEnvironment: "jsdom",
19 | setupFiles: [
20 | "./test/mockAxios.ts",
21 | "./test/mockFetch.ts",
22 | "./test/setupAxios.ts",
23 | ],
24 | },
25 | {
26 | displayName: "dom-fetch-unit",
27 | testMatch: ["**/*.unit.test.ts"],
28 | testEnvironment: "jsdom",
29 | setupFiles: [
30 | "./test/mockAxios.ts",
31 | "./test/mockFetch.ts",
32 | "./test/setupFetch.ts",
33 | ],
34 | },
35 | {
36 | displayName: "node-axios-unit",
37 | testMatch: ["**/*.unit.test.ts"],
38 | testEnvironment: "node",
39 | setupFiles: [
40 | "./test/mockAxios.ts",
41 | "./test/mockFetch.ts",
42 | "./test/setupAxios.ts",
43 | ],
44 | },
45 | {
46 | displayName: "node-fetch-unit",
47 | testMatch: ["**/*.unit.test.ts"],
48 | testEnvironment: "node",
49 | setupFiles: [
50 | "./test/mockAxios.ts",
51 | "./test/mockFetch.ts",
52 | "./test/setupFetch.ts",
53 | ],
54 | },
55 | {
56 | displayName: "dom-axios-int",
57 | testMatch: ["**/*.int.test.ts"],
58 | testEnvironment: "jsdom",
59 | setupFilesAfterEnv: ["./test/setupInt.ts"],
60 | },
61 | {
62 | displayName: "dom-fetch-int",
63 | testMatch: ["**/*.int.test.ts"],
64 | testEnvironment: "jsdom",
65 | setupFilesAfterEnv: [
66 | "./test/setupInt.ts",
67 | "./test/polyfillFetch.ts",
68 | "./test/setupFetch.ts",
69 | ],
70 | },
71 | {
72 | displayName: "node-axios-int",
73 | testMatch: ["**/*.int.test.ts"],
74 | testEnvironment: "node",
75 | setupFilesAfterEnv: ["./test/setupInt.ts", "./test/setupAxios.ts"],
76 | },
77 | {
78 | displayName: "node-fetch-int",
79 | testMatch: ["**/*.int.test.ts"],
80 | testEnvironment: "node",
81 | setupFilesAfterEnv: ["./test/setupInt.ts", "./test/setupFetch.ts"],
82 | },
83 | ],
84 | };
85 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@xapi/xapi",
3 | "version": "3.0.1",
4 | "description": "Communicate over xAPI using JavaScript.",
5 | "main": "dist/XAPI.cjs.js",
6 | "module": "dist/XAPI.esm.js",
7 | "browser": "dist/XAPI.umd.js",
8 | "typings": "dist/types/XAPI.d.ts",
9 | "files": [
10 | "dist/**/*"
11 | ],
12 | "scripts": {
13 | "prepublishOnly": "npm run build",
14 | "clean": "rimraf ./dist",
15 | "build:js": "rollup --config --bundleConfigAsCjs",
16 | "build:types": "tsc --emitDeclarationOnly",
17 | "build": "npm run clean && npm run build:types && npm run build:js",
18 | "format": "prettier --write '**/*.{js,jsx,json,ts,tsx}'",
19 | "test": "jest --runInBand && npm run test:edge && npm run test:example:node:require && npm run test:example:node:import",
20 | "test:edge": "jest --runInBand --config jest.config.edge.js",
21 | "test:unit": "jest --selectProjects dom-axios-unit dom-fetch-unit node-axios-unit node-fetch-unit",
22 | "test:int": "jest --selectProjects dom-axios-int dom-fetch-int node-axios-int node-fetch-int --runInBand",
23 | "test:format": "prettier --check .",
24 | "test:example:node:require": "node ./example/node/require.js",
25 | "test:example:node:import": "node --experimental-modules --es-module-specifier-resolution=node ./example/node/import.mjs",
26 | "lint": "eslint . --max-warnings=0"
27 | },
28 | "repository": {
29 | "type": "git",
30 | "url": "https://github.com/xapijs/xapi.git"
31 | },
32 | "keywords": [
33 | "xapi",
34 | "typescript"
35 | ],
36 | "author": "Christian Cook",
37 | "license": "MIT",
38 | "bugs": {
39 | "url": "https://github.com/xapijs/xapi/issues"
40 | },
41 | "homepage": "https://www.xapijs.dev",
42 | "funding": "https://github.com/sponsors/CookieCookson",
43 | "devDependencies": {
44 | "@babel/core": "^7.23.2",
45 | "@babel/plugin-transform-optional-chaining": "^7.25.9",
46 | "@babel/preset-env": "^7.23.2",
47 | "@babel/preset-typescript": "^7.23.2",
48 | "@edge-runtime/jest-environment": "^3.0.4",
49 | "@eslint/js": "^9.15.0",
50 | "@rollup/plugin-babel": "^6.0.4",
51 | "@rollup/plugin-commonjs": "^28.0.1",
52 | "@rollup/plugin-json": "^6.0.1",
53 | "@rollup/plugin-node-resolve": "^15.2.3",
54 | "@rollup/plugin-terser": "^0.4.4",
55 | "@types/crypto-js": "^4.2.2",
56 | "@types/jest": "^29.5.7",
57 | "@types/node": "^22.9.1",
58 | "babel-jest": "^29.7.0",
59 | "crypto-js": "^4.2.0",
60 | "dotenv": "^16.3.1",
61 | "eslint": "^9.15.0",
62 | "eslint-config-prettier": "^9.1.0",
63 | "eslint-plugin-prettier": "^5.2.1",
64 | "jest": "^29.7.0",
65 | "jest-environment-jsdom": "^29.7.0",
66 | "prettier": "^3.0.3",
67 | "rimraf": "^6.0.1",
68 | "rollup": "^4.3.0",
69 | "ts-jest": "^29.1.1",
70 | "typescript": "^5.6.3",
71 | "typescript-eslint": "^8.15.0",
72 | "whatwg-fetch": "^3.6.20"
73 | },
74 | "dependencies": {
75 | "axios": "^1.6.0"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from "@rollup/plugin-babel";
2 | import resolve from "@rollup/plugin-node-resolve";
3 | import commonjs from "@rollup/plugin-commonjs";
4 | import json from "@rollup/plugin-json";
5 | import pkg from "./package.json";
6 | import terser from "@rollup/plugin-terser";
7 |
8 | const input = "./src/XAPI.ts";
9 |
10 | const extensions = [".js", ".ts"];
11 |
12 | const resolveOptions = {
13 | extensions: extensions,
14 | };
15 |
16 | const babelPluginOptions = {
17 | babelHelpers: "bundled",
18 | extensions: extensions,
19 | };
20 |
21 | export default [
22 | {
23 | input: input,
24 | plugins: [
25 | resolve({
26 | ...resolveOptions,
27 | browser: true,
28 | }),
29 | commonjs(), // Used for Axios import
30 | json(),
31 | babel(babelPluginOptions),
32 | terser(),
33 | ],
34 | output: [
35 | {
36 | file: pkg.browser,
37 | format: "umd",
38 | name: "XAPI",
39 | },
40 | {
41 | file: pkg.module,
42 | format: "esm",
43 | exports: "default",
44 | },
45 | ],
46 | },
47 | {
48 | input: input,
49 | plugins: [
50 | resolve({
51 | ...resolveOptions,
52 | browser: false,
53 | }),
54 | commonjs(), // Used for Axios import
55 | json(),
56 | babel(babelPluginOptions),
57 | terser(),
58 | ],
59 | external: [
60 | "http",
61 | "https",
62 | "url",
63 | "zlib",
64 | "stream",
65 | "assert",
66 | "tty",
67 | "util",
68 | "os",
69 | ],
70 | output: [
71 | {
72 | file: pkg.main,
73 | format: "cjs",
74 | exports: "default",
75 | },
76 | ],
77 | },
78 | ];
79 |
--------------------------------------------------------------------------------
/src/XAPI.int.test.ts:
--------------------------------------------------------------------------------
1 | import { forEachLRS } from "../test/getCredentials";
2 | import XAPI from "./XAPI";
3 |
4 | forEachLRS((_xapi, credential) => {
5 | const endpoint: string = credential.endpoint || "";
6 |
7 | describe("xapi constructor", () => {
8 | test("can perform basic authentication challenges when no authorization process is required", () => {
9 | const noAuthXapi = new XAPI({
10 | endpoint: endpoint,
11 | adapter: global.adapter,
12 | });
13 | expect(noAuthXapi.getAbout()).resolves.toBeDefined();
14 | });
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/XAPI.unit.test.ts:
--------------------------------------------------------------------------------
1 | import { testEndpoint } from "../test/constants";
2 | import XAPI from "./XAPI";
3 | import { toBasicAuth } from "./helpers/toBasicAuth/toBasicAuth";
4 | import { Versions } from "./constants";
5 |
6 | describe("xapi constructor", () => {
7 | beforeEach(() => {
8 | global.adapterFn.mockClear();
9 | global.adapterFn.mockResolvedValueOnce({
10 | headers: {
11 | "content-type": "application/json",
12 | },
13 | });
14 | });
15 |
16 | test("can be constructed with an endpoint", () => {
17 | const xapi = new XAPI({
18 | endpoint: testEndpoint,
19 | adapter: global.adapter,
20 | });
21 | xapi.getAbout();
22 | expect(global.adapterFn).toHaveBeenCalledWith(
23 | expect.objectContaining({
24 | headers: expect.objectContaining({
25 | Authorization: toBasicAuth("", ""),
26 | }),
27 | })
28 | );
29 | });
30 |
31 | test("can be constructed with an endpoint and auth", () => {
32 | const xapi = new XAPI({
33 | endpoint: testEndpoint,
34 | auth: "test",
35 | adapter: global.adapter,
36 | });
37 | xapi.getAbout();
38 | expect(global.adapterFn).toHaveBeenCalledWith(
39 | expect.objectContaining({
40 | headers: expect.objectContaining({
41 | Authorization: "test",
42 | }),
43 | })
44 | );
45 | });
46 |
47 | test("can be constructed with an endpoint and version", () => {
48 | const xapi = new XAPI({
49 | endpoint: testEndpoint,
50 | version: "1.0.0",
51 | adapter: global.adapter,
52 | });
53 | xapi.getAbout();
54 | expect(global.adapterFn).toHaveBeenCalledWith(
55 | expect.objectContaining({
56 | headers: expect.objectContaining({
57 | "X-Experience-API-Version": "1.0.0" as Versions,
58 | }),
59 | })
60 | );
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/src/XAPIConfig.ts:
--------------------------------------------------------------------------------
1 | import { Adapter } from "./adapters";
2 | import { Versions } from "./constants";
3 |
4 | export interface XAPIConfig {
5 | endpoint: string;
6 | auth?: string;
7 | version?: Versions;
8 | adapter?: Adapter;
9 | }
10 |
--------------------------------------------------------------------------------
/src/adapters/axiosAdapter.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { AdapterFunction } from ".";
3 |
4 | const axiosAdapter: AdapterFunction = ({ url, method, data, headers }) => {
5 | return axios
6 | .request({
7 | url,
8 | method,
9 | data,
10 | headers,
11 | })
12 | .then(({ data, headers, status }) => ({
13 | data,
14 | headers,
15 | status,
16 | }));
17 | };
18 |
19 | export default axiosAdapter;
20 |
--------------------------------------------------------------------------------
/src/adapters/axiosAdapter.unit.test.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import axiosAdapter from "./axiosAdapter";
3 | import { isEdgeRuntime, testIf } from "../../test/jestUtils";
4 |
5 | describe("axiosAdapter", () => {
6 | testIf(!isEdgeRuntime())("makes an axios network request", async () => {
7 | axiosAdapter({
8 | url: "https://www.example.com",
9 | method: "POST",
10 | data: "foo",
11 | headers: {
12 | Authorization: "Basic ABCDEFG",
13 | },
14 | });
15 |
16 | expect(axios.request).toHaveBeenCalledWith(
17 | expect.objectContaining({
18 | url: "https://www.example.com",
19 | method: "POST",
20 | data: "foo",
21 | headers: {
22 | Authorization: "Basic ABCDEFG",
23 | },
24 | })
25 | );
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/src/adapters/fetchAdapter.ts:
--------------------------------------------------------------------------------
1 | import { AdapterFunction } from ".";
2 |
3 | const fetchAdapter: AdapterFunction = async ({
4 | url,
5 | method,
6 | data,
7 | headers,
8 | }) => {
9 | let body = data;
10 | const contentType = headers["Content-Type"];
11 | if (contentType === "application/json") {
12 | body = JSON.stringify(body);
13 | }
14 | const response = await fetch(url, {
15 | method,
16 | body,
17 | headers,
18 | });
19 | if (!response.ok) {
20 | const err = await response.text();
21 | throw new Error(err);
22 | }
23 | let text = await response.text();
24 | try {
25 | text = JSON.parse(text);
26 | // eslint-disable-next-line no-empty
27 | } catch {}
28 | return {
29 | data: text as T,
30 | headers: Object.fromEntries(response.headers.entries()),
31 | status: response.status,
32 | };
33 | };
34 |
35 | export default fetchAdapter;
36 |
--------------------------------------------------------------------------------
/src/adapters/fetchAdapter.unit.test.ts:
--------------------------------------------------------------------------------
1 | import fetchAdapter from "./fetchAdapter";
2 |
3 | describe("fetchAdapter", () => {
4 | it("makes a fetch network request", async () => {
5 | fetchAdapter({
6 | url: "https://www.example.com",
7 | method: "POST",
8 | data: "foo",
9 | headers: {
10 | Authorization: "Basic ABCDEFG",
11 | },
12 | });
13 |
14 | expect(fetch).toHaveBeenCalledWith(
15 | "https://www.example.com",
16 | expect.objectContaining({
17 | method: "POST",
18 | body: "foo",
19 | headers: {
20 | Authorization: "Basic ABCDEFG",
21 | },
22 | })
23 | );
24 | });
25 |
26 | it("stringifies JSON data", async () => {
27 | fetchAdapter({
28 | url: "https://www.example.com",
29 | method: "POST",
30 | data: { foo: true },
31 | headers: {
32 | "Content-Type": "application/json",
33 | },
34 | });
35 |
36 | expect(fetch).toHaveBeenCalledWith(
37 | "https://www.example.com",
38 | expect.objectContaining({
39 | method: "POST",
40 | body: '{"foo":true}',
41 | headers: {
42 | "Content-Type": "application/json",
43 | },
44 | })
45 | );
46 | });
47 |
48 | it("Returns a rejected promise with error message if an error is encountered", () => {
49 | (fetch as jest.MockedFn).mockImplementationOnce(() =>
50 | Promise.resolve({
51 | text: () => Promise.resolve("i am error"),
52 | ok: false,
53 | } as Response)
54 | );
55 |
56 | const result = fetchAdapter({
57 | url: "https://www.example.com",
58 | method: "GET",
59 | headers: {
60 | "Content-Type": "application/json",
61 | },
62 | });
63 |
64 | expect(result).rejects.toThrow("i am error");
65 | });
66 |
67 | it("Parses fetch text as JSON", () => {
68 | (fetch as jest.MockedFn).mockImplementationOnce(() =>
69 | Promise.resolve({
70 | text: () => Promise.resolve('{"foo": true}'),
71 | ok: true,
72 | headers: new Headers(),
73 | } as Response)
74 | );
75 |
76 | const result = fetchAdapter({
77 | url: "https://www.example.com",
78 | method: "GET",
79 | headers: {
80 | "Content-Type": "application/json",
81 | },
82 | });
83 |
84 | expect(result).resolves.toEqual(
85 | expect.objectContaining({
86 | data: { foo: true },
87 | })
88 | );
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/src/adapters/index.ts:
--------------------------------------------------------------------------------
1 | import axiosAdapter from "./axiosAdapter";
2 | import fetchAdapter from "./fetchAdapter";
3 | import resolveAdapterFunction from "./resolveAdapterFunction";
4 |
5 | interface AdapterRequest {
6 | method: "GET" | "POST" | "PUT" | "DELETE" | string;
7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
8 | headers?: Record;
9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
10 | data?: any;
11 | }
12 |
13 | interface AdapterResponse {
14 | data: T;
15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
16 | headers: Record;
17 | status: number;
18 | }
19 |
20 | type AdapterPromise = Promise>;
21 |
22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
23 | type AdapterFunction = (
24 | params: AdapterRequest & { url: string }
25 | ) => AdapterPromise;
26 |
27 | type Adapter = "fetch" | "axios" | AdapterFunction;
28 |
29 | export type {
30 | AdapterRequest,
31 | AdapterResponse,
32 | AdapterPromise,
33 | AdapterFunction,
34 | Adapter,
35 | };
36 |
37 | export { axiosAdapter, fetchAdapter, resolveAdapterFunction };
38 |
--------------------------------------------------------------------------------
/src/adapters/resolveAdapterFunction.ts:
--------------------------------------------------------------------------------
1 | import { Adapter, AdapterFunction, axiosAdapter, fetchAdapter } from ".";
2 |
3 | const resolveAdapterFunction = (adapter?: Adapter): AdapterFunction => {
4 | if (typeof adapter === "function") {
5 | return adapter;
6 | } else {
7 | switch (adapter) {
8 | case "fetch": {
9 | return fetchAdapter;
10 | }
11 | case "axios":
12 | default: {
13 | return axiosAdapter;
14 | }
15 | }
16 | }
17 | };
18 |
19 | export default resolveAdapterFunction;
20 |
--------------------------------------------------------------------------------
/src/adapters/resolveAdapterFunction.unit.test.ts:
--------------------------------------------------------------------------------
1 | import { AdapterFunction } from ".";
2 | import axiosAdapter from "./axiosAdapter";
3 | import fetchAdapter from "./fetchAdapter";
4 | import resolveAdapterFunction from "./resolveAdapterFunction";
5 |
6 | describe("resolveAdapterFunction", () => {
7 | it("Returns axios by default", () => {
8 | const result = resolveAdapterFunction();
9 |
10 | expect(result).toBe(axiosAdapter);
11 | });
12 |
13 | it("Returns axios if provided as parameter", () => {
14 | const result = resolveAdapterFunction("axios");
15 |
16 | expect(result).toBe(axiosAdapter);
17 | });
18 |
19 | it("Returns fetch if provided as parameter", () => {
20 | const result = resolveAdapterFunction("fetch");
21 |
22 | expect(result).toBe(fetchAdapter);
23 | });
24 |
25 | it("Returns custom if function provided as parameter", () => {
26 | const customAdapter: AdapterFunction = () =>
27 | Promise.resolve({
28 | data: {} as T,
29 | headers: {},
30 | status: 0,
31 | });
32 | const result = resolveAdapterFunction(customAdapter);
33 |
34 | expect(result).toBe(customAdapter);
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/src/constants/AttachmentUsages.ts:
--------------------------------------------------------------------------------
1 | export enum AttachmentUsages {
2 | SIGNATURE = "http://adlnet.gov/expapi/attachments/signature",
3 | CERTIFICATE_OF_COMPLETION = "http://id.tincanapi.com/attachment/certificate-of-completion",
4 | CONTRACT = "http://id.tincanapi.com/attachment/contract",
5 | SUPPORTING_MEDIA = "http://id.tincanapi.com/attachment/supporting_media",
6 | }
7 |
--------------------------------------------------------------------------------
/src/constants/Resources.ts:
--------------------------------------------------------------------------------
1 | export enum Resources {
2 | ABOUT = "about",
3 | AGENTS = "agents",
4 | ACTIVITIES = "activities",
5 | ACTIVITY_PROFILE = "activities/profile",
6 | STATE = "activities/state",
7 | AGENT_PROFILE = "agents/profile",
8 | STATEMENT = "statements",
9 | }
10 |
--------------------------------------------------------------------------------
/src/constants/Verbs.ts:
--------------------------------------------------------------------------------
1 | import { Verb } from "../XAPI";
2 |
3 | export class Verbs {
4 | public static readonly INITIALIZED: Verb = {
5 | id: "http://adlnet.gov/expapi/verbs/initialized",
6 | display: {
7 | "en-US": "initialized",
8 | },
9 | };
10 | public static readonly TERMINATED: Verb = {
11 | id: "http://adlnet.gov/expapi/verbs/terminated",
12 | display: {
13 | "en-US": "terminated",
14 | },
15 | };
16 | public static readonly SUSPENDED: Verb = {
17 | id: "http://adlnet.gov/expapi/verbs/suspended",
18 | display: {
19 | "en-US": "suspended",
20 | },
21 | };
22 | public static readonly RESUMED: Verb = {
23 | id: "http://adlnet.gov/expapi/verbs/resumed",
24 | display: {
25 | "en-US": "resumed",
26 | },
27 | };
28 | public static readonly PASSED: Verb = {
29 | id: "http://adlnet.gov/expapi/verbs/passed",
30 | display: {
31 | "en-US": "passed",
32 | },
33 | };
34 | public static readonly FAILED: Verb = {
35 | id: "http://adlnet.gov/expapi/verbs/failed",
36 | display: {
37 | "en-US": "failed",
38 | },
39 | };
40 | public static readonly SCORED: Verb = {
41 | id: "http://adlnet.gov/expapi/verbs/scored",
42 | display: {
43 | "en-US": "scored",
44 | },
45 | };
46 | public static readonly COMPLETED: Verb = {
47 | id: "http://adlnet.gov/expapi/verbs/completed",
48 | display: {
49 | "en-US": "completed",
50 | },
51 | };
52 | public static readonly RESPONDED: Verb = {
53 | id: "http://adlnet.gov/expapi/verbs/responded",
54 | display: {
55 | "en-US": "responded",
56 | },
57 | };
58 | public static readonly COMMENTED: Verb = {
59 | id: "http://adlnet.gov/expapi/verbs/commented",
60 | display: {
61 | "en-US": "commented",
62 | },
63 | };
64 | public static readonly VOIDED: Verb = {
65 | id: "http://adlnet.gov/expapi/verbs/voided",
66 | display: {
67 | "en-US": "voided",
68 | },
69 | };
70 | public static readonly PROGRESSED: Verb = {
71 | id: "http://adlnet.gov/expapi/verbs/progressed",
72 | display: {
73 | "en-US": "progressed",
74 | },
75 | };
76 | public static readonly ANSWERED: Verb = {
77 | id: "http://adlnet.gov/expapi/verbs/answered",
78 | display: {
79 | "en-US": "answered",
80 | },
81 | };
82 | }
83 |
--------------------------------------------------------------------------------
/src/constants/Versions.ts:
--------------------------------------------------------------------------------
1 | export type Versions = "1.0.0" | "1.0.1" | "1.0.2" | "1.0.3";
2 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./AttachmentUsages";
2 | export * from "./Resources";
3 | export * from "./Verbs";
4 | export * from "./Versions";
5 |
--------------------------------------------------------------------------------
/src/helpers/calculateISO8601Duration/calculateISO8601Duration.ts:
--------------------------------------------------------------------------------
1 | const millisecondsInMinute = 60000;
2 | const millisecondsInHour = millisecondsInMinute * 60;
3 | const millisecondsInDay = millisecondsInHour * 24;
4 |
5 | export function calculateISO8601Duration(
6 | startDate: Date,
7 | endDate: Date
8 | ): string {
9 | const differenceMilliseconds = endDate.getTime() - startDate.getTime();
10 | if (differenceMilliseconds <= 0) return "PT0S";
11 | const days = Math.floor(differenceMilliseconds / millisecondsInDay);
12 | const hoursMilliseconds = differenceMilliseconds % millisecondsInDay;
13 | const hours = Math.floor(hoursMilliseconds / millisecondsInHour);
14 | const minuteMilliseconds = hoursMilliseconds % millisecondsInHour;
15 | const minutes = Math.floor(minuteMilliseconds / millisecondsInMinute);
16 | const remainingMilliseconds = minuteMilliseconds % millisecondsInMinute;
17 | const seconds = remainingMilliseconds / 1000;
18 | return `P${days ? days + "D" : ""}T${hours ? hours + "H" : ""}${
19 | minutes ? minutes + "M" : ""
20 | }${seconds ? seconds + "S" : ""}`;
21 | }
22 |
--------------------------------------------------------------------------------
/src/helpers/calculateISO8601Duration/calculateISO8601Duration.unit.test.ts:
--------------------------------------------------------------------------------
1 | import { calculateISO8601Duration } from "./calculateISO8601Duration";
2 |
3 | describe("calculateISO8601Duration", () => {
4 | test("should create correct timestring for milliseconds", () => {
5 | const startDate: Date = new Date();
6 | const endDate: Date = new Date(startDate);
7 | endDate.setMilliseconds(endDate.getMilliseconds() + 123);
8 | expect(calculateISO8601Duration(startDate, endDate)).toEqual("PT0.123S");
9 | });
10 |
11 | test("should create correct timestring for seconds", () => {
12 | const startDate: Date = new Date();
13 | const endDate: Date = new Date(startDate);
14 | endDate.setSeconds(endDate.getSeconds() + 1);
15 | expect(calculateISO8601Duration(startDate, endDate)).toEqual("PT1S");
16 | });
17 |
18 | test("should create correct timestring for minutes", () => {
19 | const startDate: Date = new Date();
20 | const endDate: Date = new Date(startDate);
21 | endDate.setMinutes(endDate.getMinutes() + 1);
22 | expect(calculateISO8601Duration(startDate, endDate)).toEqual("PT1M");
23 | });
24 |
25 | test("should create correct timestring for hours", () => {
26 | const startDate: Date = new Date();
27 | const endDate: Date = new Date(startDate);
28 | endDate.setHours(endDate.getHours() + 1);
29 | expect(calculateISO8601Duration(startDate, endDate)).toEqual("PT1H");
30 | });
31 |
32 | test("should create correct timestring for days", () => {
33 | const startDate: Date = new Date();
34 | const endDate: Date = new Date(startDate);
35 | endDate.setDate(endDate.getDate() + 1);
36 | expect(calculateISO8601Duration(startDate, endDate)).toEqual("P1DT");
37 | });
38 |
39 | test("should create correct timestring for milliseconds, seconds, hours, minutes and days", () => {
40 | const startDate: Date = new Date();
41 | const endDate: Date = new Date(startDate);
42 | endDate.setSeconds(endDate.getSeconds() + 1);
43 | endDate.setMinutes(endDate.getMinutes() + 1);
44 | endDate.setHours(endDate.getHours() + 1);
45 | endDate.setDate(endDate.getDate() + 1);
46 | endDate.setMilliseconds(endDate.getMilliseconds() + 123);
47 | expect(calculateISO8601Duration(startDate, endDate)).toEqual(
48 | "P1DT1H1M1.123S"
49 | );
50 | });
51 |
52 | test("Should create correct timestring for 0 seconds", () => {
53 | const startDate: Date = new Date();
54 | const endDate: Date = new Date(startDate);
55 | expect(calculateISO8601Duration(startDate, endDate)).toEqual("PT0S");
56 | });
57 |
58 | test("Should create correct timestring for negative durations", () => {
59 | const startDate: Date = new Date();
60 | const endDate: Date = new Date(startDate);
61 | endDate.setHours(endDate.getHours() - 1);
62 | expect(calculateISO8601Duration(startDate, endDate)).toEqual("PT0S");
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/src/helpers/getSearchQueryParamsAsObject/getSearchQueryParamsAsObject.ts:
--------------------------------------------------------------------------------
1 | import { Actor, Agent, IdentifiedGroup } from "../../XAPI";
2 |
3 | function coerceActor(actor: Actor): Actor {
4 | const actorKeys = ["name", "mbox", "account"];
5 | actorKeys.forEach((actorKey) => {
6 | if (Array.isArray(actor[actorKey])) {
7 | switch (actorKey) {
8 | case "account": {
9 | actor = actor as Agent | IdentifiedGroup;
10 | actor[actorKey] = {
11 | ...(!!actor.account[0].accountServiceHomePage && {
12 | homePage: actor.account[0].accountServiceHomePage,
13 | }),
14 | ...(!!actor.account[0].accountName && {
15 | name: actor.account[0].accountName,
16 | }),
17 | };
18 | break;
19 | }
20 | default: {
21 | actor[actorKey] = actor[actorKey][0];
22 | }
23 | }
24 | }
25 | });
26 | return actor;
27 | }
28 |
29 | export function getSearchQueryParamsAsObject(str: string): {
30 | [key: string]: string | number | boolean | Actor;
31 | } {
32 | const obj: { [key: string]: string | number | boolean | Actor } = {};
33 | if (str.indexOf("?") === -1) return obj;
34 | let queryString = str.substring(str.indexOf("?"));
35 | queryString = queryString.split("#").shift();
36 | const usp = new URLSearchParams(queryString);
37 | usp.forEach((val, key) => {
38 | try {
39 | obj[key] = JSON.parse(val);
40 | } catch {
41 | obj[key] = val;
42 | }
43 | if (key === "actor" && typeof obj.actor === "object") {
44 | obj.actor = coerceActor(obj.actor);
45 | }
46 | });
47 | return obj;
48 | }
49 |
--------------------------------------------------------------------------------
/src/helpers/getTinCanLaunchData/TinCanLaunchData.ts:
--------------------------------------------------------------------------------
1 | import { Actor } from "../../XAPI";
2 |
3 | export interface TinCanLaunchData {
4 | activity_id?: string;
5 | actor?: Actor;
6 | auth?: string;
7 | content_endpoint?: string;
8 | content_token?: string;
9 | endpoint?: string;
10 | externalConfiguration?: string;
11 | externalRegistration?: string;
12 | grouping?: string;
13 | registration?: string;
14 | }
15 |
--------------------------------------------------------------------------------
/src/helpers/getTinCanLaunchData/getTinCanLaunchData.ts:
--------------------------------------------------------------------------------
1 | import { getSearchQueryParamsAsObject } from "../getSearchQueryParamsAsObject/getSearchQueryParamsAsObject";
2 | import { TinCanLaunchData } from "./TinCanLaunchData";
3 |
4 | export function getTinCanLaunchData(): TinCanLaunchData {
5 | if (typeof location === "undefined")
6 | throw new Error("Environment does not support location.search");
7 |
8 | const params: TinCanLaunchData = getSearchQueryParamsAsObject(
9 | location.search
10 | );
11 | return params;
12 | }
13 |
--------------------------------------------------------------------------------
/src/helpers/getTinCanLaunchData/getTinCanLaunchData.unit.test.ts:
--------------------------------------------------------------------------------
1 | import { getTinCanLaunchData } from "./getTinCanLaunchData";
2 | import { TinCanLaunchData } from "./TinCanLaunchData";
3 | import { testIf, isNode } from "../../../test/jestUtils";
4 |
5 | testIf(!isNode())("it returns launch data in browser environment", () => {
6 | const params: TinCanLaunchData = {
7 | auth: "abcdefgh",
8 | endpoint: "http://www.abcdefgh.com/",
9 | };
10 |
11 | const search =
12 | "?" +
13 | Object.keys(params)
14 | .map((key) => key + "=" + params[key])
15 | .join("&");
16 |
17 | Object.defineProperty(window, "location", {
18 | value: {
19 | search: search,
20 | },
21 | });
22 |
23 | const tinCanLaunchData = getTinCanLaunchData();
24 | expect(tinCanLaunchData).toEqual(params);
25 | });
26 |
27 | testIf(isNode())("it throws an error in node environment", () => {
28 | try {
29 | getTinCanLaunchData();
30 | } catch (error) {
31 | expect(error).toBeInstanceOf(Error);
32 | }
33 | });
34 |
--------------------------------------------------------------------------------
/src/helpers/getXAPILaunchData/XAPILaunchData.ts:
--------------------------------------------------------------------------------
1 | import { Actor } from "../../XAPI";
2 |
3 | export interface XAPILaunchData {
4 | endpoint: string;
5 | actor: Actor;
6 | }
7 |
--------------------------------------------------------------------------------
/src/helpers/getXAPILaunchData/XAPILaunchParameters.ts:
--------------------------------------------------------------------------------
1 | export interface XAPILaunchParameters {
2 | xAPILaunchKey?: string;
3 | xAPILaunchService?: string;
4 | encrypted?: string;
5 | }
6 |
--------------------------------------------------------------------------------
/src/helpers/getXAPILaunchData/getXAPILaunchData.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Adapter,
3 | AdapterPromise,
4 | resolveAdapterFunction,
5 | } from "../../adapters";
6 | import { getSearchQueryParamsAsObject } from "../getSearchQueryParamsAsObject/getSearchQueryParamsAsObject";
7 | import { XAPILaunchData } from "./XAPILaunchData";
8 | import { XAPILaunchParameters } from "./XAPILaunchParameters";
9 |
10 | export function getXAPILaunchData(params?: {
11 | adapter?: Adapter;
12 | }): AdapterPromise {
13 | if (typeof location === "undefined")
14 | return Promise.reject(
15 | new Error("Environment does not support location.search")
16 | );
17 |
18 | const launchParams: XAPILaunchParameters = getSearchQueryParamsAsObject(
19 | location.search
20 | );
21 |
22 | if (!launchParams.xAPILaunchService) {
23 | return Promise.reject(
24 | new Error("xAPILaunchService parameter not found in URL.")
25 | );
26 | }
27 |
28 | const launchURL: URL = new URL(launchParams.xAPILaunchService);
29 | launchURL.pathname += `launch/${launchParams.xAPILaunchKey}`;
30 | const adapter = resolveAdapterFunction(params.adapter);
31 | return adapter({
32 | method: "POST",
33 | url: launchURL.toString(),
34 | }).then((response) => {
35 | return response.data;
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/src/helpers/getXAPILaunchData/getXAPILaunchData.unit.test.ts:
--------------------------------------------------------------------------------
1 | import { getXAPILaunchData } from "./getXAPILaunchData";
2 | import { testIf, isNode } from "../../../test/jestUtils";
3 | import { XAPILaunchParameters } from "./XAPILaunchParameters";
4 |
5 | testIf(isNode())("return error in node environment", () => {
6 | return getXAPILaunchData().catch((error: Error) => {
7 | return expect(error.message).toBe(
8 | "Environment does not support location.search"
9 | );
10 | });
11 | });
12 |
13 | testIf(!isNode())(
14 | "return error if xAPILaunchService params not present in URL",
15 | () => {
16 | return getXAPILaunchData().catch((error: Error) => {
17 | return expect(error.message).toBe(
18 | "xAPILaunchService parameter not found in URL."
19 | );
20 | });
21 | }
22 | );
23 |
24 | testIf(!isNode())("can request launch data from url", async () => {
25 | const params: XAPILaunchParameters = {
26 | xAPILaunchService: "https://my.launch.service/",
27 | xAPILaunchKey: "test-launch-key",
28 | };
29 |
30 | const search =
31 | "?" +
32 | Object.keys(params)
33 | .map((key) => key + "=" + params[key])
34 | .join("&");
35 |
36 | Object.defineProperty(window, "location", {
37 | value: {
38 | search: search,
39 | },
40 | });
41 |
42 | global.adapterFn.mockClear();
43 | global.adapterFn.mockResolvedValueOnce({
44 | headers: {
45 | "content-type": "application/json",
46 | },
47 | });
48 | const launchURL: URL = new URL(params.xAPILaunchService);
49 | launchURL.pathname += `launch/${params.xAPILaunchKey}`;
50 | await getXAPILaunchData({ adapter: global.adapter });
51 | expect(global.adapterFn).toHaveBeenCalledWith(
52 | expect.objectContaining({
53 | method: "POST",
54 | url: launchURL.toString(),
55 | })
56 | );
57 | });
58 |
--------------------------------------------------------------------------------
/src/helpers/toBasicAuth/toBasicAuth.ts:
--------------------------------------------------------------------------------
1 | export function toBasicAuth(username: string, password: string): string {
2 | const credentials = `${username}:${password}`;
3 | if (typeof btoa === "function") {
4 | return `Basic ${btoa(credentials)}`;
5 | } else if (typeof Buffer !== "undefined") {
6 | return `Basic ${Buffer.from(credentials, "binary").toString("base64")}`;
7 | }
8 | throw new Error("Environment does not support base64 conversion.");
9 | }
10 |
--------------------------------------------------------------------------------
/src/helpers/toBasicAuth/toBasicAuth.unit.test.ts:
--------------------------------------------------------------------------------
1 | import { toBasicAuth } from "./toBasicAuth";
2 |
3 | const pairs = [
4 | ["tom", "1234"],
5 | ["BUG-mb'5#,:,fC83", "4jXGtgwY%\\'xm.k;"],
6 | ["Mr}VBHb^)zyc39`<", '?YrAhvP{}"s94:%%'],
7 | ];
8 |
9 | const encodedPairs = [
10 | "Basic dG9tOjEyMzQ=",
11 | "Basic QlVHLW1iJzUjLDosZkM4Mzo0alhHdGd3WSVcJ3htLms7",
12 | "Basic TXJ9VkJIYl4penljMzlgPDo/WXJBaHZQe30iczk0OiUl",
13 | ];
14 |
15 | test("converts username and password into Basic Auth header", () => {
16 | const helperFunction = (pair) => toBasicAuth(pair[0], pair[1]);
17 |
18 | const results = pairs.map((pair, index) => {
19 | return encodedPairs[index] === helperFunction(pair);
20 | });
21 |
22 | return expect(results).not.toContain(false);
23 | });
24 |
25 | test("still converts using Buffer if btoa is not supported", () => {
26 | // @ts-expect-error Overriding global/window btoa
27 | if (typeof btoa === "function") btoa = undefined;
28 |
29 | const helperFunction = (pair) => toBasicAuth(pair[0], pair[1]);
30 |
31 | const results = pairs.map((pair, index) => {
32 | return encodedPairs[index] === helperFunction(pair);
33 | });
34 |
35 | expect(results).not.toContain(false);
36 | expect(() => toBasicAuth("", "")).not.toThrow();
37 | });
38 |
39 | test("throws error if environment not supported", () => {
40 | // @ts-expect-error Overriding global/window btoa
41 | if (typeof btoa === "function") btoa = undefined;
42 | // @ts-expect-error Overriding global/window Buffer
43 | if (Buffer) Buffer = undefined;
44 | expect(() => toBasicAuth("", "")).toThrow(
45 | new Error("Environment does not support base64 conversion.")
46 | );
47 | });
48 |
--------------------------------------------------------------------------------
/src/internal/WithRequiredProperty.ts:
--------------------------------------------------------------------------------
1 | export type WithRequiredProperty = Type & {
2 | [Property in Key]-?: Type[Property];
3 | };
4 |
--------------------------------------------------------------------------------
/src/internal/formatEndpoint.ts:
--------------------------------------------------------------------------------
1 | export function formatEndpoint(endpoint: string): string {
2 | return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
3 | }
4 |
--------------------------------------------------------------------------------
/src/internal/formatEndpoint.unit.test.ts:
--------------------------------------------------------------------------------
1 | import { formatEndpoint } from "./formatEndpoint";
2 |
3 | test("returns endpoint with trailing slash intact", () => {
4 | const endpoint = "https://cloud.scorm.com/lrs/xxxxxxxxxx/sandbox/";
5 | const formattedEndpoint = formatEndpoint(endpoint);
6 | return expect(formattedEndpoint).toEqual(endpoint);
7 | });
8 |
9 | test("appends trailing slash to endpoint without trailing slash", () => {
10 | const endpoint = "https://cloud.scorm.com/lrs/xxxxxxxxxx/sandbox";
11 | const formattedEndpoint = formatEndpoint(endpoint);
12 | return expect(formattedEndpoint).toEqual(endpoint + "/");
13 | });
14 |
--------------------------------------------------------------------------------
/src/internal/multiPart.unit.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | testAttachmentArrayBuffer,
3 | testAttachmentContent,
4 | testMultiPartData,
5 | testStatementWithEmbeddedAttachments,
6 | } from "../../test/constants";
7 | import { testIf, isNode } from "../../test/jestUtils";
8 | import { createMultiPart, parseMultiPart } from "./multiPart";
9 |
10 | testIf(!isNode())("creates a multi-part payload", () => {
11 | const multiPart = createMultiPart(testStatementWithEmbeddedAttachments, [
12 | testAttachmentArrayBuffer,
13 | ]);
14 | expect(multiPart.header["Content-Type"]).toContain(
15 | "multipart/mixed; boundary="
16 | );
17 | expect(multiPart.blob).toBeInstanceOf(Blob);
18 | });
19 |
20 | testIf(!isNode())(
21 | "creates a multi-part payload with multiple statements",
22 | () => {
23 | const multiPart = createMultiPart(
24 | [
25 | testStatementWithEmbeddedAttachments,
26 | testStatementWithEmbeddedAttachments,
27 | ],
28 | [testAttachmentArrayBuffer, testAttachmentArrayBuffer]
29 | );
30 | expect(multiPart.header["Content-Type"]).toContain(
31 | "multipart/mixed; boundary="
32 | );
33 | expect(multiPart.blob).toBeInstanceOf(Blob);
34 | }
35 | );
36 |
37 | test("parses a multi-part payload", () => {
38 | const payload = testMultiPartData;
39 |
40 | const parsed = parseMultiPart(payload);
41 | expect(parsed[0]).toEqual(testStatementWithEmbeddedAttachments);
42 | expect(parsed[1]).toEqual(testAttachmentContent);
43 | });
44 |
--------------------------------------------------------------------------------
/src/resources/GetParamsBase.ts:
--------------------------------------------------------------------------------
1 | export interface GetParamsBase {
2 | /**
3 | * Appends a timestamp onto the URL to trigger cache busting, encouraging a new response to be generated by the LRS.
4 | */
5 | useCacheBuster?: true;
6 | }
7 |
--------------------------------------------------------------------------------
/src/resources/about/About.ts:
--------------------------------------------------------------------------------
1 | import { Extensions } from "../../XAPI";
2 |
3 | export interface About {
4 | /**
5 | * xAPI versions this LRS supports
6 | */
7 | version: string[];
8 | /**
9 | * Extensions this LRS supports
10 | */
11 | extensions?: Extensions;
12 | }
13 |
--------------------------------------------------------------------------------
/src/resources/about/getAbout/GetAboutParams.ts:
--------------------------------------------------------------------------------
1 | import { GetParamsBase } from "../../GetParamsBase";
2 |
3 | export type GetAboutParams = GetParamsBase;
4 |
--------------------------------------------------------------------------------
/src/resources/about/getAbout/getAbout.int.test.ts:
--------------------------------------------------------------------------------
1 | import { forEachLRS } from "../../../../test/getCredentials";
2 |
3 | forEachLRS((xapi) => {
4 | describe("about resource", () => {
5 | test("can get about", () => {
6 | return xapi.getAbout().then((result) => {
7 | return expect(result.data).toEqual(
8 | expect.objectContaining({
9 | version: expect.any(Array),
10 | })
11 | );
12 | });
13 | });
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/resources/about/getAbout/getAbout.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../adapters";
2 | import { Resources } from "../../../constants";
3 | import XAPI from "../../../XAPI";
4 | import { About } from "../About";
5 | import { GetAboutParams } from "./GetAboutParams";
6 |
7 | export function getAbout(
8 | this: XAPI,
9 | params?: GetAboutParams
10 | ): AdapterPromise {
11 | return this.requestResource({
12 | resource: Resources.ABOUT,
13 | requestOptions: {
14 | useCacheBuster: params?.useCacheBuster,
15 | },
16 | });
17 | }
18 |
--------------------------------------------------------------------------------
/src/resources/about/getAbout/getAbout.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../XAPI";
2 | import { testEndpoint } from "../../../../test/constants";
3 | import { Resources } from "../../../constants";
4 |
5 | describe("about resource", () => {
6 | beforeEach(() => {
7 | global.adapterFn.mockClear();
8 | global.adapterFn.mockResolvedValueOnce({
9 | headers: {
10 | "content-type": "application/json",
11 | },
12 | });
13 | });
14 |
15 | test("can get about", async () => {
16 | const xapi = new XAPI({
17 | endpoint: testEndpoint,
18 | adapter: global.adapter,
19 | });
20 | await xapi.getAbout();
21 | expect(global.adapterFn).toHaveBeenCalledWith(
22 | expect.objectContaining({
23 | method: "GET",
24 | url: `${testEndpoint}${Resources.ABOUT}`,
25 | })
26 | );
27 | });
28 |
29 | test("can get about with cache buster", () => {
30 | const xapi = new XAPI({
31 | endpoint: testEndpoint,
32 | adapter: global.adapter,
33 | });
34 | xapi.getAbout({
35 | useCacheBuster: true,
36 | });
37 | expect(global.adapterFn).toHaveBeenCalledWith(
38 | expect.objectContaining({
39 | url: expect.stringContaining(
40 | `${testEndpoint}${Resources.ABOUT}?cachebuster=`
41 | ),
42 | })
43 | );
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/src/resources/activities/Activity.ts:
--------------------------------------------------------------------------------
1 | import { ActivityDefinition } from "./ActivityDefinition";
2 |
3 | export interface Activity {
4 | objectType?: "Activity";
5 | id: string;
6 | definition?: ActivityDefinition;
7 | }
8 |
--------------------------------------------------------------------------------
/src/resources/activities/ActivityDefinition.ts:
--------------------------------------------------------------------------------
1 | import { Extensions, LanguageMap } from "../../XAPI";
2 |
3 | export interface ActivityDefinition {
4 | type?: string;
5 | name?: LanguageMap;
6 | description?: LanguageMap;
7 | moreInfo?: string;
8 | extensions?: Extensions;
9 | }
10 |
--------------------------------------------------------------------------------
/src/resources/activities/getActivity/GetActivityParams.ts:
--------------------------------------------------------------------------------
1 | import { GetParamsBase } from "../../GetParamsBase";
2 |
3 | export interface GetActivityParams extends GetParamsBase {
4 | activityId: string;
5 | }
6 |
--------------------------------------------------------------------------------
/src/resources/activities/getActivity/getActivity.int.test.ts:
--------------------------------------------------------------------------------
1 | import { testActivity } from "../../../../test/constants";
2 | import { forEachLRS } from "../../../../test/getCredentials";
3 |
4 | forEachLRS((xapi) => {
5 | describe("activities resource", () => {
6 | test("can get activity", () => {
7 | return xapi
8 | .getActivity({
9 | activityId: testActivity.id,
10 | })
11 | .then((result) => {
12 | return expect(result.data).toMatchObject(testActivity);
13 | });
14 | });
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/resources/activities/getActivity/getActivity.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../adapters";
2 | import { Resources } from "../../../constants";
3 | import XAPI from "../../../XAPI";
4 | import { Activity } from "../Activity";
5 | import { GetActivityParams } from "./GetActivityParams";
6 |
7 | export function getActivity(
8 | this: XAPI,
9 | params: GetActivityParams
10 | ): AdapterPromise {
11 | return this.requestResource({
12 | resource: Resources.ACTIVITIES,
13 | queryParams: {
14 | activityId: params.activityId,
15 | },
16 | requestOptions: {
17 | useCacheBuster: params.useCacheBuster,
18 | },
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/src/resources/activities/getActivity/getActivity.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../XAPI";
2 | import { testActivity, testEndpoint } from "../../../../test/constants";
3 | import { Resources } from "../../../constants";
4 |
5 | describe("activities resource", () => {
6 | beforeEach(() => {
7 | global.adapterFn.mockClear();
8 | global.adapterFn.mockResolvedValueOnce({
9 | headers: {
10 | "content-type": "application/json",
11 | },
12 | });
13 | });
14 |
15 | test("can get activity", async () => {
16 | const xapi = new XAPI({
17 | endpoint: testEndpoint,
18 | adapter: global.adapter,
19 | });
20 | await xapi.getActivity({
21 | activityId: testActivity.id,
22 | });
23 | expect(global.adapterFn).toHaveBeenCalledWith(
24 | expect.objectContaining({
25 | method: "GET",
26 | url: `${testEndpoint}${
27 | Resources.ACTIVITIES
28 | }?activityId=${encodeURIComponent(testActivity.id)}`,
29 | })
30 | );
31 | });
32 |
33 | test("can get activity with cache buster", async () => {
34 | const xapi = new XAPI({
35 | endpoint: testEndpoint,
36 | adapter: global.adapter,
37 | });
38 | await xapi.getActivity({
39 | activityId: testActivity.id,
40 | useCacheBuster: true,
41 | });
42 | expect(global.adapterFn).toHaveBeenCalledWith(
43 | expect.objectContaining({
44 | method: "GET",
45 | url: expect.stringContaining(
46 | `${testEndpoint}${
47 | Resources.ACTIVITIES
48 | }?activityId=${encodeURIComponent(testActivity.id)}&cachebuster=`
49 | ),
50 | })
51 | );
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/src/resources/agents/Person.ts:
--------------------------------------------------------------------------------
1 | import { Account } from "../statement/Account";
2 |
3 | export interface Person {
4 | objectType: "Person";
5 | name?: string[];
6 | mbox?: string[];
7 | mbox_sha1sum?: string[];
8 | openid?: string[];
9 | account?: Account[];
10 | }
11 |
--------------------------------------------------------------------------------
/src/resources/agents/getAgent/GetAgentParams.ts:
--------------------------------------------------------------------------------
1 | import { Agent } from "../../../XAPI";
2 | import { GetParamsBase } from "../../GetParamsBase";
3 |
4 | export interface GetAgentParams extends GetParamsBase {
5 | agent: Agent;
6 | }
7 |
--------------------------------------------------------------------------------
/src/resources/agents/getAgent/getAgent.int.test.ts:
--------------------------------------------------------------------------------
1 | import { testAgent } from "../../../../test/constants";
2 | import { forEachLRS } from "../../../../test/getCredentials";
3 |
4 | forEachLRS((xapi) => {
5 | describe("agents resource", () => {
6 | describe("get agent", () => {
7 | test("can get person by agent", () => {
8 | return xapi
9 | .getAgent({
10 | agent: testAgent,
11 | })
12 | .then((result) => {
13 | return expect(result.data).toBeDefined();
14 | });
15 | });
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/resources/agents/getAgent/getAgent.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../adapters";
2 | import { Resources } from "../../../constants";
3 | import XAPI from "../../../XAPI";
4 | import { Person } from "../Person";
5 | import { GetAgentParams } from "./GetAgentParams";
6 |
7 | export function getAgent(
8 | this: XAPI,
9 | params: GetAgentParams
10 | ): AdapterPromise {
11 | return this.requestResource({
12 | resource: Resources.AGENTS,
13 | queryParams: {
14 | agent: params.agent,
15 | },
16 | requestOptions: {
17 | useCacheBuster: params.useCacheBuster,
18 | },
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/src/resources/agents/getAgent/getAgent.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../XAPI";
2 | import { testAgent, testEndpoint } from "../../../../test/constants";
3 | import { Resources } from "../../../constants";
4 |
5 | describe("agent resource", () => {
6 | describe("get agent", () => {
7 | beforeEach(() => {
8 | global.adapterFn.mockClear();
9 | global.adapterFn.mockResolvedValueOnce({
10 | headers: {
11 | "content-type": "application/json",
12 | },
13 | });
14 | });
15 |
16 | test("can get person by agent", async () => {
17 | const xapi = new XAPI({
18 | endpoint: testEndpoint,
19 | adapter: global.adapter,
20 | });
21 | await xapi.getAgent({
22 | agent: testAgent,
23 | });
24 | expect(global.adapterFn).toHaveBeenCalledWith(
25 | expect.objectContaining({
26 | method: "GET",
27 | url: `${testEndpoint}${Resources.AGENTS}?agent=${encodeURIComponent(
28 | JSON.stringify(testAgent)
29 | )}`,
30 | })
31 | );
32 | });
33 |
34 | test("can get person by agent with cache buster", async () => {
35 | const xapi = new XAPI({
36 | endpoint: testEndpoint,
37 | adapter: global.adapter,
38 | });
39 | await xapi.getAgent({
40 | agent: testAgent,
41 | useCacheBuster: true,
42 | });
43 | expect(global.adapterFn).toHaveBeenCalledWith(
44 | expect.objectContaining({
45 | method: "GET",
46 | url: expect.stringContaining(
47 | `${testEndpoint}${Resources.AGENTS}?agent=${encodeURIComponent(
48 | JSON.stringify(testAgent)
49 | )}&cachebuster=`
50 | ),
51 | })
52 | );
53 | });
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/src/resources/document/Document.ts:
--------------------------------------------------------------------------------
1 | export type DocumentJson = { [key: string]: unknown };
2 |
3 | export type DocumentUnknown = unknown;
4 |
5 | export type Document = DocumentJson | DocumentUnknown;
6 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/createActivityProfile/CreateActivityProfileParams.ts:
--------------------------------------------------------------------------------
1 | import { DocumentJson } from "../../Document";
2 |
3 | export interface CreateActivityProfileParams {
4 | activityId: string;
5 | profileId: string;
6 | profile: DocumentJson;
7 | etag?: string;
8 | matchHeader?: "If-Match" | "If-None-Match";
9 | }
10 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/createActivityProfile/createActivityProfile.int.test.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 | import {
3 | testActivity,
4 | testProfileId,
5 | testDocument,
6 | } from "../../../../../test/constants";
7 | import { forEachLRS } from "../../../../../test/getCredentials";
8 |
9 | forEachLRS((xapi) => {
10 | describe("activity profile resource", () => {
11 | test("can create activity profile", () => {
12 | return xapi
13 | .createActivityProfile({
14 | activityId: testActivity.id,
15 | profileId: testProfileId,
16 | profile: testDocument,
17 | })
18 | .then((result) => {
19 | return expect(result.data).toBeDefined();
20 | });
21 | });
22 |
23 | test("can add to an activity profile using an etag", () => {
24 | const profileId = crypto.randomUUID();
25 | return xapi
26 | .createActivityProfile({
27 | activityId: testActivity.id,
28 | profileId: profileId,
29 | profile: {
30 | x: "foo",
31 | y: "bar",
32 | },
33 | })
34 | .then(() => {
35 | return xapi.getActivityProfile({
36 | activityId: testActivity.id,
37 | profileId: profileId,
38 | });
39 | })
40 | .then((response) => {
41 | return xapi.createActivityProfile({
42 | activityId: testActivity.id,
43 | profileId: profileId,
44 | profile: {
45 | x: "bash",
46 | z: "faz",
47 | },
48 | etag: response.headers.etag,
49 | matchHeader: "If-Match",
50 | });
51 | })
52 | .then(() => {
53 | return xapi.getActivityProfile({
54 | activityId: testActivity.id,
55 | profileId: profileId,
56 | });
57 | })
58 | .then((response) => {
59 | return expect(response.data).toEqual({
60 | x: "bash",
61 | y: "bar",
62 | z: "faz",
63 | });
64 | });
65 | });
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/createActivityProfile/createActivityProfile.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { CreateActivityProfileParams } from "./CreateActivityProfileParams";
5 |
6 | export function createActivityProfile(
7 | this: XAPI,
8 | params: CreateActivityProfileParams
9 | ): AdapterPromise {
10 | const headers = {};
11 | if (params.etag) headers[params.matchHeader] = params.etag;
12 | return this.requestResource({
13 | resource: Resources.ACTIVITY_PROFILE,
14 | queryParams: {
15 | activityId: params.activityId,
16 | profileId: params.profileId,
17 | },
18 | requestConfig: {
19 | method: "POST",
20 | data: params.profile,
21 | headers: headers,
22 | },
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/createActivityProfile/createActivityProfile.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../../XAPI";
2 | import {
3 | testActivity,
4 | testDocument,
5 | testEndpoint,
6 | testProfileId,
7 | } from "../../../../../test/constants";
8 | import { Resources } from "../../../../constants";
9 |
10 | describe("activity profile resource", () => {
11 | beforeEach(() => {
12 | global.adapterFn.mockClear();
13 | global.adapterFn.mockResolvedValueOnce({
14 | headers: {
15 | "content-type": "application/json",
16 | },
17 | });
18 | });
19 |
20 | test("can create activity profile", async () => {
21 | const xapi = new XAPI({
22 | endpoint: testEndpoint,
23 | adapter: global.adapter,
24 | });
25 | await xapi.createActivityProfile({
26 | activityId: testActivity.id,
27 | profileId: testProfileId,
28 | profile: testDocument,
29 | });
30 | expect(global.adapterFn).toHaveBeenCalledWith(
31 | expect.objectContaining({
32 | method: "POST",
33 | url: `${testEndpoint}${
34 | Resources.ACTIVITY_PROFILE
35 | }?activityId=${encodeURIComponent(
36 | testActivity.id
37 | )}&profileId=${encodeURIComponent(testProfileId)}`,
38 | data: testDocument,
39 | })
40 | );
41 | });
42 |
43 | test("can create activity profile with etag and match header", async () => {
44 | const xapi = new XAPI({
45 | endpoint: testEndpoint,
46 | adapter: global.adapter,
47 | });
48 | const testEtag = "my-etag";
49 | const testMatchHeader = "If-Match";
50 | await xapi.createActivityProfile({
51 | activityId: testActivity.id,
52 | profileId: testProfileId,
53 | profile: testDocument,
54 | etag: testEtag,
55 | matchHeader: testMatchHeader,
56 | });
57 | expect(global.adapterFn).toHaveBeenCalledWith(
58 | expect.objectContaining({
59 | method: "POST",
60 | headers: expect.objectContaining({
61 | [testMatchHeader]: testEtag,
62 | }),
63 | url: `${testEndpoint}${
64 | Resources.ACTIVITY_PROFILE
65 | }?activityId=${encodeURIComponent(
66 | testActivity.id
67 | )}&profileId=${encodeURIComponent(testProfileId)}`,
68 | data: testDocument,
69 | })
70 | );
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/deleteActivityProfile/DeleteActivityProfileParams.ts:
--------------------------------------------------------------------------------
1 | export interface DeleteActivityProfileParams {
2 | activityId: string;
3 | profileId: string;
4 | etag?: string;
5 | }
6 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/deleteActivityProfile/deleteActivityProfile.int.test.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 | import {
3 | testActivity,
4 | testDocument,
5 | testProfileId,
6 | } from "../../../../../test/constants";
7 | import { forEachLRS } from "../../../../../test/getCredentials";
8 |
9 | forEachLRS((xapi) => {
10 | describe("activity profile resource", () => {
11 | test("can delete an activity profile", () => {
12 | return xapi
13 | .deleteActivityProfile({
14 | activityId: testActivity.id,
15 | profileId: testProfileId,
16 | })
17 | .then((result) => {
18 | return expect(result.data).toBeDefined();
19 | });
20 | });
21 |
22 | test("can delete an activity profile with an etag", () => {
23 | const profileId = crypto.randomUUID();
24 | return xapi
25 | .createActivityProfile({
26 | activityId: testActivity.id,
27 | profileId: profileId,
28 | profile: testDocument,
29 | })
30 | .then(() => {
31 | return xapi.getActivityProfile({
32 | activityId: testActivity.id,
33 | profileId: profileId,
34 | });
35 | })
36 | .then((response) => {
37 | return xapi.deleteActivityProfile({
38 | activityId: testActivity.id,
39 | profileId: profileId,
40 | etag: response.headers.etag,
41 | });
42 | })
43 | .then((response) => {
44 | return expect(response.data).toBeDefined();
45 | });
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/deleteActivityProfile/deleteActivityProfile.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { DeleteActivityProfileParams } from "./DeleteActivityProfileParams";
5 |
6 | export function deleteActivityProfile(
7 | this: XAPI,
8 | params: DeleteActivityProfileParams
9 | ): AdapterPromise {
10 | const headers = {};
11 | if (params.etag) headers["If-Match"] = params.etag;
12 | return this.requestResource({
13 | resource: Resources.ACTIVITY_PROFILE,
14 | queryParams: {
15 | activityId: params.activityId,
16 | profileId: params.profileId,
17 | },
18 | requestConfig: {
19 | method: "DELETE",
20 | headers: headers,
21 | },
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/deleteActivityProfile/deleteActivityProfile.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../../XAPI";
2 | import {
3 | testActivity,
4 | testEndpoint,
5 | testProfileId,
6 | } from "../../../../../test/constants";
7 | import { Resources } from "../../../../constants";
8 |
9 | describe("activity profile resource", () => {
10 | beforeEach(() => {
11 | global.adapterFn.mockClear();
12 | global.adapterFn.mockResolvedValueOnce({
13 | headers: {
14 | "content-type": "application/json",
15 | },
16 | });
17 | });
18 |
19 | test("can delete an activity profile", async () => {
20 | const xapi = new XAPI({
21 | endpoint: testEndpoint,
22 | adapter: global.adapter,
23 | });
24 | await xapi.deleteActivityProfile({
25 | activityId: testActivity.id,
26 | profileId: testProfileId,
27 | });
28 | expect(global.adapterFn).toHaveBeenCalledWith(
29 | expect.objectContaining({
30 | method: "DELETE",
31 | url: `${testEndpoint}${
32 | Resources.ACTIVITY_PROFILE
33 | }?activityId=${encodeURIComponent(
34 | testActivity.id
35 | )}&profileId=${encodeURIComponent(testProfileId)}`,
36 | })
37 | );
38 | });
39 |
40 | test("can delete an activity profile with etag and match header", async () => {
41 | const xapi = new XAPI({
42 | endpoint: testEndpoint,
43 | adapter: global.adapter,
44 | });
45 | const testEtag = "my-etag";
46 | await xapi.deleteActivityProfile({
47 | activityId: testActivity.id,
48 | profileId: testProfileId,
49 | etag: testEtag,
50 | });
51 | expect(global.adapterFn).toHaveBeenCalledWith(
52 | expect.objectContaining({
53 | method: "DELETE",
54 | url: `${testEndpoint}${
55 | Resources.ACTIVITY_PROFILE
56 | }?activityId=${encodeURIComponent(
57 | testActivity.id
58 | )}&profileId=${encodeURIComponent(testProfileId)}`,
59 | headers: expect.objectContaining({
60 | ["If-Match"]: testEtag,
61 | }),
62 | })
63 | );
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/getActivityProfile/GetActivityProfileParams.ts:
--------------------------------------------------------------------------------
1 | import { GetParamsBase } from "../../../GetParamsBase";
2 |
3 | export interface GetActivityProfileParams extends GetParamsBase {
4 | activityId: string;
5 | profileId: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/getActivityProfile/getActivityProfile.int.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | testActivity,
3 | testProfileId,
4 | testDocument,
5 | } from "../../../../../test/constants";
6 | import { forEachLRS } from "../../../../../test/getCredentials";
7 |
8 | forEachLRS((xapi) => {
9 | describe("activity profile resource", () => {
10 | test("can get an activity profile", () => {
11 | return xapi
12 | .createActivityProfile({
13 | activityId: testActivity.id,
14 | profileId: testProfileId,
15 | profile: testDocument,
16 | })
17 | .then(() => {
18 | return xapi.getActivityProfile({
19 | activityId: testActivity.id,
20 | profileId: testProfileId,
21 | });
22 | })
23 | .then((result) => {
24 | return expect(result.data).toMatchObject(testDocument);
25 | });
26 | });
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/getActivityProfile/getActivityProfile.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { GetActivityProfileParams } from "./GetActivityProfileParams";
5 | import { Document } from "../../Document";
6 |
7 | export function getActivityProfile(
8 | this: XAPI,
9 | params: GetActivityProfileParams
10 | ): AdapterPromise {
11 | return this.requestResource({
12 | resource: Resources.ACTIVITY_PROFILE,
13 | queryParams: {
14 | activityId: params.activityId,
15 | profileId: params.profileId,
16 | },
17 | requestOptions: {
18 | useCacheBuster: params.useCacheBuster,
19 | },
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/getActivityProfile/getActivityProfile.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../../XAPI";
2 | import {
3 | testActivity,
4 | testEndpoint,
5 | testProfileId,
6 | } from "../../../../../test/constants";
7 | import { Resources } from "../../../../constants";
8 |
9 | describe("activity profile resource", () => {
10 | beforeEach(() => {
11 | global.adapterFn.mockClear();
12 | global.adapterFn.mockResolvedValueOnce({
13 | headers: {
14 | "content-type": "application/json",
15 | },
16 | });
17 | });
18 |
19 | test("can get an activity profile", async () => {
20 | const xapi = new XAPI({
21 | endpoint: testEndpoint,
22 | adapter: global.adapter,
23 | });
24 | await xapi.getActivityProfile({
25 | activityId: testActivity.id,
26 | profileId: testProfileId,
27 | });
28 | expect(global.adapterFn).toHaveBeenCalledWith(
29 | expect.objectContaining({
30 | method: "GET",
31 | url: `${testEndpoint}${
32 | Resources.ACTIVITY_PROFILE
33 | }?activityId=${encodeURIComponent(
34 | testActivity.id
35 | )}&profileId=${encodeURIComponent(testProfileId)}`,
36 | })
37 | );
38 | });
39 |
40 | test("can get an activity profile with cache buster", async () => {
41 | const xapi = new XAPI({
42 | endpoint: testEndpoint,
43 | adapter: global.adapter,
44 | });
45 | await xapi.getActivityProfile({
46 | activityId: testActivity.id,
47 | profileId: testProfileId,
48 | useCacheBuster: true,
49 | });
50 | expect(global.adapterFn).toHaveBeenCalledWith(
51 | expect.objectContaining({
52 | method: "GET",
53 | url: expect.stringContaining(
54 | `${testEndpoint}${
55 | Resources.ACTIVITY_PROFILE
56 | }?activityId=${encodeURIComponent(
57 | testActivity.id
58 | )}&profileId=${encodeURIComponent(testProfileId)}&cachebuster=`
59 | ),
60 | })
61 | );
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/getActivityProfiles/GetActivityProfilesParams.ts:
--------------------------------------------------------------------------------
1 | import { Timestamp } from "../../../../XAPI";
2 | import { GetParamsBase } from "../../../GetParamsBase";
3 |
4 | export interface GetActivityProfilesParams extends GetParamsBase {
5 | activityId: string;
6 | since?: Timestamp;
7 | }
8 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/getActivityProfiles/getActivityProfiles.int.test.ts:
--------------------------------------------------------------------------------
1 | import { testActivity } from "../../../../../test/constants";
2 | import { forEachLRS } from "../../../../../test/getCredentials";
3 |
4 | forEachLRS((xapi) => {
5 | describe("activity profile resource", () => {
6 | test("can get all activity profiles", () => {
7 | return xapi
8 | .getActivityProfiles({
9 | activityId: testActivity.id,
10 | })
11 | .then((result) => {
12 | return expect(result.data).toEqual(expect.any(Array));
13 | });
14 | });
15 |
16 | test("can get all activity profiles since a certain date", () => {
17 | const since = new Date();
18 | since.setDate(since.getDate() - 1); // yesterday
19 | return xapi
20 | .getActivityProfiles({
21 | activityId: testActivity.id,
22 | since: since.toISOString(),
23 | })
24 | .then((result) => {
25 | return expect(result.data).toEqual(expect.any(Array));
26 | });
27 | });
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/getActivityProfiles/getActivityProfiles.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { GetActivityProfilesParams } from "./GetActivityProfilesParams";
5 |
6 | export function getActivityProfiles(
7 | this: XAPI,
8 | params: GetActivityProfilesParams
9 | ): AdapterPromise {
10 | return this.requestResource({
11 | resource: Resources.ACTIVITY_PROFILE,
12 | queryParams: {
13 | activityId: params.activityId,
14 | ...(!!params.since && { since: params.since }),
15 | },
16 | requestOptions: { useCacheBuster: params.useCacheBuster },
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/getActivityProfiles/getActivityProfiles.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../../XAPI";
2 | import { testActivity, testEndpoint } from "../../../../../test/constants";
3 | import { Resources } from "../../../../constants";
4 |
5 | describe("activity profile resource", () => {
6 | beforeEach(() => {
7 | global.adapterFn.mockClear();
8 | global.adapterFn.mockResolvedValueOnce({
9 | headers: {
10 | "content-type": "application/json",
11 | },
12 | });
13 | });
14 |
15 | test("can get all activity profiles", async () => {
16 | const xapi = new XAPI({
17 | endpoint: testEndpoint,
18 | adapter: global.adapter,
19 | });
20 | await xapi.getActivityProfiles({
21 | activityId: testActivity.id,
22 | });
23 | expect(global.adapterFn).toHaveBeenCalledWith(
24 | expect.objectContaining({
25 | method: "GET",
26 | url: `${testEndpoint}${
27 | Resources.ACTIVITY_PROFILE
28 | }?activityId=${encodeURIComponent(testActivity.id)}`,
29 | })
30 | );
31 | });
32 |
33 | test("can get all activity profiles since a certain date", async () => {
34 | const xapi = new XAPI({
35 | endpoint: testEndpoint,
36 | adapter: global.adapter,
37 | });
38 | const since = new Date();
39 | since.setDate(since.getDate() - 1); // yesterday
40 | await xapi.getActivityProfiles({
41 | activityId: testActivity.id,
42 | since: since.toISOString(),
43 | });
44 | expect(global.adapterFn).toHaveBeenCalledWith(
45 | expect.objectContaining({
46 | method: "GET",
47 | url: `${testEndpoint}${
48 | Resources.ACTIVITY_PROFILE
49 | }?activityId=${encodeURIComponent(
50 | testActivity.id
51 | )}&since=${encodeURIComponent(since.toISOString())}`,
52 | })
53 | );
54 | });
55 |
56 | test("can get all activity profiles with cache buster", async () => {
57 | const xapi = new XAPI({
58 | endpoint: testEndpoint,
59 | adapter: global.adapter,
60 | });
61 | await xapi.getActivityProfiles({
62 | activityId: testActivity.id,
63 | useCacheBuster: true,
64 | });
65 | expect(global.adapterFn).toHaveBeenCalledWith(
66 | expect.objectContaining({
67 | method: "GET",
68 | url: expect.stringContaining(
69 | `${testEndpoint}${
70 | Resources.ACTIVITY_PROFILE
71 | }?activityId=${encodeURIComponent(testActivity.id)}&cachebuster=`
72 | ),
73 | })
74 | );
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/setActivityProfile/SetActivityProfileParams.ts:
--------------------------------------------------------------------------------
1 | import { Document } from "../../Document";
2 |
3 | export interface SetActivityProfileParams {
4 | activityId: string;
5 | profileId: string;
6 | profile: Document;
7 | etag: string;
8 | matchHeader: "If-Match" | "If-None-Match";
9 | contentType?: string;
10 | }
11 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/setActivityProfile/setActivityProfile.int.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | testActivity,
3 | testProfileId,
4 | testDocument,
5 | testProfileIdTextPlain,
6 | } from "../../../../../test/constants";
7 | import { forEachLRS } from "../../../../../test/getCredentials";
8 |
9 | forEachLRS((xapi) => {
10 | describe("activity profile resource", () => {
11 | test("can set activity profile", () => {
12 | return xapi
13 | .createActivityProfile({
14 | activityId: testActivity.id,
15 | profileId: testProfileId,
16 | profile: {
17 | foo: "bar",
18 | },
19 | })
20 | .then(() => {
21 | return xapi.getActivityProfile({
22 | activityId: testActivity.id,
23 | profileId: testProfileId,
24 | });
25 | })
26 | .then((result) => {
27 | return xapi
28 | .setActivityProfile({
29 | activityId: testActivity.id,
30 | profileId: testProfileId,
31 | profile: testDocument,
32 | etag: result.headers.etag,
33 | matchHeader: "If-Match",
34 | })
35 | .then(() => {
36 | return xapi.getActivityProfile({
37 | activityId: testActivity.id,
38 | profileId: testProfileId,
39 | });
40 | })
41 | .then((result) => {
42 | return expect(result.data).toEqual(testDocument);
43 | });
44 | });
45 | });
46 |
47 | test("can set activity profile with text/plain content type", () => {
48 | return xapi
49 | .setActivityProfile({
50 | activityId: testActivity.id,
51 | profileId: testProfileIdTextPlain,
52 | profile: testDocument.test,
53 | etag: "*",
54 | matchHeader: "If-Match",
55 | contentType: "text/plain",
56 | })
57 | .then(() => {
58 | return xapi.getActivityProfile({
59 | activityId: testActivity.id,
60 | profileId: testProfileIdTextPlain,
61 | });
62 | })
63 | .then((result) => {
64 | return expect(result.data).toEqual(testDocument.test);
65 | });
66 | });
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/setActivityProfile/setActivityProfile.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { SetActivityProfileParams } from "./SetActivityProfileParams";
5 |
6 | export function setActivityProfile(
7 | this: XAPI,
8 | params: SetActivityProfileParams
9 | ): AdapterPromise {
10 | const headers = {};
11 | headers[params.matchHeader] = params.etag;
12 | if (params.contentType) headers["Content-Type"] = params.contentType;
13 | return this.requestResource({
14 | resource: Resources.ACTIVITY_PROFILE,
15 | queryParams: {
16 | activityId: params.activityId,
17 | profileId: params.profileId,
18 | },
19 | requestConfig: {
20 | method: "PUT",
21 | data: params.profile,
22 | headers: headers,
23 | },
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/src/resources/document/activityProfile/setActivityProfile/setActivityProfile.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../../XAPI";
2 | import {
3 | testActivity,
4 | testDocument,
5 | testEndpoint,
6 | testProfileId,
7 | testProfileIdTextPlain,
8 | } from "../../../../../test/constants";
9 | import { Resources } from "../../../../constants";
10 |
11 | describe("activity profile resource", () => {
12 | beforeEach(() => {
13 | global.adapterFn.mockClear();
14 | global.adapterFn.mockResolvedValueOnce({
15 | headers: {
16 | "content-type": "application/json",
17 | },
18 | });
19 | });
20 |
21 | test("can set activity profile", async () => {
22 | const xapi = new XAPI({
23 | endpoint: testEndpoint,
24 | adapter: global.adapter,
25 | });
26 | const testEtag = "my-etag";
27 | const testMatchHeader = "If-Match";
28 | await xapi.setActivityProfile({
29 | activityId: testActivity.id,
30 | profileId: testProfileId,
31 | profile: testDocument,
32 | etag: testEtag,
33 | matchHeader: testMatchHeader,
34 | });
35 | expect(global.adapterFn).toHaveBeenCalledWith(
36 | expect.objectContaining({
37 | method: "PUT",
38 | headers: expect.objectContaining({
39 | [testMatchHeader]: testEtag,
40 | }),
41 | url: `${testEndpoint}${
42 | Resources.ACTIVITY_PROFILE
43 | }?activityId=${encodeURIComponent(
44 | testActivity.id
45 | )}&profileId=${encodeURIComponent(testProfileId)}`,
46 | data: testDocument,
47 | })
48 | );
49 | });
50 |
51 | test("can set activity profile with text/plain content type", async () => {
52 | const xapi = new XAPI({
53 | endpoint: testEndpoint,
54 | adapter: global.adapter,
55 | });
56 | const testEtag = "my-etag";
57 | const testMatchHeader = "If-Match";
58 | const plainTextContentType = "text/plain";
59 | await xapi.setActivityProfile({
60 | activityId: testActivity.id,
61 | profileId: testProfileIdTextPlain,
62 | profile: testDocument.test,
63 | etag: testEtag,
64 | matchHeader: testMatchHeader,
65 | contentType: plainTextContentType,
66 | });
67 | expect(global.adapterFn).toHaveBeenCalledWith(
68 | expect.objectContaining({
69 | method: "PUT",
70 | headers: expect.objectContaining({
71 | [testMatchHeader]: testEtag,
72 | "Content-Type": plainTextContentType,
73 | }),
74 | url: `${testEndpoint}${
75 | Resources.ACTIVITY_PROFILE
76 | }?activityId=${encodeURIComponent(
77 | testActivity.id
78 | )}&profileId=${encodeURIComponent(testProfileIdTextPlain)}`,
79 | data: testDocument.test,
80 | })
81 | );
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/createAgentProfile/CreateAgentProfileParams.ts:
--------------------------------------------------------------------------------
1 | import { Agent } from "../../../../XAPI";
2 | import { DocumentJson } from "../../Document";
3 |
4 | export interface CreateAgentProfileParams {
5 | agent: Agent;
6 | profileId: string;
7 | profile: DocumentJson;
8 | etag?: string;
9 | matchHeader?: "If-Match" | "If-None-Match";
10 | }
11 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/createAgentProfile/createAgentProfile.int.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | testAgent,
3 | testProfileId,
4 | testDocument,
5 | } from "../../../../../test/constants";
6 | import { forEachLRS } from "../../../../../test/getCredentials";
7 |
8 | forEachLRS((xapi) => {
9 | describe("agent profile resource", () => {
10 | test("can create agent profile", () => {
11 | return xapi
12 | .createAgentProfile({
13 | agent: testAgent,
14 | profileId: testProfileId,
15 | profile: testDocument,
16 | })
17 | .then((result) => {
18 | return expect(result.data).toBeDefined();
19 | });
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/createAgentProfile/createAgentProfile.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { CreateAgentProfileParams } from "./CreateAgentProfileParams";
5 |
6 | export function createAgentProfile(
7 | this: XAPI,
8 | params: CreateAgentProfileParams
9 | ): AdapterPromise {
10 | const headers = {};
11 | if (params.etag) headers[params.matchHeader] = params.etag;
12 | return this.requestResource({
13 | resource: Resources.AGENT_PROFILE,
14 | queryParams: {
15 | agent: params.agent,
16 | profileId: params.profileId,
17 | },
18 | requestConfig: {
19 | method: "POST",
20 | data: params.profile,
21 | headers: headers,
22 | },
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/createAgentProfile/createAgentProfile.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../../XAPI";
2 | import {
3 | testAgent,
4 | testDocument,
5 | testEndpoint,
6 | testProfileId,
7 | } from "../../../../../test/constants";
8 | import { Resources } from "../../../../constants";
9 |
10 | describe("agent profile resource", () => {
11 | beforeEach(() => {
12 | global.adapterFn.mockClear();
13 | global.adapterFn.mockResolvedValueOnce({
14 | headers: {
15 | "content-type": "application/json",
16 | },
17 | });
18 | });
19 |
20 | test("can create agent profile", async () => {
21 | const xapi = new XAPI({
22 | endpoint: testEndpoint,
23 | adapter: global.adapter,
24 | });
25 | await xapi.createAgentProfile({
26 | agent: testAgent,
27 | profileId: testProfileId,
28 | profile: testDocument,
29 | });
30 | expect(global.adapterFn).toHaveBeenCalledWith(
31 | expect.objectContaining({
32 | method: "POST",
33 | url: `${testEndpoint}${
34 | Resources.AGENT_PROFILE
35 | }?agent=${encodeURIComponent(
36 | JSON.stringify(testAgent)
37 | )}&profileId=${encodeURIComponent(testProfileId)}`,
38 | data: testDocument,
39 | })
40 | );
41 | });
42 |
43 | test("can create agent profile with an etag", async () => {
44 | const xapi = new XAPI({
45 | endpoint: testEndpoint,
46 | adapter: global.adapter,
47 | });
48 | const testEtag = "my-etag";
49 | const testMatchHeader = "If-Match";
50 | await xapi.createAgentProfile({
51 | agent: testAgent,
52 | profileId: testProfileId,
53 | profile: testDocument,
54 | etag: testEtag,
55 | matchHeader: testMatchHeader,
56 | });
57 | expect(global.adapterFn).toHaveBeenCalledWith(
58 | expect.objectContaining({
59 | method: "POST",
60 | url: `${testEndpoint}${
61 | Resources.AGENT_PROFILE
62 | }?agent=${encodeURIComponent(
63 | JSON.stringify(testAgent)
64 | )}&profileId=${encodeURIComponent(testProfileId)}`,
65 | data: testDocument,
66 | headers: expect.objectContaining({
67 | [testMatchHeader]: testEtag,
68 | }),
69 | })
70 | );
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/deleteAgentProfile/DeleteAgentProfileParams.ts:
--------------------------------------------------------------------------------
1 | import { Agent } from "../../../../XAPI";
2 |
3 | export interface DeleteAgentProfileParams {
4 | agent: Agent;
5 | profileId: string;
6 | etag?: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/deleteAgentProfile/deleteAgentProfile.int.test.ts:
--------------------------------------------------------------------------------
1 | import { testAgent, testProfileId } from "../../../../../test/constants";
2 | import { forEachLRS } from "../../../../../test/getCredentials";
3 |
4 | forEachLRS((xapi) => {
5 | describe("agent profile resource", () => {
6 | test("can delete an agent profile", () => {
7 | return xapi
8 | .deleteAgentProfile({
9 | agent: testAgent,
10 | profileId: testProfileId,
11 | })
12 | .then((result) => {
13 | return expect(result.data).toBeDefined();
14 | });
15 | });
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/deleteAgentProfile/deleteAgentProfile.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { DeleteAgentProfileParams } from "./DeleteAgentProfileParams";
5 |
6 | export function deleteAgentProfile(
7 | this: XAPI,
8 | params: DeleteAgentProfileParams
9 | ): AdapterPromise {
10 | const headers = {};
11 | if (params.etag) headers["If-Match"] = params.etag;
12 | return this.requestResource({
13 | resource: Resources.AGENT_PROFILE,
14 | queryParams: {
15 | agent: params.agent,
16 | profileId: params.profileId,
17 | },
18 | requestConfig: {
19 | method: "DELETE",
20 | headers: headers,
21 | },
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/deleteAgentProfile/deleteAgentProfile.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../../XAPI";
2 | import {
3 | testAgent,
4 | testEndpoint,
5 | testProfileId,
6 | } from "../../../../../test/constants";
7 | import { Resources } from "../../../../constants";
8 |
9 | describe("agent profile resource", () => {
10 | beforeEach(() => {
11 | global.adapterFn.mockClear();
12 | global.adapterFn.mockResolvedValueOnce({
13 | headers: {
14 | "content-type": "application/json",
15 | },
16 | });
17 | });
18 |
19 | test("can delete an agent profile", async () => {
20 | const xapi = new XAPI({
21 | endpoint: testEndpoint,
22 | adapter: global.adapter,
23 | });
24 | await xapi.deleteAgentProfile({
25 | agent: testAgent,
26 | profileId: testProfileId,
27 | });
28 | expect(global.adapterFn).toHaveBeenCalledWith(
29 | expect.objectContaining({
30 | method: "DELETE",
31 | url: `${testEndpoint}${
32 | Resources.AGENT_PROFILE
33 | }?agent=${encodeURIComponent(
34 | JSON.stringify(testAgent)
35 | )}&profileId=${encodeURIComponent(testProfileId)}`,
36 | })
37 | );
38 | });
39 |
40 | test("can delete an agent profile with an etag", async () => {
41 | const xapi = new XAPI({
42 | endpoint: testEndpoint,
43 | adapter: global.adapter,
44 | });
45 | const testEtag = "my-etag";
46 | await xapi.deleteAgentProfile({
47 | agent: testAgent,
48 | profileId: testProfileId,
49 | etag: testEtag,
50 | });
51 | expect(global.adapterFn).toHaveBeenCalledWith(
52 | expect.objectContaining({
53 | method: "DELETE",
54 | url: `${testEndpoint}${
55 | Resources.AGENT_PROFILE
56 | }?agent=${encodeURIComponent(
57 | JSON.stringify(testAgent)
58 | )}&profileId=${encodeURIComponent(testProfileId)}`,
59 | headers: expect.objectContaining({
60 | ["If-Match"]: testEtag,
61 | }),
62 | })
63 | );
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/getAgentProfile/GetAgentProfileParams.ts:
--------------------------------------------------------------------------------
1 | import { Agent } from "../../../../XAPI";
2 | import { GetParamsBase } from "../../../GetParamsBase";
3 |
4 | export interface GetAgentProfileParams extends GetParamsBase {
5 | agent: Agent;
6 | profileId: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/getAgentProfile/getAgentProfile.int.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | testAgent,
3 | testProfileId,
4 | testDocument,
5 | } from "../../../../../test/constants";
6 | import { forEachLRS } from "../../../../../test/getCredentials";
7 |
8 | forEachLRS((xapi) => {
9 | describe("agent profile resource", () => {
10 | test("can get an agent profile", () => {
11 | return xapi
12 | .createAgentProfile({
13 | agent: testAgent,
14 | profileId: testProfileId,
15 | profile: testDocument,
16 | })
17 | .then(() => {
18 | return xapi.getAgentProfile({
19 | agent: testAgent,
20 | profileId: testProfileId,
21 | });
22 | })
23 | .then((result) => {
24 | return expect(result.data).toMatchObject(testDocument);
25 | });
26 | });
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/getAgentProfile/getAgentProfile.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { Document } from "../../Document";
5 | import { GetAgentProfileParams } from "./GetAgentProfileParams";
6 |
7 | export function getAgentProfile(
8 | this: XAPI,
9 | params: GetAgentProfileParams
10 | ): AdapterPromise {
11 | return this.requestResource({
12 | resource: Resources.AGENT_PROFILE,
13 | queryParams: {
14 | agent: params.agent,
15 | profileId: params.profileId,
16 | },
17 | requestOptions: {
18 | useCacheBuster: params.useCacheBuster,
19 | },
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/getAgentProfile/getAgentProfile.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../../XAPI";
2 | import {
3 | testAgent,
4 | testEndpoint,
5 | testProfileId,
6 | } from "../../../../../test/constants";
7 | import { Resources } from "../../../../constants";
8 |
9 | describe("agent profile resource", () => {
10 | beforeEach(() => {
11 | global.adapterFn.mockClear();
12 | global.adapterFn.mockResolvedValueOnce({
13 | headers: {
14 | "content-type": "application/json",
15 | },
16 | });
17 | });
18 |
19 | test("can get an agent profile", async () => {
20 | const xapi = new XAPI({
21 | endpoint: testEndpoint,
22 | adapter: global.adapter,
23 | });
24 | await xapi.getAgentProfile({
25 | agent: testAgent,
26 | profileId: testProfileId,
27 | });
28 | expect(global.adapterFn).toHaveBeenCalledWith(
29 | expect.objectContaining({
30 | method: "GET",
31 | url: `${testEndpoint}${
32 | Resources.AGENT_PROFILE
33 | }?agent=${encodeURIComponent(
34 | JSON.stringify(testAgent)
35 | )}&profileId=${encodeURIComponent(testProfileId)}`,
36 | })
37 | );
38 | });
39 |
40 | test("can get an agent profile with cache buster", async () => {
41 | const xapi = new XAPI({
42 | endpoint: testEndpoint,
43 | adapter: global.adapter,
44 | });
45 | await xapi.getAgentProfile({
46 | agent: testAgent,
47 | profileId: testProfileId,
48 | useCacheBuster: true,
49 | });
50 | expect(global.adapterFn).toHaveBeenCalledWith(
51 | expect.objectContaining({
52 | method: "GET",
53 | url: expect.stringContaining(
54 | `${testEndpoint}${Resources.AGENT_PROFILE}?agent=${encodeURIComponent(
55 | JSON.stringify(testAgent)
56 | )}&profileId=${encodeURIComponent(testProfileId)}&cachebuster=`
57 | ),
58 | })
59 | );
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/getAgentProfiles/GetAgentProfilesParams.ts:
--------------------------------------------------------------------------------
1 | import { Agent, Timestamp } from "../../../../XAPI";
2 | import { GetParamsBase } from "../../../GetParamsBase";
3 |
4 | export interface GetAgentProfilesParams extends GetParamsBase {
5 | agent: Agent;
6 | since?: Timestamp;
7 | }
8 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/getAgentProfiles/getAgentProfiles.int.test.ts:
--------------------------------------------------------------------------------
1 | import { testAgent } from "../../../../../test/constants";
2 | import { forEachLRS } from "../../../../../test/getCredentials";
3 |
4 | forEachLRS((xapi) => {
5 | describe("agent profile resource", () => {
6 | test("can get all agent profiles", () => {
7 | return xapi
8 | .getAgentProfiles({
9 | agent: testAgent,
10 | })
11 | .then((result) => {
12 | return expect(result.data).toEqual(expect.any(Array));
13 | });
14 | });
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/getAgentProfiles/getAgentProfiles.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { GetAgentProfilesParams } from "./GetAgentProfilesParams";
5 |
6 | export function getAgentProfiles(
7 | this: XAPI,
8 | params: GetAgentProfilesParams
9 | ): AdapterPromise {
10 | return this.requestResource({
11 | resource: Resources.AGENT_PROFILE,
12 | queryParams: {
13 | agent: params.agent,
14 | ...(!!params.since && { since: params.since }),
15 | },
16 | requestOptions: { useCacheBuster: params.useCacheBuster },
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/getAgentProfiles/getAgentProfiles.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../../XAPI";
2 | import { testAgent, testEndpoint } from "../../../../../test/constants";
3 | import { Resources } from "../../../../constants";
4 |
5 | describe("agent profile resource", () => {
6 | beforeEach(() => {
7 | global.adapterFn.mockClear();
8 | global.adapterFn.mockResolvedValueOnce({
9 | headers: {
10 | "content-type": "application/json",
11 | },
12 | });
13 | });
14 |
15 | test("can get all agent profiles", async () => {
16 | const xapi = new XAPI({
17 | endpoint: testEndpoint,
18 | adapter: global.adapter,
19 | });
20 | await xapi.getAgentProfiles({
21 | agent: testAgent,
22 | });
23 | expect(global.adapterFn).toHaveBeenCalledWith(
24 | expect.objectContaining({
25 | method: "GET",
26 | url: `${testEndpoint}${
27 | Resources.AGENT_PROFILE
28 | }?agent=${encodeURIComponent(JSON.stringify(testAgent))}`,
29 | })
30 | );
31 | });
32 |
33 | test("can get all agent profiles since a certain date", async () => {
34 | const xapi = new XAPI({
35 | endpoint: testEndpoint,
36 | adapter: global.adapter,
37 | });
38 | const since = new Date();
39 | since.setDate(since.getDate() - 1); // yesterday
40 | await xapi.getAgentProfiles({
41 | agent: testAgent,
42 | since: since.toISOString(),
43 | });
44 | expect(global.adapterFn).toHaveBeenCalledWith(
45 | expect.objectContaining({
46 | method: "GET",
47 | url: `${testEndpoint}${
48 | Resources.AGENT_PROFILE
49 | }?agent=${encodeURIComponent(
50 | JSON.stringify(testAgent)
51 | )}&since=${encodeURIComponent(since.toISOString())}`,
52 | })
53 | );
54 | });
55 |
56 | test("can get all agent profiles with cache buster", async () => {
57 | const xapi = new XAPI({
58 | endpoint: testEndpoint,
59 | adapter: global.adapter,
60 | });
61 | await xapi.getAgentProfiles({
62 | agent: testAgent,
63 | useCacheBuster: true,
64 | });
65 | expect(global.adapterFn).toHaveBeenCalledWith(
66 | expect.objectContaining({
67 | method: "GET",
68 | url: expect.stringContaining(
69 | `${testEndpoint}${Resources.AGENT_PROFILE}?agent=${encodeURIComponent(
70 | JSON.stringify(testAgent)
71 | )}&cachebuster=`
72 | ),
73 | })
74 | );
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/setAgentProfile/SetAgentProfileParams.ts:
--------------------------------------------------------------------------------
1 | import { Agent } from "../../../../XAPI";
2 | import { Document } from "../../Document";
3 |
4 | export interface SetAgentProfileParams {
5 | agent: Agent;
6 | profileId: string;
7 | profile: Document;
8 | etag: string;
9 | matchHeader: "If-Match" | "If-None-Match";
10 | contentType?: string;
11 | }
12 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/setAgentProfile/setAgentProfile.int.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | testAgent,
3 | testProfileId,
4 | testDocument,
5 | testProfileIdTextPlain,
6 | } from "../../../../../test/constants";
7 | import { forEachLRS } from "../../../../../test/getCredentials";
8 |
9 | forEachLRS((xapi) => {
10 | describe("agent profile resource", () => {
11 | test("can set agent profile", () => {
12 | return xapi
13 | .createAgentProfile({
14 | agent: testAgent,
15 | profileId: testProfileId,
16 | profile: testDocument,
17 | })
18 | .then(() => {
19 | return xapi.getAgentProfile({
20 | agent: testAgent,
21 | profileId: testProfileId,
22 | });
23 | })
24 | .then((result) => {
25 | return xapi.setAgentProfile({
26 | agent: testAgent,
27 | profileId: testProfileId,
28 | profile: testDocument,
29 | etag: result.headers.etag,
30 | matchHeader: "If-Match",
31 | });
32 | })
33 | .then((result) => {
34 | return expect(result.data).toBeDefined();
35 | });
36 | });
37 |
38 | test("can set agent profile with text/plain content type", () => {
39 | return xapi
40 | .deleteAgentProfile({
41 | agent: testAgent,
42 | profileId: testProfileIdTextPlain,
43 | })
44 | .then(() => {
45 | return xapi.setAgentProfile({
46 | agent: testAgent,
47 | profileId: testProfileIdTextPlain,
48 | profile: testDocument.test,
49 | etag: "*",
50 | matchHeader: "If-None-Match",
51 | contentType: "text/plain",
52 | });
53 | })
54 | .then((result) => {
55 | return expect(result.data).toBeDefined();
56 | });
57 | });
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/setAgentProfile/setAgentProfile.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { SetAgentProfileParams } from "./SetAgentProfileParams";
5 |
6 | export function setAgentProfile(
7 | this: XAPI,
8 | params: SetAgentProfileParams
9 | ): AdapterPromise {
10 | const headers = {};
11 | headers[params.matchHeader] = params.etag;
12 | if (params.contentType) headers["Content-Type"] = params.contentType;
13 | return this.requestResource({
14 | resource: Resources.AGENT_PROFILE,
15 | queryParams: {
16 | agent: params.agent,
17 | profileId: params.profileId,
18 | },
19 | requestConfig: {
20 | method: "PUT",
21 | data: params.profile,
22 | headers: headers,
23 | },
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/src/resources/document/agentProfile/setAgentProfile/setAgentProfile.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../../XAPI";
2 | import {
3 | testAgent,
4 | testDocument,
5 | testEndpoint,
6 | testProfileId,
7 | } from "../../../../../test/constants";
8 | import { Resources } from "../../../../constants";
9 |
10 | describe("agent profile resource", () => {
11 | beforeEach(() => {
12 | global.adapterFn.mockClear();
13 | global.adapterFn.mockResolvedValueOnce({
14 | headers: {
15 | "content-type": "application/json",
16 | },
17 | });
18 | });
19 |
20 | test("can set agent profile", async () => {
21 | const xapi = new XAPI({
22 | endpoint: testEndpoint,
23 | adapter: global.adapter,
24 | });
25 | const testEtag = "my-etag";
26 | const testMatchHeader = "If-Match";
27 | await xapi.setAgentProfile({
28 | agent: testAgent,
29 | profileId: testProfileId,
30 | profile: testDocument,
31 | etag: testEtag,
32 | matchHeader: testMatchHeader,
33 | });
34 | expect(global.adapterFn).toHaveBeenCalledWith(
35 | expect.objectContaining({
36 | method: "PUT",
37 | url: `${testEndpoint}${
38 | Resources.AGENT_PROFILE
39 | }?agent=${encodeURIComponent(
40 | JSON.stringify(testAgent)
41 | )}&profileId=${encodeURIComponent(testProfileId)}`,
42 | data: testDocument,
43 | headers: expect.objectContaining({
44 | [testMatchHeader]: testEtag,
45 | }),
46 | })
47 | );
48 | });
49 |
50 | test("can set agent profile with content type", async () => {
51 | const xapi = new XAPI({
52 | endpoint: testEndpoint,
53 | adapter: global.adapter,
54 | });
55 | const testEtag = "my-etag";
56 | const testMatchHeader = "If-Match";
57 | const plainTextContentType = "text/plain";
58 | await xapi.setAgentProfile({
59 | agent: testAgent,
60 | profileId: testProfileId,
61 | profile: testDocument.test,
62 | etag: testEtag,
63 | matchHeader: testMatchHeader,
64 | contentType: plainTextContentType,
65 | });
66 | expect(global.adapterFn).toHaveBeenCalledWith(
67 | expect.objectContaining({
68 | method: "PUT",
69 | url: `${testEndpoint}${
70 | Resources.AGENT_PROFILE
71 | }?agent=${encodeURIComponent(
72 | JSON.stringify(testAgent)
73 | )}&profileId=${encodeURIComponent(testProfileId)}`,
74 | data: testDocument.test,
75 | headers: expect.objectContaining({
76 | [testMatchHeader]: testEtag,
77 | "Content-Type": plainTextContentType,
78 | }),
79 | })
80 | );
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/src/resources/document/state/createState/CreateStateParams.ts:
--------------------------------------------------------------------------------
1 | import { Agent } from "../../../../XAPI";
2 | import { DocumentJson } from "../../Document";
3 |
4 | export interface CreateStateParams {
5 | agent: Agent;
6 | activityId: string;
7 | stateId: string;
8 | state: DocumentJson;
9 | registration?: string;
10 | etag?: string;
11 | matchHeader?: "If-Match" | "If-None-Match";
12 | }
13 |
--------------------------------------------------------------------------------
/src/resources/document/state/createState/createState.int.test.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 | import {
3 | testAgent,
4 | testActivity,
5 | testStateId,
6 | testDocument,
7 | } from "../../../../../test/constants";
8 | import { forEachLRS } from "../../../../../test/getCredentials";
9 |
10 | forEachLRS((xapi) => {
11 | describe("state resource", () => {
12 | describe("create state", () => {
13 | test("can create state", () => {
14 | return xapi
15 | .createState({
16 | agent: testAgent,
17 | activityId: testActivity.id,
18 | stateId: testStateId,
19 | state: testDocument,
20 | })
21 | .then((result) => {
22 | return expect(result.data).toBeDefined();
23 | });
24 | });
25 |
26 | test("can create state with registration", () => {
27 | return xapi
28 | .createState({
29 | agent: testAgent,
30 | activityId: testActivity.id,
31 | stateId: testStateId,
32 | state: testDocument,
33 | registration: crypto.randomUUID(),
34 | })
35 | .then((response) => {
36 | return expect(response.data).toBeDefined();
37 | });
38 | });
39 |
40 | test("can add to a state using an etag", () => {
41 | const stateId = new Date().getTime().toString();
42 | return xapi
43 | .createState({
44 | agent: testAgent,
45 | activityId: testActivity.id,
46 | stateId: stateId,
47 | state: {
48 | x: "foo",
49 | y: "bar",
50 | },
51 | })
52 | .then(() => {
53 | return xapi.getState({
54 | agent: testAgent,
55 | activityId: testActivity.id,
56 | stateId: stateId,
57 | });
58 | })
59 | .then((response) => {
60 | return xapi.createState({
61 | agent: testAgent,
62 | activityId: testActivity.id,
63 | stateId: stateId,
64 | state: {
65 | x: "bash",
66 | z: "faz",
67 | },
68 | etag: response.headers.etag,
69 | matchHeader: "If-Match",
70 | });
71 | })
72 | .then(() => {
73 | return xapi.getState({
74 | agent: testAgent,
75 | activityId: testActivity.id,
76 | stateId: stateId,
77 | });
78 | })
79 | .then((response) => {
80 | return expect(response.data).toEqual({
81 | x: "bash",
82 | y: "bar",
83 | z: "faz",
84 | });
85 | });
86 | });
87 | });
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/src/resources/document/state/createState/createState.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { CreateStateParams } from "./CreateStateParams";
5 |
6 | export function createState(
7 | this: XAPI,
8 | params: CreateStateParams
9 | ): AdapterPromise {
10 | const headers = {};
11 | if (params.etag && params.matchHeader)
12 | headers[params.matchHeader] = params.etag;
13 | return this.requestResource({
14 | resource: Resources.STATE,
15 | queryParams: {
16 | agent: params.agent,
17 | activityId: params.activityId,
18 | stateId: params.stateId,
19 | ...(!!params.registration && { registration: params.registration }),
20 | },
21 | requestConfig: {
22 | method: "POST",
23 | data: params.state,
24 | headers: headers,
25 | },
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/src/resources/document/state/createState/createState.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../../XAPI";
2 | import {
3 | testActivity,
4 | testAgent,
5 | testDocument,
6 | testEndpoint,
7 | testStateId,
8 | } from "../../../../../test/constants";
9 | import { Resources } from "../../../../constants";
10 |
11 | describe("state resource", () => {
12 | beforeEach(() => {
13 | global.adapterFn.mockClear();
14 | global.adapterFn.mockResolvedValueOnce({
15 | headers: {
16 | "content-type": "application/json",
17 | },
18 | });
19 | });
20 |
21 | test("can create state", async () => {
22 | const xapi = new XAPI({
23 | endpoint: testEndpoint,
24 | adapter: global.adapter,
25 | });
26 | await xapi.createState({
27 | agent: testAgent,
28 | activityId: testActivity.id,
29 | stateId: testStateId,
30 | state: testDocument,
31 | });
32 | expect(global.adapterFn).toHaveBeenCalledWith(
33 | expect.objectContaining({
34 | method: "POST",
35 | url: `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
36 | JSON.stringify(testAgent)
37 | )}&activityId=${encodeURIComponent(
38 | testActivity.id
39 | )}&stateId=${encodeURIComponent(testStateId)}`,
40 | data: testDocument,
41 | })
42 | );
43 | });
44 |
45 | test("can create state with registration", async () => {
46 | const xapi = new XAPI({
47 | endpoint: testEndpoint,
48 | adapter: global.adapter,
49 | });
50 | const testRegistration = "test-registration";
51 | await xapi.createState({
52 | agent: testAgent,
53 | activityId: testActivity.id,
54 | stateId: testStateId,
55 | state: testDocument,
56 | registration: testRegistration,
57 | });
58 | expect(global.adapterFn).toHaveBeenCalledWith(
59 | expect.objectContaining({
60 | method: "POST",
61 | url: `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
62 | JSON.stringify(testAgent)
63 | )}&activityId=${encodeURIComponent(
64 | testActivity.id
65 | )}&stateId=${encodeURIComponent(
66 | testStateId
67 | )}®istration=${testRegistration}`,
68 | data: testDocument,
69 | })
70 | );
71 | });
72 |
73 | test("can create state with etag and match header", async () => {
74 | const xapi = new XAPI({
75 | endpoint: testEndpoint,
76 | adapter: global.adapter,
77 | });
78 | const testEtag = "my-etag";
79 | const testMatchHeader = "If-Match";
80 | await xapi.createState({
81 | agent: testAgent,
82 | activityId: testActivity.id,
83 | stateId: testStateId,
84 | state: testDocument,
85 | etag: testEtag,
86 | matchHeader: testMatchHeader,
87 | });
88 | expect(global.adapterFn).toHaveBeenCalledWith(
89 | expect.objectContaining({
90 | method: "POST",
91 | headers: expect.objectContaining({
92 | [testMatchHeader]: testEtag,
93 | }),
94 | url: `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
95 | JSON.stringify(testAgent)
96 | )}&activityId=${encodeURIComponent(
97 | testActivity.id
98 | )}&stateId=${encodeURIComponent(testStateId)}`,
99 | data: testDocument,
100 | })
101 | );
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/src/resources/document/state/deleteState/DeleteStateParams.ts:
--------------------------------------------------------------------------------
1 | import { Agent } from "../../../../XAPI";
2 |
3 | export interface DeleteStateParams {
4 | agent: Agent;
5 | activityId: string;
6 | stateId: string;
7 | registration?: string;
8 | etag?: string;
9 | }
10 |
--------------------------------------------------------------------------------
/src/resources/document/state/deleteState/deleteState.int.test.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 | import {
3 | testAgent,
4 | testActivity,
5 | testStateId,
6 | testDocument,
7 | } from "../../../../../test/constants";
8 | import { forEachLRS } from "../../../../../test/getCredentials";
9 |
10 | forEachLRS((xapi) => {
11 | describe("state resource", () => {
12 | describe("delete state", () => {
13 | test("can delete a state", () => {
14 | return xapi
15 | .deleteState({
16 | agent: testAgent,
17 | activityId: testActivity.id,
18 | stateId: testStateId,
19 | })
20 | .then((result) => {
21 | return expect(result.data).toBeDefined();
22 | });
23 | });
24 |
25 | test("can delete a state with a registration", () => {
26 | const registration = crypto.randomUUID();
27 | return xapi
28 | .createState({
29 | agent: testAgent,
30 | activityId: testActivity.id,
31 | stateId: testStateId,
32 | state: testDocument,
33 | registration: registration,
34 | })
35 | .then(() => {
36 | return xapi.deleteState({
37 | agent: testAgent,
38 | activityId: testActivity.id,
39 | stateId: testStateId,
40 | registration: registration,
41 | });
42 | })
43 | .then((response) => {
44 | return expect(response.data).toBeDefined();
45 | });
46 | });
47 |
48 | test("can delete a state with an etag", () => {
49 | return xapi
50 | .createState({
51 | agent: testAgent,
52 | activityId: testActivity.id,
53 | stateId: testStateId,
54 | state: testDocument,
55 | })
56 | .then(() => {
57 | return xapi.getState({
58 | agent: testAgent,
59 | activityId: testActivity.id,
60 | stateId: testStateId,
61 | });
62 | })
63 | .then((response) => {
64 | return xapi.deleteState({
65 | agent: testAgent,
66 | activityId: testActivity.id,
67 | stateId: testStateId,
68 | etag: response.headers.etag,
69 | });
70 | })
71 | .then((response) => {
72 | return expect(response.data).toBeDefined();
73 | });
74 | });
75 | });
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/src/resources/document/state/deleteState/deleteState.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { DeleteStateParams } from "./DeleteStateParams";
5 |
6 | export function deleteState(
7 | this: XAPI,
8 | params: DeleteStateParams
9 | ): AdapterPromise {
10 | const headers = {};
11 | if (params.etag) headers["If-Match"] = params.etag;
12 | return this.requestResource({
13 | resource: Resources.STATE,
14 | queryParams: {
15 | agent: params.agent,
16 | activityId: params.activityId,
17 | stateId: params.stateId,
18 | ...(!!params.registration && { registration: params.registration }),
19 | },
20 | requestConfig: {
21 | method: "DELETE",
22 | headers: headers,
23 | },
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/src/resources/document/state/deleteState/deleteState.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../../XAPI";
2 | import {
3 | testActivity,
4 | testAgent,
5 | testEndpoint,
6 | testStateId,
7 | } from "../../../../../test/constants";
8 | import { Resources } from "../../../../constants";
9 |
10 | describe("state resource", () => {
11 | beforeEach(() => {
12 | global.adapterFn.mockClear();
13 | global.adapterFn.mockResolvedValueOnce({
14 | headers: {
15 | "content-type": "application/json",
16 | },
17 | });
18 | });
19 |
20 | test("can delete a state", async () => {
21 | const xapi = new XAPI({
22 | endpoint: testEndpoint,
23 | adapter: global.adapter,
24 | });
25 | await xapi.deleteState({
26 | agent: testAgent,
27 | activityId: testActivity.id,
28 | stateId: testStateId,
29 | });
30 | expect(global.adapterFn).toHaveBeenCalledWith(
31 | expect.objectContaining({
32 | method: "DELETE",
33 | url: `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
34 | JSON.stringify(testAgent)
35 | )}&activityId=${encodeURIComponent(
36 | testActivity.id
37 | )}&stateId=${encodeURIComponent(testStateId)}`,
38 | })
39 | );
40 | });
41 |
42 | test("can delete a state with registration", async () => {
43 | const xapi = new XAPI({
44 | endpoint: testEndpoint,
45 | adapter: global.adapter,
46 | });
47 | const testRegistration = "test-registration";
48 | await xapi.deleteState({
49 | agent: testAgent,
50 | activityId: testActivity.id,
51 | stateId: testStateId,
52 | registration: testRegistration,
53 | });
54 | expect(global.adapterFn).toHaveBeenCalledWith(
55 | expect.objectContaining({
56 | method: "DELETE",
57 | url: `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
58 | JSON.stringify(testAgent)
59 | )}&activityId=${encodeURIComponent(
60 | testActivity.id
61 | )}&stateId=${encodeURIComponent(
62 | testStateId
63 | )}®istration=${testRegistration}`,
64 | })
65 | );
66 | });
67 |
68 | test("can delete a state with etag", async () => {
69 | const xapi = new XAPI({
70 | endpoint: testEndpoint,
71 | adapter: global.adapter,
72 | });
73 | const testEtag = "my-etag";
74 | await xapi.deleteState({
75 | agent: testAgent,
76 | activityId: testActivity.id,
77 | stateId: testStateId,
78 | etag: testEtag,
79 | });
80 | expect(global.adapterFn).toHaveBeenCalledWith(
81 | expect.objectContaining({
82 | method: "DELETE",
83 | headers: expect.objectContaining({
84 | ["If-Match"]: testEtag,
85 | }),
86 | url: `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
87 | JSON.stringify(testAgent)
88 | )}&activityId=${encodeURIComponent(
89 | testActivity.id
90 | )}&stateId=${encodeURIComponent(testStateId)}`,
91 | })
92 | );
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/src/resources/document/state/deleteStates/DeleteStatesParams.ts:
--------------------------------------------------------------------------------
1 | import { Agent } from "../../../../XAPI";
2 |
3 | export interface DeleteStatesParams {
4 | agent: Agent;
5 | activityId: string;
6 | registration?: string;
7 | etag?: string;
8 | }
9 |
--------------------------------------------------------------------------------
/src/resources/document/state/deleteStates/deleteStates.int.test.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 | import {
3 | testActivity,
4 | testAgent,
5 | testDocument,
6 | testStateId,
7 | } from "../../../../../test/constants";
8 | import { forEachLRS } from "../../../../../test/getCredentials";
9 |
10 | forEachLRS((xapi) => {
11 | describe("state resource", () => {
12 | describe("delete states", () => {
13 | test("can delete all states", () => {
14 | return xapi
15 | .deleteStates({
16 | agent: testAgent,
17 | activityId: testActivity.id,
18 | })
19 | .then((result) => {
20 | return expect(result.data).toBeDefined();
21 | });
22 | });
23 |
24 | test("can delete all states for a registration", () => {
25 | const registration = crypto.randomUUID();
26 | return xapi
27 | .createState({
28 | agent: testAgent,
29 | activityId: testActivity.id,
30 | stateId: testStateId,
31 | state: testDocument,
32 | registration: registration,
33 | })
34 | .then(() => {
35 | return xapi.deleteStates({
36 | agent: testAgent,
37 | activityId: testActivity.id,
38 | registration: registration,
39 | });
40 | })
41 | .then((response) => {
42 | return expect(response.data).toBeDefined();
43 | });
44 | });
45 |
46 | test("can delete all states with an etag", () => {
47 | return xapi
48 | .getStates({
49 | agent: testAgent,
50 | activityId: testActivity.id,
51 | })
52 | .then((response) => {
53 | return xapi.deleteStates({
54 | agent: testAgent,
55 | activityId: testActivity.id,
56 | etag: response.headers.etag,
57 | });
58 | })
59 | .then((response) => {
60 | return expect(response.data).toBeDefined();
61 | });
62 | });
63 | });
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/src/resources/document/state/deleteStates/deleteStates.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { DeleteStatesParams } from "./DeleteStatesParams";
5 |
6 | export function deleteStates(
7 | this: XAPI,
8 | params: DeleteStatesParams
9 | ): AdapterPromise {
10 | const headers = {};
11 | if (params.etag) headers["If-Match"] = params.etag;
12 | return this.requestResource({
13 | resource: Resources.STATE,
14 | queryParams: {
15 | agent: params.agent,
16 | activityId: params.activityId,
17 | ...(!!params.registration && { registration: params.registration }),
18 | },
19 | requestConfig: {
20 | method: "DELETE",
21 | headers: headers,
22 | },
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/src/resources/document/state/deleteStates/deleteStates.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../../XAPI";
2 | import {
3 | testActivity,
4 | testAgent,
5 | testEndpoint,
6 | } from "../../../../../test/constants";
7 | import { Resources } from "../../../../constants";
8 |
9 | describe("state resource", () => {
10 | beforeEach(() => {
11 | global.adapterFn.mockClear();
12 | global.adapterFn.mockResolvedValueOnce({
13 | headers: {
14 | "content-type": "application/json",
15 | },
16 | });
17 | });
18 |
19 | test("can delete all states", async () => {
20 | const xapi = new XAPI({
21 | endpoint: testEndpoint,
22 | adapter: global.adapter,
23 | });
24 | await xapi.deleteStates({
25 | agent: testAgent,
26 | activityId: testActivity.id,
27 | });
28 | expect(global.adapterFn).toHaveBeenCalledWith(
29 | expect.objectContaining({
30 | method: "DELETE",
31 | url: `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
32 | JSON.stringify(testAgent)
33 | )}&activityId=${encodeURIComponent(testActivity.id)}`,
34 | })
35 | );
36 | });
37 |
38 | test("can delete all state for a registration", async () => {
39 | const xapi = new XAPI({
40 | endpoint: testEndpoint,
41 | adapter: global.adapter,
42 | });
43 | const testRegistration = "test-registration";
44 | await xapi.deleteStates({
45 | agent: testAgent,
46 | activityId: testActivity.id,
47 | registration: testRegistration,
48 | });
49 | expect(global.adapterFn).toHaveBeenCalledWith(
50 | expect.objectContaining({
51 | method: "DELETE",
52 | url: `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
53 | JSON.stringify(testAgent)
54 | )}&activityId=${encodeURIComponent(
55 | testActivity.id
56 | )}®istration=${testRegistration}`,
57 | })
58 | );
59 | });
60 |
61 | test("can delete all states with etag", async () => {
62 | const xapi = new XAPI({
63 | endpoint: testEndpoint,
64 | adapter: global.adapter,
65 | });
66 | const testEtag = "my-etag";
67 | await xapi.deleteStates({
68 | agent: testAgent,
69 | activityId: testActivity.id,
70 | etag: testEtag,
71 | });
72 | expect(global.adapterFn).toHaveBeenCalledWith(
73 | expect.objectContaining({
74 | method: "DELETE",
75 | headers: expect.objectContaining({
76 | ["If-Match"]: testEtag,
77 | }),
78 | url: `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
79 | JSON.stringify(testAgent)
80 | )}&activityId=${encodeURIComponent(testActivity.id)}`,
81 | })
82 | );
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/src/resources/document/state/getState/GetStateParams.ts:
--------------------------------------------------------------------------------
1 | import { Agent } from "../../../../XAPI";
2 | import { GetParamsBase } from "../../../GetParamsBase";
3 |
4 | export interface GetStateParams extends GetParamsBase {
5 | agent: Agent;
6 | activityId: string;
7 | stateId: string;
8 | registration?: string;
9 | }
10 |
--------------------------------------------------------------------------------
/src/resources/document/state/getState/getState.int.test.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 | import {
3 | testAgent,
4 | testActivity,
5 | testStateId,
6 | testDocument,
7 | } from "../../../../../test/constants";
8 | import { forEachLRS } from "../../../../../test/getCredentials";
9 |
10 | forEachLRS((xapi) => {
11 | describe("state resource", () => {
12 | describe("get state", () => {
13 | test("can get a state", () => {
14 | return xapi
15 | .createState({
16 | agent: testAgent,
17 | activityId: testActivity.id,
18 | stateId: testStateId,
19 | state: testDocument,
20 | })
21 | .then(() => {
22 | return xapi.getState({
23 | agent: testAgent,
24 | activityId: testActivity.id,
25 | stateId: testStateId,
26 | });
27 | })
28 | .then((result) => {
29 | return expect(result.data).toMatchObject(testDocument);
30 | });
31 | });
32 |
33 | test("can get a state with a registration", () => {
34 | const registration = crypto.randomUUID();
35 | return xapi
36 | .createState({
37 | agent: testAgent,
38 | activityId: testActivity.id,
39 | stateId: testStateId,
40 | state: testDocument,
41 | registration: registration,
42 | })
43 | .then(() => {
44 | return xapi.getState({
45 | agent: testAgent,
46 | activityId: testActivity.id,
47 | stateId: testStateId,
48 | registration: registration,
49 | });
50 | })
51 | .then((response) => {
52 | return expect(response.data).toMatchObject(testDocument);
53 | });
54 | });
55 | });
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/src/resources/document/state/getState/getState.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { GetStateParams } from "./GetStateParams";
5 | import { Document } from "../../Document";
6 |
7 | export function getState(
8 | this: XAPI,
9 | params: GetStateParams
10 | ): AdapterPromise {
11 | return this.requestResource({
12 | resource: Resources.STATE,
13 | queryParams: {
14 | agent: params.agent,
15 | activityId: params.activityId,
16 | stateId: params.stateId,
17 | ...(!!params.registration && { registration: params.registration }),
18 | },
19 | requestOptions: { useCacheBuster: params.useCacheBuster },
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/src/resources/document/state/getState/getState.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../../XAPI";
2 | import {
3 | testActivity,
4 | testAgent,
5 | testEndpoint,
6 | testStateId,
7 | } from "../../../../../test/constants";
8 | import { Resources } from "../../../../constants";
9 |
10 | describe("state resource", () => {
11 | beforeEach(() => {
12 | global.adapterFn.mockClear();
13 | global.adapterFn.mockResolvedValueOnce({
14 | headers: {
15 | "content-type": "application/json",
16 | },
17 | });
18 | });
19 |
20 | test("can get a state", async () => {
21 | const xapi = new XAPI({
22 | endpoint: testEndpoint,
23 | adapter: global.adapter,
24 | });
25 | await xapi.getState({
26 | agent: testAgent,
27 | activityId: testActivity.id,
28 | stateId: testStateId,
29 | });
30 | expect(global.adapterFn).toHaveBeenCalledWith(
31 | expect.objectContaining({
32 | method: "GET",
33 | url: `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
34 | JSON.stringify(testAgent)
35 | )}&activityId=${encodeURIComponent(
36 | testActivity.id
37 | )}&stateId=${encodeURIComponent(testStateId)}`,
38 | })
39 | );
40 | });
41 |
42 | test("can get a state with a registration", async () => {
43 | const xapi = new XAPI({
44 | endpoint: testEndpoint,
45 | adapter: global.adapter,
46 | });
47 | const testRegistration = "test-registration";
48 | await xapi.getState({
49 | agent: testAgent,
50 | activityId: testActivity.id,
51 | stateId: testStateId,
52 | registration: testRegistration,
53 | });
54 | expect(global.adapterFn).toHaveBeenCalledWith(
55 | expect.objectContaining({
56 | method: "GET",
57 | url: `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
58 | JSON.stringify(testAgent)
59 | )}&activityId=${encodeURIComponent(
60 | testActivity.id
61 | )}&stateId=${encodeURIComponent(
62 | testStateId
63 | )}®istration=${testRegistration}`,
64 | })
65 | );
66 | });
67 |
68 | test("can get a state with cache buster", async () => {
69 | const xapi = new XAPI({
70 | endpoint: testEndpoint,
71 | adapter: global.adapter,
72 | });
73 | await xapi.getState({
74 | agent: testAgent,
75 | activityId: testActivity.id,
76 | stateId: testStateId,
77 | useCacheBuster: true,
78 | });
79 | expect(global.adapterFn).toHaveBeenCalledWith(
80 | expect.objectContaining({
81 | method: "GET",
82 | url: expect.stringContaining(
83 | `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
84 | JSON.stringify(testAgent)
85 | )}&activityId=${encodeURIComponent(
86 | testActivity.id
87 | )}&stateId=${encodeURIComponent(testStateId)}&cachebuster=`
88 | ),
89 | })
90 | );
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/src/resources/document/state/getStates/GetStatesParams.ts:
--------------------------------------------------------------------------------
1 | import { Agent, Timestamp } from "../../../../XAPI";
2 | import { GetParamsBase } from "../../../GetParamsBase";
3 |
4 | export interface GetStatesParams extends GetParamsBase {
5 | agent: Agent;
6 | activityId: string;
7 | registration?: string;
8 | since?: Timestamp;
9 | }
10 |
--------------------------------------------------------------------------------
/src/resources/document/state/getStates/getStates.int.test.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 | import {
3 | testAgent,
4 | testActivity,
5 | testDocument,
6 | testStateId,
7 | } from "../../../../../test/constants";
8 | import { forEachLRS } from "../../../../../test/getCredentials";
9 |
10 | forEachLRS((xapi) => {
11 | describe("state resource", () => {
12 | describe("get states", () => {
13 | test("can get all states", () => {
14 | return xapi
15 | .getStates({
16 | agent: testAgent,
17 | activityId: testActivity.id,
18 | })
19 | .then((result) => {
20 | return expect(result.data).toEqual(expect.any(Array));
21 | });
22 | });
23 |
24 | test("can get all states with a registration", () => {
25 | const registration = crypto.randomUUID();
26 | const stateId = new Date().getTime().toString();
27 | return xapi
28 | .createState({
29 | agent: testAgent,
30 | activityId: testActivity.id,
31 | stateId: stateId,
32 | state: { foo: "bar" },
33 | registration: registration,
34 | })
35 | .then(() => {
36 | return xapi.getStates({
37 | agent: testAgent,
38 | activityId: testActivity.id,
39 | registration: registration,
40 | });
41 | })
42 | .then((response) => {
43 | return expect(response.data).toEqual(
44 | expect.arrayContaining([expect.objectContaining({})])
45 | );
46 | });
47 | });
48 |
49 | test("can get all states since a certain date", () => {
50 | const since = new Date();
51 | since.setDate(since.getDate() - 1); // yesterday
52 | return xapi
53 | .createState({
54 | agent: testAgent,
55 | activityId: testActivity.id,
56 | stateId: testStateId,
57 | state: testDocument,
58 | })
59 | .then(() => {
60 | return xapi.getStates({
61 | agent: testAgent,
62 | activityId: testActivity.id,
63 | since: since.toISOString(),
64 | });
65 | })
66 | .then((response) => {
67 | return expect(response.data).toEqual(
68 | expect.arrayContaining([expect.objectContaining({})])
69 | );
70 | });
71 | });
72 | });
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/src/resources/document/state/getStates/getStates.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { GetStatesParams } from "./GetStatesParams";
5 |
6 | export function getStates(
7 | this: XAPI,
8 | params: GetStatesParams
9 | ): AdapterPromise {
10 | return this.requestResource({
11 | resource: Resources.STATE,
12 | queryParams: {
13 | agent: params.agent,
14 | activityId: params.activityId,
15 | ...(!!params.registration && { registration: params.registration }),
16 | ...(!!params.since && { since: params.since }),
17 | },
18 | requestOptions: { useCacheBuster: params.useCacheBuster },
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/src/resources/document/state/getStates/getStates.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../../XAPI";
2 | import {
3 | testActivity,
4 | testAgent,
5 | testEndpoint,
6 | } from "../../../../../test/constants";
7 | import { Resources } from "../../../../constants";
8 |
9 | describe("state resource", () => {
10 | beforeEach(() => {
11 | global.adapterFn.mockClear();
12 | global.adapterFn.mockResolvedValueOnce({
13 | headers: {
14 | "content-type": "application/json",
15 | },
16 | });
17 | });
18 |
19 | test("can get all states", async () => {
20 | const xapi = new XAPI({
21 | endpoint: testEndpoint,
22 | adapter: global.adapter,
23 | });
24 | await xapi.getStates({
25 | agent: testAgent,
26 | activityId: testActivity.id,
27 | });
28 | expect(global.adapterFn).toHaveBeenCalledWith(
29 | expect.objectContaining({
30 | method: "GET",
31 | url: `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
32 | JSON.stringify(testAgent)
33 | )}&activityId=${encodeURIComponent(testActivity.id)}`,
34 | })
35 | );
36 | });
37 |
38 | test("can get all states for a registration", async () => {
39 | const xapi = new XAPI({
40 | endpoint: testEndpoint,
41 | adapter: global.adapter,
42 | });
43 | const testRegistration = "test-registration";
44 | await xapi.getStates({
45 | agent: testAgent,
46 | activityId: testActivity.id,
47 | registration: testRegistration,
48 | });
49 | expect(global.adapterFn).toHaveBeenCalledWith(
50 | expect.objectContaining({
51 | method: "GET",
52 | url: `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
53 | JSON.stringify(testAgent)
54 | )}&activityId=${encodeURIComponent(
55 | testActivity.id
56 | )}®istration=${testRegistration}`,
57 | })
58 | );
59 | });
60 |
61 | test("can get all states since a certain date", async () => {
62 | const xapi = new XAPI({
63 | endpoint: testEndpoint,
64 | adapter: global.adapter,
65 | });
66 | const since = new Date();
67 | since.setDate(since.getDate() - 1); // yesterday
68 | await xapi.getStates({
69 | agent: testAgent,
70 | activityId: testActivity.id,
71 | since: since.toISOString(),
72 | });
73 | expect(global.adapterFn).toHaveBeenCalledWith(
74 | expect.objectContaining({
75 | method: "GET",
76 | url: `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
77 | JSON.stringify(testAgent)
78 | )}&activityId=${encodeURIComponent(
79 | testActivity.id
80 | )}&since=${encodeURIComponent(since.toISOString())}`,
81 | })
82 | );
83 | });
84 |
85 | test("can get all states with cache buster", async () => {
86 | const xapi = new XAPI({
87 | endpoint: testEndpoint,
88 | adapter: global.adapter,
89 | });
90 | await xapi.getStates({
91 | agent: testAgent,
92 | activityId: testActivity.id,
93 | useCacheBuster: true,
94 | });
95 | expect(global.adapterFn).toHaveBeenCalledWith(
96 | expect.objectContaining({
97 | method: "GET",
98 | url: expect.stringContaining(
99 | `${testEndpoint}${Resources.STATE}?agent=${encodeURIComponent(
100 | JSON.stringify(testAgent)
101 | )}&activityId=${encodeURIComponent(testActivity.id)}&cachebuster=`
102 | ),
103 | })
104 | );
105 | });
106 | });
107 |
--------------------------------------------------------------------------------
/src/resources/document/state/setState/SetStateParams.ts:
--------------------------------------------------------------------------------
1 | import { Agent } from "../../../../XAPI";
2 | import { Document } from "../../Document";
3 |
4 | export interface SetStateParams {
5 | agent: Agent;
6 | activityId: string;
7 | stateId: string;
8 | state: Document;
9 | registration?: string;
10 | etag?: string;
11 | matchHeader?: "If-Match" | "If-None-Match";
12 | contentType?: string;
13 | }
14 |
--------------------------------------------------------------------------------
/src/resources/document/state/setState/setState.int.test.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 | import {
3 | testAgent,
4 | testActivity,
5 | testStateId,
6 | testDocument,
7 | testStateIdTextPlain,
8 | } from "../../../../../test/constants";
9 | import { forEachLRS } from "../../../../../test/getCredentials";
10 |
11 | forEachLRS((xapi) => {
12 | describe("state resource", () => {
13 | describe("set state", () => {
14 | test("can set state", () => {
15 | return xapi
16 | .setState({
17 | agent: testAgent,
18 | activityId: testActivity.id,
19 | stateId: testStateId,
20 | state: testDocument,
21 | })
22 | .then((result) => {
23 | return expect(result.data).toBeDefined();
24 | });
25 | });
26 |
27 | test("can set state with registration", () => {
28 | return xapi
29 | .setState({
30 | agent: testAgent,
31 | activityId: testActivity.id,
32 | stateId: testStateId,
33 | state: testDocument,
34 | registration: crypto.randomUUID(),
35 | })
36 | .then((result) => {
37 | return expect(result.data).toBeDefined();
38 | });
39 | });
40 |
41 | test("can set state with text/plain content type", () => {
42 | return xapi
43 | .setState({
44 | agent: testAgent,
45 | activityId: testActivity.id,
46 | stateId: testStateIdTextPlain,
47 | state: testDocument.test,
48 | contentType: "text/plain",
49 | })
50 | .then((result) => {
51 | return expect(result.data).toBeDefined();
52 | });
53 | });
54 |
55 | test("can set a state using an etag", () => {
56 | const stateId = new Date().getTime().toString();
57 | return xapi
58 | .setState({
59 | agent: testAgent,
60 | activityId: testActivity.id,
61 | stateId: stateId,
62 | state: {
63 | x: "foo",
64 | y: "bar",
65 | },
66 | })
67 | .then(() => {
68 | return xapi.getState({
69 | agent: testAgent,
70 | activityId: testActivity.id,
71 | stateId: stateId,
72 | });
73 | })
74 | .then((response) => {
75 | return xapi.setState({
76 | agent: testAgent,
77 | activityId: testActivity.id,
78 | stateId: stateId,
79 | state: {
80 | x: "bash",
81 | z: "faz",
82 | },
83 | etag: response.headers.etag,
84 | matchHeader: "If-Match",
85 | });
86 | })
87 | .then(() => {
88 | return xapi.getState({
89 | agent: testAgent,
90 | activityId: testActivity.id,
91 | stateId: stateId,
92 | });
93 | })
94 | .then((response) => {
95 | return expect(response.data).toEqual({
96 | x: "bash",
97 | z: "faz",
98 | });
99 | });
100 | });
101 | });
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/src/resources/document/state/setState/setState.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../../adapters";
2 | import { Resources } from "../../../../constants";
3 | import XAPI from "../../../../XAPI";
4 | import { SetStateParams } from "./SetStateParams";
5 |
6 | export function setState(
7 | this: XAPI,
8 | params: SetStateParams
9 | ): AdapterPromise {
10 | const headers = {};
11 | if (params.etag && params.matchHeader)
12 | headers[params.matchHeader] = params.etag;
13 | if (params.contentType) headers["Content-Type"] = params.contentType;
14 | return this.requestResource({
15 | resource: Resources.STATE,
16 | queryParams: {
17 | agent: params.agent,
18 | activityId: params.activityId,
19 | stateId: params.stateId,
20 | ...(!!params.registration && { registration: params.registration }),
21 | },
22 | requestConfig: {
23 | method: "PUT",
24 | data: params.state,
25 | headers: headers,
26 | },
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/src/resources/statement/Account.ts:
--------------------------------------------------------------------------------
1 | export interface Account {
2 | homePage: string;
3 | name: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/resources/statement/Actor.ts:
--------------------------------------------------------------------------------
1 | import { Agent, Group } from ".";
2 |
3 | export type Actor = Agent | Group;
4 |
--------------------------------------------------------------------------------
/src/resources/statement/Agent.ts:
--------------------------------------------------------------------------------
1 | import { InverseFunctionalIdentifier } from ".";
2 |
3 | export interface Agent extends InverseFunctionalIdentifier {
4 | objectType?: "Agent";
5 | name?: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/resources/statement/AnonymousGroup.ts:
--------------------------------------------------------------------------------
1 | import { Agent } from ".";
2 |
3 | export interface AnonymousGroup {
4 | objectType: "Group";
5 | name?: string;
6 | member: Agent[];
7 | }
8 |
--------------------------------------------------------------------------------
/src/resources/statement/Attachment.ts:
--------------------------------------------------------------------------------
1 | import { LanguageMap, AttachmentUsage } from ".";
2 |
3 | export interface Attachment {
4 | usageType: AttachmentUsage;
5 | display: LanguageMap;
6 | contentType: string;
7 | length: number;
8 | sha2: string;
9 | description?: LanguageMap;
10 | fileUrl?: string;
11 | }
12 |
--------------------------------------------------------------------------------
/src/resources/statement/AttachmentUsage.ts:
--------------------------------------------------------------------------------
1 | import { AttachmentUsages } from "../../constants/AttachmentUsages";
2 |
3 | export type AttachmentUsage = AttachmentUsages | string;
4 |
--------------------------------------------------------------------------------
/src/resources/statement/Context.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ContextActivity,
3 | StatementRef,
4 | Extensions,
5 | RFC5646LanguageCodes,
6 | Actor,
7 | Group,
8 | } from ".";
9 |
10 | export interface Context {
11 | registration?: string;
12 | instructor?: Actor;
13 | team?: Group;
14 | contextActivities?: {
15 | parent?: ContextActivity[];
16 | grouping?: ContextActivity[];
17 | category?: ContextActivity[];
18 | other?: ContextActivity[];
19 | };
20 | statement?: StatementRef;
21 | revision?: string;
22 | platform?: string;
23 | language?: RFC5646LanguageCodes;
24 | extensions?: Extensions;
25 | }
26 |
--------------------------------------------------------------------------------
/src/resources/statement/ContextActivity.ts:
--------------------------------------------------------------------------------
1 | import { LanguageMap } from ".";
2 |
3 | export interface ContextActivity {
4 | objectType?: "Activity";
5 | id: string;
6 | definition?: {
7 | name?: LanguageMap;
8 | description?: LanguageMap;
9 | type: string;
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/src/resources/statement/Extensions.ts:
--------------------------------------------------------------------------------
1 | export interface Extensions {
2 | [uri: string]: unknown;
3 | }
4 |
--------------------------------------------------------------------------------
/src/resources/statement/Group.ts:
--------------------------------------------------------------------------------
1 | import { AnonymousGroup, IdentifiedGroup } from ".";
2 |
3 | export type Group = AnonymousGroup | IdentifiedGroup;
4 |
--------------------------------------------------------------------------------
/src/resources/statement/IdentifiedGroup.ts:
--------------------------------------------------------------------------------
1 | import { Agent, InverseFunctionalIdentifier } from ".";
2 |
3 | export interface IdentifiedGroup extends InverseFunctionalIdentifier {
4 | objectType: "Group";
5 | name?: string;
6 | member?: Agent[];
7 | }
8 |
--------------------------------------------------------------------------------
/src/resources/statement/InteractionActivity.ts:
--------------------------------------------------------------------------------
1 | import { InteractionActivityDefinition } from ".";
2 | import { Activity } from "../../XAPI";
3 |
4 | export interface InteractionActivity extends Activity {
5 | definition: InteractionActivityDefinition;
6 | }
7 |
--------------------------------------------------------------------------------
/src/resources/statement/InteractionActivityDefinition.ts:
--------------------------------------------------------------------------------
1 | import { LanguageMap } from ".";
2 | import { ActivityDefinition } from "../activities/ActivityDefinition";
3 |
4 | interface BaseInteractionActivityDefinition extends ActivityDefinition {
5 | type: "http://adlnet.gov/expapi/activities/cmi.interaction";
6 | correctResponsesPattern?: string[];
7 | }
8 |
9 | export interface InteractionComponent {
10 | id: string;
11 | description?: LanguageMap;
12 | }
13 |
14 | interface TrueFalseInteractionActivityDefinition
15 | extends BaseInteractionActivityDefinition {
16 | interactionType: "true-false";
17 | correctResponsesPattern?: ["true"] | ["false"];
18 | }
19 |
20 | interface ChoiceInteractionActivityDefinition
21 | extends BaseInteractionActivityDefinition {
22 | interactionType: "choice";
23 | choices?: InteractionComponent[];
24 | }
25 |
26 | interface FillInInteractionActivityDefinition
27 | extends BaseInteractionActivityDefinition {
28 | interactionType: "fill-in";
29 | }
30 |
31 | interface LongFillInInteractionActivityDefinition
32 | extends BaseInteractionActivityDefinition {
33 | interactionType: "long-fill-in";
34 | }
35 |
36 | interface LikertInteractionActivityDefinition
37 | extends BaseInteractionActivityDefinition {
38 | interactionType: "likert";
39 | scale?: InteractionComponent[];
40 | }
41 |
42 | interface MatchingInteractionActivityDefinition
43 | extends BaseInteractionActivityDefinition {
44 | interactionType: "matching";
45 | source?: InteractionComponent[];
46 | target?: InteractionComponent[];
47 | }
48 |
49 | interface PerformanceInteractionActivityDefinition
50 | extends BaseInteractionActivityDefinition {
51 | interactionType: "performance";
52 | steps?: InteractionComponent[];
53 | }
54 |
55 | interface SequencingInteractionActivityDefinition
56 | extends BaseInteractionActivityDefinition {
57 | interactionType: "sequencing";
58 | choices?: InteractionComponent[];
59 | }
60 |
61 | interface NumericInteractionActivityDefinition
62 | extends BaseInteractionActivityDefinition {
63 | interactionType: "numeric";
64 | }
65 |
66 | interface OtherInteractionActivityDefinition
67 | extends BaseInteractionActivityDefinition {
68 | interactionType: "other";
69 | }
70 |
71 | export type InteractionActivityDefinition =
72 | | TrueFalseInteractionActivityDefinition
73 | | ChoiceInteractionActivityDefinition
74 | | FillInInteractionActivityDefinition
75 | | LongFillInInteractionActivityDefinition
76 | | LikertInteractionActivityDefinition
77 | | MatchingInteractionActivityDefinition
78 | | PerformanceInteractionActivityDefinition
79 | | SequencingInteractionActivityDefinition
80 | | NumericInteractionActivityDefinition
81 | | OtherInteractionActivityDefinition;
82 |
--------------------------------------------------------------------------------
/src/resources/statement/InverseFunctionalIdentifier.ts:
--------------------------------------------------------------------------------
1 | import { Account } from "./Account";
2 |
3 | export interface InverseFunctionalIdentifier {
4 | mbox?: string;
5 | mbox_sha1sum?: string;
6 | account?: Account;
7 | openid?: string;
8 | }
9 |
--------------------------------------------------------------------------------
/src/resources/statement/LanguageMap.ts:
--------------------------------------------------------------------------------
1 | import { RFC5646LanguageCodes } from ".";
2 |
3 | export type LanguageMap = {
4 | [languageCode in RFC5646LanguageCodes]: string;
5 | };
6 |
--------------------------------------------------------------------------------
/src/resources/statement/ObjectiveActivity.ts:
--------------------------------------------------------------------------------
1 | import { ObjectiveActivityDefinition } from ".";
2 | import { Activity } from "../../XAPI";
3 |
4 | export interface ObjectiveActivity extends Activity {
5 | definition: ObjectiveActivityDefinition;
6 | }
7 |
--------------------------------------------------------------------------------
/src/resources/statement/ObjectiveActivityDefinition.ts:
--------------------------------------------------------------------------------
1 | import { ActivityDefinition } from "../../XAPI";
2 |
3 | export interface ObjectiveActivityDefinition extends ActivityDefinition {
4 | type: "http://adlnet.gov/expapi/activities/objective";
5 | }
6 |
--------------------------------------------------------------------------------
/src/resources/statement/Part.ts:
--------------------------------------------------------------------------------
1 | export type Part = unknown;
2 |
--------------------------------------------------------------------------------
/src/resources/statement/Result.ts:
--------------------------------------------------------------------------------
1 | import { Extensions } from ".";
2 |
3 | export interface ResultScore {
4 | scaled: number;
5 | raw?: number;
6 | min?: number;
7 | max?: number;
8 | }
9 |
10 | export interface Result {
11 | score?: ResultScore;
12 | success?: boolean;
13 | completion?: boolean;
14 | response?: string;
15 | duration?: string;
16 | extensions?: Extensions;
17 | }
18 |
--------------------------------------------------------------------------------
/src/resources/statement/Statement.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Verb,
3 | StatementObject,
4 | Result,
5 | Context,
6 | Attachment,
7 | Actor,
8 | Timestamp,
9 | } from ".";
10 | import { Versions } from "../../constants";
11 |
12 | export interface Statement {
13 | id?: string;
14 | actor: Actor;
15 | verb: Verb;
16 | object: StatementObject;
17 | result?: Result;
18 | context?: Context;
19 | timestamp?: Timestamp;
20 | stored?: Timestamp;
21 | authority?: Actor;
22 | version?: Versions;
23 | attachments?: Attachment[];
24 | }
25 |
--------------------------------------------------------------------------------
/src/resources/statement/StatementObject.ts:
--------------------------------------------------------------------------------
1 | import {
2 | StatementRef,
3 | SubStatement,
4 | InteractionActivity,
5 | ObjectiveActivity,
6 | Agent,
7 | Group,
8 | } from ".";
9 | import { Activity } from "../../XAPI";
10 | import type { WithRequiredProperty } from "../../internal/WithRequiredProperty";
11 |
12 | export type StatementObject =
13 | | Activity
14 | | InteractionActivity
15 | | ObjectiveActivity
16 | | WithRequiredProperty
17 | | Group
18 | | StatementRef
19 | | SubStatement;
20 |
--------------------------------------------------------------------------------
/src/resources/statement/StatementParamsBase.ts:
--------------------------------------------------------------------------------
1 | import { GetParamsBase } from "../GetParamsBase";
2 |
3 | export interface StatementParamsBase extends GetParamsBase {
4 | /**
5 | * Boolean determining if the statements’ attachments should be returned. Defaults to `false`.
6 | */
7 | attachments?: boolean;
8 | /**
9 | * `format` – what human readable names and descriptions are included in the statements.
10 | *
11 | * - `exact` format returns the statements exactly as they were received by the LRS (with some possible exceptions). `exact` format should be used when moving statements between LRSs or other systems that store statements.
12 | * - `ids` format returns only ids are returned with none of the human readable descriptions. This is useful if you need to fetch data that will be used for aggregation or other processing where human language names and descriptions are not required.
13 | * - `canonical` format requests the LRS to return its own internal definitions of objects, rather than those provided in the statement. If you trust the LRS, this is normally the most appropriate format when the data will be displayed to the end user. The LRS will build its internal definitions of objects based on statements it receives and other authoritative sources.
14 | */
15 | format?: "exact" | "ids" | "canonical";
16 | }
17 |
--------------------------------------------------------------------------------
/src/resources/statement/StatementRef.ts:
--------------------------------------------------------------------------------
1 | export interface StatementRef {
2 | objectType: "StatementRef";
3 | id: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/resources/statement/StatementResponseWithAttachments.ts:
--------------------------------------------------------------------------------
1 | import { Part, Statement } from ".";
2 |
3 | export type StatementResponseWithAttachments = [Statement, ...Part[]];
4 |
--------------------------------------------------------------------------------
/src/resources/statement/StatementsResponse.ts:
--------------------------------------------------------------------------------
1 | import { Statement } from ".";
2 |
3 | export interface StatementsResponse {
4 | statements: Statement[];
5 | /**
6 | * Relative URL that may be used to fetch more results, including the full path and optionally a query
7 | string but excluding scheme, host, and port. Empty string if there are no more results to fetch.
8 | */
9 | more: string;
10 | }
11 |
--------------------------------------------------------------------------------
/src/resources/statement/StatementsResponseWithAttachments.ts:
--------------------------------------------------------------------------------
1 | import { Part, StatementsResponse } from ".";
2 |
3 | export type StatementsResponseWithAttachments = [StatementsResponse, ...Part[]];
4 |
--------------------------------------------------------------------------------
/src/resources/statement/SubStatement.ts:
--------------------------------------------------------------------------------
1 | import { Statement, StatementObject } from ".";
2 |
3 | export interface SubStatement
4 | extends Omit {
5 | objectType: "SubStatement";
6 | object: Exclude;
7 | }
8 |
--------------------------------------------------------------------------------
/src/resources/statement/Timestamp.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * # 4.5 ISO 8601 Timestamps
3 | *
4 | * Timestamps are a format type which represent a specific time. They are formatted according to ISO 8601's normal format. Statements sent to an LRS can be expected to keep precision to at least milliseconds
5 | *
6 | * ## Requirements
7 | *
8 | * - A Timestamp MUST be formatted according to ISO 8601.
9 | * - A Timestamp SHOULD* be expressed using the format described in RFC 3339, which is a profile of ISO 8601.
10 | * - A Timestamp MUST preserve precision to at least milliseconds (3 decimal points beyond seconds).
11 | * - A Timestamp SHOULD* include the time zone.
12 | * - If the Timestamp includes a time zone, the LRS MAY be return the Timestamp using a different timezone to the one originally used in the Statement so long as the point in time referenced is not affected.
13 | * - The LRS SHOULD* return the Timestamp in UTC timezone.
14 | * - A Timestamp MAY be truncated or rounded to a precision of at least 3 decimal digits for seconds.
15 | *
16 | * Reference: https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#45-iso-8601-timestamps
17 | */
18 | export type Timestamp = string;
19 |
--------------------------------------------------------------------------------
/src/resources/statement/Verb.ts:
--------------------------------------------------------------------------------
1 | import { LanguageMap } from ".";
2 |
3 | export interface Verb {
4 | id: string;
5 | display?: LanguageMap;
6 | }
7 |
--------------------------------------------------------------------------------
/src/resources/statement/getMoreStatements/GetMoreStatementsParams.ts:
--------------------------------------------------------------------------------
1 | export interface GetMoreStatementsParams {
2 | more: string;
3 | }
4 |
--------------------------------------------------------------------------------
/src/resources/statement/getMoreStatements/getMoreStatements.int.test.ts:
--------------------------------------------------------------------------------
1 | import { forEachLRS } from "../../../../test/getCredentials";
2 | import { StatementsResponse } from "..";
3 |
4 | forEachLRS((xapi) => {
5 | describe("statement resource", () => {
6 | describe("more statements", () => {
7 | test("can get more statements using the more property", () => {
8 | return xapi
9 | .getStatements({
10 | limit: 1,
11 | })
12 | .then((result) => {
13 | return xapi.getMoreStatements({
14 | more: result.data.more,
15 | });
16 | })
17 | .then((result) => {
18 | return expect(
19 | (result.data as StatementsResponse).statements
20 | ).toBeTruthy();
21 | });
22 | });
23 |
24 | test("can get more statements with attachments using the more property", () => {
25 | return xapi
26 | .getStatements({
27 | limit: 1,
28 | attachments: true,
29 | })
30 | .then((result) => {
31 | return xapi.getMoreStatements({
32 | more: result.data[0].more,
33 | });
34 | })
35 | .then((result) => {
36 | return expect(result.data[0].statements).toBeTruthy();
37 | });
38 | });
39 | });
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/src/resources/statement/getMoreStatements/getMoreStatements.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../adapters";
2 | import XAPI from "../../../XAPI";
3 | import { StatementsResponse, StatementsResponseWithAttachments } from "..";
4 | import { GetMoreStatementsParams } from "./GetMoreStatementsParams";
5 |
6 | export function getMoreStatements(
7 | this: XAPI,
8 | params: GetMoreStatementsParams
9 | ): AdapterPromise {
10 | const endpoint = new URL(this.endpoint);
11 | const url = `${endpoint.protocol}//${endpoint.host}${params.more}`;
12 | return this.requestURL(url);
13 | }
14 |
--------------------------------------------------------------------------------
/src/resources/statement/getMoreStatements/getMoreStatements.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../XAPI";
2 | import { testEndpoint } from "../../../../test/constants";
3 |
4 | describe("statement resource", () => {
5 | beforeEach(() => {
6 | global.adapterFn.mockClear();
7 | global.adapterFn.mockResolvedValueOnce({
8 | headers: {
9 | "content-type": "application/json",
10 | },
11 | });
12 | });
13 |
14 | test("can get more statements", async () => {
15 | const xapi = new XAPI({
16 | endpoint: testEndpoint,
17 | adapter: global.adapter,
18 | });
19 | const endpoint = new URL(testEndpoint);
20 | const testMoreIrl = "?more=test-more-irl";
21 | await xapi.getMoreStatements({
22 | more: testMoreIrl,
23 | });
24 | expect(global.adapterFn).toHaveBeenCalledWith(
25 | expect.objectContaining({
26 | method: "GET",
27 | url: `${endpoint.protocol}//${endpoint.host}${testMoreIrl}`,
28 | })
29 | );
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/src/resources/statement/getStatement/GetStatementParams.ts:
--------------------------------------------------------------------------------
1 | import { StatementParamsBase } from "../StatementParamsBase";
2 |
3 | interface GetStatementParamsBase extends StatementParamsBase {
4 | /**
5 | * The UUID of the statement.
6 | */
7 | statementId: string;
8 | }
9 |
10 | export interface GetStatementParamsWithAttachments
11 | extends GetStatementParamsBase {
12 | attachments: true;
13 | }
14 |
15 | export interface GetStatementParamsWithoutAttachments
16 | extends GetStatementParamsBase {
17 | attachments?: false;
18 | }
19 |
20 | export type GetStatementParams =
21 | | GetStatementParamsWithoutAttachments
22 | | GetStatementParamsWithAttachments;
23 |
--------------------------------------------------------------------------------
/src/resources/statement/getStatement/getStatement.int.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | testStatement,
3 | testAttachment,
4 | testAttachmentArrayBuffer,
5 | testAttachmentContent,
6 | } from "../../../../test/constants";
7 | import { forEachLRS } from "../../../../test/getCredentials";
8 | import { testIf, isNode } from "../../../../test/jestUtils";
9 | import { Statement } from "..";
10 |
11 | forEachLRS((xapi) => {
12 | describe("statement resource", () => {
13 | describe("get statement", () => {
14 | test("can get a single statement", () => {
15 | return xapi
16 | .sendStatement({
17 | statement: testStatement,
18 | })
19 | .then((result) => {
20 | return xapi.getStatement({
21 | statementId: result.data[0],
22 | });
23 | })
24 | .then((result) => {
25 | return expect(result.data.id).toBeTruthy();
26 | });
27 | });
28 |
29 | testIf(!isNode())(
30 | "can get a statement with an embedded attachment",
31 | () => {
32 | const statement: Statement = Object.assign({}, testStatement);
33 | statement.attachments = [testAttachment];
34 | return xapi
35 | .sendStatement({
36 | statement: statement,
37 | attachments: [testAttachmentArrayBuffer],
38 | })
39 | .then((result) => {
40 | return xapi.getStatement({
41 | statementId: result.data[0],
42 | attachments: true,
43 | });
44 | })
45 | .then((response) => {
46 | const parts = response.data;
47 | const attachmentData = parts[1];
48 | return expect(attachmentData).toEqual(testAttachmentContent);
49 | });
50 | }
51 | );
52 | });
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/src/resources/statement/getStatement/getStatement.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../adapters";
2 | import { Resources } from "../../../constants";
3 | import XAPI from "../../../XAPI";
4 | import { StatementResponseWithAttachments, Statement } from "..";
5 | import {
6 | GetStatementParamsWithAttachments,
7 | GetStatementParamsWithoutAttachments,
8 | GetStatementParams,
9 | } from "./GetStatementParams";
10 |
11 | export function getStatement(
12 | this: XAPI,
13 | params: GetStatementParamsWithAttachments
14 | ): AdapterPromise;
15 |
16 | export function getStatement(
17 | this: XAPI,
18 | params: GetStatementParamsWithoutAttachments
19 | ): AdapterPromise;
20 |
21 | export function getStatement(
22 | this: XAPI,
23 | params: GetStatementParams
24 | ): AdapterPromise {
25 | return this.requestResource({
26 | resource: Resources.STATEMENT,
27 | queryParams: {
28 | statementId: params.statementId,
29 | ...(!!params.attachments && { attachments: params.attachments }),
30 | ...(!!params.format && { format: params.format }),
31 | },
32 | requestOptions: { useCacheBuster: params.useCacheBuster },
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/src/resources/statement/getStatement/getStatement.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../XAPI";
2 | import {
3 | testAttachmentContent,
4 | testEndpoint,
5 | testMultiPartData,
6 | testStatementWithEmbeddedAttachments,
7 | } from "../../../../test/constants";
8 | import { Resources } from "../../../constants";
9 |
10 | describe("statement resource", () => {
11 | beforeEach(() => {
12 | global.adapterFn.mockClear();
13 | global.adapterFn.mockResolvedValueOnce({
14 | headers: {
15 | "content-type": "application/json",
16 | },
17 | });
18 | });
19 |
20 | test("can get a single statement", async () => {
21 | const xapi = new XAPI({
22 | endpoint: testEndpoint,
23 | adapter: global.adapter,
24 | });
25 | const testStatementId = "test-statement-id";
26 | await xapi.getStatement({
27 | statementId: testStatementId,
28 | });
29 | expect(global.adapterFn).toHaveBeenCalledWith(
30 | expect.objectContaining({
31 | method: "GET",
32 | url: `${testEndpoint}${Resources.STATEMENT}?statementId=${testStatementId}`,
33 | })
34 | );
35 | });
36 |
37 | test("can get a single statement with attachments", async () => {
38 | jest.resetAllMocks();
39 | global.adapterFn.mockClear();
40 | global.adapterFn.mockResolvedValueOnce({
41 | headers: {
42 | "content-type": "multipart/mixed; boundary=",
43 | },
44 | data: testMultiPartData,
45 | });
46 | const xapi = new XAPI({
47 | endpoint: testEndpoint,
48 | adapter: global.adapter,
49 | });
50 | const testStatementId = "test-statement-id";
51 | const result = await xapi.getStatement({
52 | statementId: testStatementId,
53 | attachments: true,
54 | });
55 | expect(global.adapterFn).toHaveBeenCalledWith(
56 | expect.objectContaining({
57 | method: "GET",
58 | url: `${testEndpoint}${Resources.STATEMENT}?statementId=${testStatementId}&attachments=true`,
59 | })
60 | );
61 | expect(result.data[0]).toEqual(testStatementWithEmbeddedAttachments);
62 | expect(result.data[1]).toEqual(testAttachmentContent);
63 | });
64 |
65 | test("can get a single statement with chosen format", async () => {
66 | const xapi = new XAPI({
67 | endpoint: testEndpoint,
68 | adapter: global.adapter,
69 | });
70 | const testStatementId = "test-statement-id";
71 | await xapi.getStatement({
72 | statementId: testStatementId,
73 | format: "canonical",
74 | });
75 | expect(global.adapterFn).toHaveBeenCalledWith(
76 | expect.objectContaining({
77 | method: "GET",
78 | url: `${testEndpoint}${Resources.STATEMENT}?statementId=${testStatementId}&format=canonical`,
79 | })
80 | );
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/src/resources/statement/getStatements/GetStatementsParams.ts:
--------------------------------------------------------------------------------
1 | import { Agent } from "../../../XAPI";
2 | import { Timestamp } from "..";
3 | import { StatementParamsBase } from "../StatementParamsBase";
4 |
5 | interface GetStatementsParamsBase extends StatementParamsBase {
6 | /**
7 | * JSON encoded object containing an IFI to match an agent or group.
8 | */
9 | agent?: Agent;
10 | /**
11 | * String matching the statement’s verb identifier.
12 | */
13 | verb?: string;
14 | /**
15 | * String matching the statement’s object identifier.
16 | */
17 | activity?: string;
18 | /**
19 | * String matching the statement’s registration from the context.
20 | */
21 | registration?: string;
22 | /**
23 | * Applies the activity filter to any activity in the statement when `true`. Defaults to `false`.
24 | */
25 | related_activities?: boolean;
26 | /**
27 | * Applies the activity filter to any agent/group in the statement when `true`. Defaults to `false`.
28 | */
29 | related_agents?: boolean;
30 | /**
31 | * String that returns statements stored after the given timestamp (exclusive).
32 | */
33 | since?: Timestamp;
34 | /**
35 | * String that returns statements stored before the given timestamp (inclusive).
36 | */
37 | until?: Timestamp;
38 | /**
39 | * Number of statements to return. Defaults to `0` which returns the maximum the server will allow.
40 | */
41 | limit?: number;
42 | /**
43 | * Boolean determining if the statements should be returned in ascending stored order. Defaults to `false`.
44 | */
45 | ascending?: boolean;
46 | }
47 |
48 | export interface GetStatementsParamsWithAttachments
49 | extends GetStatementsParamsBase {
50 | attachments: true;
51 | }
52 |
53 | export interface GetStatementsParamsWithoutAttachments
54 | extends GetStatementsParamsBase {
55 | attachments?: false;
56 | }
57 |
58 | export type GetStatementsParams =
59 | | GetStatementsParamsWithAttachments
60 | | GetStatementsParamsWithoutAttachments;
61 |
--------------------------------------------------------------------------------
/src/resources/statement/getStatements/getStatements.int.test.ts:
--------------------------------------------------------------------------------
1 | import { testAgent } from "../../../../test/constants";
2 | import { forEachLRS } from "../../../../test/getCredentials";
3 |
4 | forEachLRS((xapi) => {
5 | describe("statement resource", () => {
6 | describe("get statements", () => {
7 | test("can get multiple statements", () => {
8 | return xapi.getStatements().then((result) => {
9 | return expect(result.data.statements).toBeTruthy();
10 | });
11 | });
12 | });
13 |
14 | test("can get multiple statements with attachments", () => {
15 | return xapi
16 | .getStatements({
17 | attachments: true,
18 | limit: 2,
19 | })
20 | .then((result) => {
21 | const statementsResponse = result.data[0];
22 | return expect(statementsResponse.statements).toHaveLength(2);
23 | });
24 | });
25 |
26 | test("can query for statements using the actor property", () => {
27 | return xapi
28 | .getStatements({
29 | agent: testAgent,
30 | })
31 | .then((result) => {
32 | return expect(result.data.statements).toBeTruthy();
33 | });
34 | });
35 |
36 | test("can query a single statement using the limit property", () => {
37 | return xapi
38 | .getStatements({
39 | limit: 1,
40 | })
41 | .then((result) => {
42 | return expect(result.data.statements).toHaveLength(1);
43 | });
44 | });
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/src/resources/statement/getStatements/getStatements.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../adapters";
2 | import { Resources } from "../../../constants";
3 | import XAPI from "../../../XAPI";
4 | import { StatementsResponseWithAttachments, StatementsResponse } from "..";
5 | import {
6 | GetStatementsParams,
7 | GetStatementsParamsWithAttachments,
8 | GetStatementsParamsWithoutAttachments,
9 | } from "./GetStatementsParams";
10 |
11 | export function getStatements(
12 | this: XAPI,
13 | params: GetStatementsParamsWithAttachments
14 | ): AdapterPromise;
15 |
16 | export function getStatements(
17 | this: XAPI,
18 | params?: GetStatementsParamsWithoutAttachments
19 | ): AdapterPromise;
20 |
21 | export function getStatements(
22 | this: XAPI,
23 | params?: GetStatementsParams
24 | ): AdapterPromise {
25 | return this.requestResource({
26 | resource: Resources.STATEMENT,
27 | queryParams: {
28 | ...(!!params?.activity && { activity: params.activity }),
29 | ...(!!params?.agent && { agent: params.agent }),
30 | ...(!!params?.ascending && { ascending: params.ascending }),
31 | ...(!!params?.attachments && { attachments: params.attachments }),
32 | ...(!!params?.format && { format: params.format }),
33 | ...(!!params?.limit && { limit: params.limit }),
34 | ...(!!params?.registration && { registration: params.registration }),
35 | ...(!!params?.related_activities && {
36 | related_activities: params.related_activities,
37 | }),
38 | ...(!!params?.related_agents && {
39 | related_agents: params.related_agents,
40 | }),
41 | ...(!!params?.since && { since: params.since }),
42 | ...(!!params?.until && { until: params.until }),
43 | ...(!!params?.verb && { verb: params.verb }),
44 | },
45 | requestOptions: { useCacheBuster: params?.useCacheBuster },
46 | });
47 | }
48 |
--------------------------------------------------------------------------------
/src/resources/statement/getStatements/getStatements.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../XAPI";
2 | import {
3 | testActivity,
4 | testAgent,
5 | testEndpoint,
6 | testVerb,
7 | } from "../../../../test/constants";
8 | import { Resources } from "../../../constants";
9 |
10 | describe("statement resource", () => {
11 | beforeEach(() => {
12 | global.adapterFn.mockClear();
13 | global.adapterFn.mockResolvedValueOnce({
14 | headers: {
15 | "content-type": "application/json",
16 | },
17 | });
18 | });
19 |
20 | test("can get multiple statements", async () => {
21 | const xapi = new XAPI({
22 | endpoint: testEndpoint,
23 | adapter: global.adapter,
24 | });
25 | await xapi.getStatements();
26 | expect(global.adapterFn).toHaveBeenCalledWith(
27 | expect.objectContaining({
28 | method: "GET",
29 | url: `${testEndpoint}${Resources.STATEMENT}`,
30 | })
31 | );
32 | });
33 |
34 | test("can get multiple statements with attachments", async () => {
35 | const xapi = new XAPI({
36 | endpoint: testEndpoint,
37 | adapter: global.adapter,
38 | });
39 | await xapi.getStatements({
40 | attachments: true,
41 | });
42 | expect(global.adapterFn).toHaveBeenCalledWith(
43 | expect.objectContaining({
44 | method: "GET",
45 | url: `${testEndpoint}${Resources.STATEMENT}?attachments=true`,
46 | })
47 | );
48 | });
49 |
50 | test("can get multiple statements with all query parameters", async () => {
51 | const xapi = new XAPI({
52 | endpoint: testEndpoint,
53 | adapter: global.adapter,
54 | });
55 | const testRegistration = "test-registration";
56 | const since = new Date();
57 | since.setDate(since.getDate() - 1); // yesterday
58 | await xapi.getStatements({
59 | activity: testActivity.id,
60 | agent: testAgent,
61 | ascending: true,
62 | attachments: true,
63 | format: "ids",
64 | limit: 10,
65 | registration: testRegistration,
66 | related_activities: true,
67 | related_agents: true,
68 | since: since.toISOString(),
69 | until: since.toISOString(),
70 | verb: testVerb.id,
71 | });
72 | expect(global.adapterFn).toHaveBeenCalledWith(
73 | expect.objectContaining({
74 | method: "GET",
75 | url: `${testEndpoint}${
76 | Resources.STATEMENT
77 | }?activity=${encodeURIComponent(
78 | testActivity.id
79 | )}&agent=${encodeURIComponent(
80 | JSON.stringify(testAgent)
81 | )}&ascending=true&attachments=true&format=ids&limit=10®istration=${testRegistration}&related_activities=true&related_agents=true&since=${encodeURIComponent(
82 | since.toISOString()
83 | )}&until=${encodeURIComponent(
84 | since.toISOString()
85 | )}&verb=${encodeURIComponent(testVerb.id)}`,
86 | })
87 | );
88 | });
89 |
90 | test("can get multiple statements with cache buster", async () => {
91 | const xapi = new XAPI({
92 | endpoint: testEndpoint,
93 | adapter: global.adapter,
94 | });
95 | await xapi.getStatements({
96 | useCacheBuster: true,
97 | });
98 | expect(global.adapterFn).toHaveBeenCalledWith(
99 | expect.objectContaining({
100 | method: "GET",
101 | url: expect.stringContaining(
102 | `${testEndpoint}${Resources.STATEMENT}?cachebuster=`
103 | ),
104 | })
105 | );
106 | });
107 | });
108 |
--------------------------------------------------------------------------------
/src/resources/statement/getVoidedStatement/GetVoidedStatementParams.ts:
--------------------------------------------------------------------------------
1 | import { StatementParamsBase } from "../StatementParamsBase";
2 |
3 | interface GetVoidedStatementParamsBase extends StatementParamsBase {
4 | /**
5 | * The original UUID of the statement before it was voided.
6 | */
7 | voidedStatementId: string;
8 | }
9 |
10 | export interface GetVoidedStatementParamsWithAttachments
11 | extends GetVoidedStatementParamsBase {
12 | attachments: true;
13 | }
14 |
15 | export interface GetVoidedStatementParamsWithoutAttachments
16 | extends GetVoidedStatementParamsBase {
17 | attachments?: false;
18 | }
19 |
20 | export type GetVoidedStatementParams =
21 | | GetVoidedStatementParamsWithAttachments
22 | | GetVoidedStatementParamsWithoutAttachments;
23 |
--------------------------------------------------------------------------------
/src/resources/statement/getVoidedStatement/getVoidedStatement.int.test.ts:
--------------------------------------------------------------------------------
1 | import { testStatement, testAgent } from "../../../../test/constants";
2 | import { forEachLRS } from "../../../../test/getCredentials";
3 |
4 | forEachLRS((xapi) => {
5 | describe("statement resource", () => {
6 | describe("void statement", () => {
7 | test("can get a voided statement", () => {
8 | let statementId: string;
9 | return xapi
10 | .sendStatement({
11 | statement: testStatement,
12 | })
13 | .then((result) => {
14 | statementId = result.data[0];
15 | return xapi.voidStatement({
16 | actor: testAgent,
17 | statementId: statementId,
18 | });
19 | })
20 | .then(() => {
21 | return xapi.getVoidedStatement({
22 | voidedStatementId: statementId,
23 | });
24 | })
25 | .then((result) => {
26 | return expect(result.data).toHaveProperty("id");
27 | });
28 | });
29 | });
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/src/resources/statement/getVoidedStatement/getVoidedStatement.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../adapters";
2 | import { Resources } from "../../../constants";
3 | import XAPI from "../../../XAPI";
4 | import { StatementResponseWithAttachments, Statement } from "..";
5 | import {
6 | GetVoidedStatementParams,
7 | GetVoidedStatementParamsWithAttachments,
8 | GetVoidedStatementParamsWithoutAttachments,
9 | } from "./GetVoidedStatementParams";
10 |
11 | export function getVoidedStatement(
12 | this: XAPI,
13 | params: GetVoidedStatementParamsWithAttachments
14 | ): AdapterPromise;
15 |
16 | export function getVoidedStatement(
17 | this: XAPI,
18 | params: GetVoidedStatementParamsWithoutAttachments
19 | ): AdapterPromise;
20 |
21 | export function getVoidedStatement(
22 | this: XAPI,
23 | params: GetVoidedStatementParams
24 | ): AdapterPromise {
25 | return this.requestResource({
26 | resource: Resources.STATEMENT,
27 | queryParams: {
28 | voidedStatementId: params.voidedStatementId,
29 | ...(!!params.attachments && { attachments: params.attachments }),
30 | ...(!!params.format && { format: params.format }),
31 | },
32 | requestOptions: { useCacheBuster: params.useCacheBuster },
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/src/resources/statement/getVoidedStatement/getVoidedStatement.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../XAPI";
2 | import { testEndpoint } from "../../../../test/constants";
3 | import { Resources } from "../../../constants";
4 |
5 | describe("statement resource", () => {
6 | beforeEach(() => {
7 | global.adapterFn.mockClear();
8 | global.adapterFn.mockResolvedValueOnce({
9 | headers: {
10 | "content-type": "application/json",
11 | },
12 | });
13 | });
14 |
15 | test("can get a voided statement", async () => {
16 | const xapi = new XAPI({
17 | endpoint: testEndpoint,
18 | adapter: global.adapter,
19 | });
20 | const testStatementId = "test-statement-id";
21 | await xapi.getVoidedStatement({
22 | voidedStatementId: testStatementId,
23 | });
24 | expect(global.adapterFn).toHaveBeenCalledWith(
25 | expect.objectContaining({
26 | method: "GET",
27 | url: `${testEndpoint}${Resources.STATEMENT}?voidedStatementId=${testStatementId}`,
28 | })
29 | );
30 | });
31 |
32 | test("can get a voided statement with attachments", async () => {
33 | const xapi = new XAPI({
34 | endpoint: testEndpoint,
35 | adapter: global.adapter,
36 | });
37 | const testStatementId = "test-statement-id";
38 | await xapi.getVoidedStatement({
39 | voidedStatementId: testStatementId,
40 | attachments: true,
41 | });
42 | expect(global.adapterFn).toHaveBeenCalledWith(
43 | expect.objectContaining({
44 | method: "GET",
45 | url: `${testEndpoint}${Resources.STATEMENT}?voidedStatementId=${testStatementId}&attachments=true`,
46 | })
47 | );
48 | });
49 |
50 | test("can get a voided statement with chosen format", async () => {
51 | const xapi = new XAPI({
52 | endpoint: testEndpoint,
53 | adapter: global.adapter,
54 | });
55 | const testStatementId = "test-statement-id";
56 | await xapi.getVoidedStatement({
57 | voidedStatementId: testStatementId,
58 | format: "canonical",
59 | });
60 | expect(global.adapterFn).toHaveBeenCalledWith(
61 | expect.objectContaining({
62 | method: "GET",
63 | url: `${testEndpoint}${Resources.STATEMENT}?voidedStatementId=${testStatementId}&format=canonical`,
64 | })
65 | );
66 | });
67 |
68 | test("can get a voided statement with cache buster", async () => {
69 | const xapi = new XAPI({
70 | endpoint: testEndpoint,
71 | adapter: global.adapter,
72 | });
73 | const testStatementId = "test-statement-id";
74 | await xapi.getVoidedStatement({
75 | voidedStatementId: testStatementId,
76 | useCacheBuster: true,
77 | });
78 | expect(global.adapterFn).toHaveBeenCalledWith(
79 | expect.objectContaining({
80 | method: "GET",
81 | url: expect.stringContaining(
82 | `${testEndpoint}${Resources.STATEMENT}?voidedStatementId=${testStatementId}&cachebuster=`
83 | ),
84 | })
85 | );
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/src/resources/statement/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Actor";
2 | export * from "./Agent";
3 | export * from "./AnonymousGroup";
4 | export * from "./Attachment";
5 | export * from "./AttachmentUsage";
6 | export * from "./Context";
7 | export * from "./ContextActivity";
8 | export * from "./Extensions";
9 | export * from "./Group";
10 | export * from "./IdentifiedGroup";
11 | export * from "./InteractionActivity";
12 | export * from "./InteractionActivityDefinition";
13 | export * from "./InverseFunctionalIdentifier";
14 | export * from "./LanguageMap";
15 | export * from "./ObjectiveActivity";
16 | export * from "./ObjectiveActivityDefinition";
17 | export * from "./Part";
18 | export * from "./Result";
19 | export * from "./RFC5646LanguageCodes";
20 | export * from "./Statement";
21 | export * from "./StatementObject";
22 | export * from "./StatementRef";
23 | export * from "./StatementResponseWithAttachments";
24 | export * from "./StatementsResponse";
25 | export * from "./StatementsResponseWithAttachments";
26 | export * from "./SubStatement";
27 | export * from "./Timestamp";
28 | export * from "./Verb";
29 |
--------------------------------------------------------------------------------
/src/resources/statement/sendStatement/SendStatementParams.ts:
--------------------------------------------------------------------------------
1 | import { Statement } from "..";
2 |
3 | export interface SendStatementParams {
4 | statement: Statement;
5 | attachments?: ArrayBuffer[];
6 | }
7 |
--------------------------------------------------------------------------------
/src/resources/statement/sendStatement/sendStatement.int.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | testStatement,
3 | returnTestStatementWithRemoteAttachment,
4 | testStatementWithEmbeddedAttachments,
5 | testAttachmentArrayBuffer,
6 | } from "../../../../test/constants";
7 | import { forEachLRS } from "../../../../test/getCredentials";
8 | import { testIf, isNode } from "../../../../test/jestUtils";
9 |
10 | forEachLRS((xapi) => {
11 | describe("statement resource", () => {
12 | describe("send statement", () => {
13 | test("can send a statement", () => {
14 | return xapi
15 | .sendStatement({
16 | statement: testStatement,
17 | })
18 | .then((result) => {
19 | return expect(result.data).toHaveLength(1);
20 | });
21 | });
22 |
23 | test("can send a statement with a remote attachment", () => {
24 | return returnTestStatementWithRemoteAttachment()
25 | .then((testStatementWithRemoteAttachment) => {
26 | return xapi.sendStatement({
27 | statement: testStatementWithRemoteAttachment,
28 | });
29 | })
30 | .then((result) => {
31 | return expect(result.data).toHaveLength(1);
32 | });
33 | });
34 |
35 | testIf(!isNode())(
36 | "can send a statement with an embedded attachment",
37 | () => {
38 | return xapi
39 | .sendStatement({
40 | statement: testStatementWithEmbeddedAttachments,
41 | attachments: [testAttachmentArrayBuffer],
42 | })
43 | .then((result) => {
44 | return expect(result.data).toHaveLength(1);
45 | });
46 | }
47 | );
48 | });
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/src/resources/statement/sendStatement/sendStatement.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../adapters";
2 | import { Resources } from "../../../constants";
3 | import { createMultiPart, MultiPart } from "../../../internal/multiPart";
4 | import XAPI from "../../../XAPI";
5 | import { SendStatementParams } from "./SendStatementParams";
6 |
7 | export function sendStatement(
8 | this: XAPI,
9 | params: SendStatementParams
10 | ): AdapterPromise {
11 | const hasAttachments = params.attachments?.length;
12 | if (hasAttachments) {
13 | const multiPart: MultiPart = createMultiPart(
14 | params.statement,
15 | params.attachments
16 | );
17 | return this.requestResource({
18 | resource: Resources.STATEMENT,
19 | requestConfig: {
20 | method: "POST",
21 | headers: multiPart.header,
22 | data: multiPart.blob,
23 | },
24 | });
25 | } else {
26 | return this.requestResource({
27 | resource: Resources.STATEMENT,
28 | requestConfig: {
29 | method: "POST",
30 | data: params.statement,
31 | },
32 | });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/resources/statement/sendStatement/sendStatement.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../XAPI";
2 | import {
3 | testAttachmentArrayBuffer,
4 | testEndpoint,
5 | testStatement,
6 | testStatementWithEmbeddedAttachments,
7 | } from "../../../../test/constants";
8 | import { Resources } from "../../../constants";
9 | import { createMultiPart } from "../../../internal/multiPart";
10 | import { testIf, isNode } from "../../../../test/jestUtils";
11 |
12 | describe("statement resource", () => {
13 | beforeEach(() => {
14 | global.adapterFn.mockClear();
15 | global.adapterFn.mockResolvedValueOnce({
16 | headers: {
17 | "content-type": "application/json",
18 | },
19 | });
20 | });
21 |
22 | test("can send a statement", async () => {
23 | const xapi = new XAPI({
24 | endpoint: testEndpoint,
25 | adapter: global.adapter,
26 | });
27 | await xapi.sendStatement({
28 | statement: testStatement,
29 | });
30 | expect(global.adapterFn).toHaveBeenCalledWith(
31 | expect.objectContaining({
32 | method: "POST",
33 | url: `${testEndpoint}${Resources.STATEMENT}`,
34 | data: testStatement,
35 | })
36 | );
37 | });
38 |
39 | testIf(!isNode())(
40 | "can send a statement with embedded attachments",
41 | async () => {
42 | const xapi = new XAPI({
43 | endpoint: testEndpoint,
44 | adapter: global.adapter,
45 | });
46 | await xapi.sendStatement({
47 | statement: testStatementWithEmbeddedAttachments,
48 | attachments: [testAttachmentArrayBuffer],
49 | });
50 | expect(global.adapterFn).toHaveBeenCalledWith(
51 | expect.objectContaining({
52 | method: "POST",
53 | headers: expect.objectContaining({
54 | ["Content-Type"]: expect.stringContaining(
55 | "multipart/mixed; boundary="
56 | ),
57 | }),
58 | url: `${testEndpoint}${Resources.STATEMENT}`,
59 | data: createMultiPart(testStatementWithEmbeddedAttachments, [
60 | testAttachmentArrayBuffer,
61 | ]).blob,
62 | })
63 | );
64 | }
65 | );
66 | });
67 |
--------------------------------------------------------------------------------
/src/resources/statement/sendStatements/SendStatementsParams.ts:
--------------------------------------------------------------------------------
1 | import { Statement } from "..";
2 |
3 | export interface SendStatementsParams {
4 | statements: Statement[];
5 | attachments?: ArrayBuffer[];
6 | }
7 |
--------------------------------------------------------------------------------
/src/resources/statement/sendStatements/sendStatements.int.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | testStatement,
3 | testStatementWithEmbeddedAttachments,
4 | testAttachmentArrayBuffer,
5 | } from "../../../../test/constants";
6 | import { forEachLRS } from "../../../../test/getCredentials";
7 | import { testIf, isNode } from "../../../../test/jestUtils";
8 |
9 | forEachLRS((xapi) => {
10 | describe("statement resource", () => {
11 | describe("send statements", () => {
12 | test("can send multiple statements", () => {
13 | return xapi
14 | .sendStatements({
15 | statements: [testStatement, testStatement],
16 | })
17 | .then((result) => {
18 | return expect(result.data).toHaveLength(2);
19 | });
20 | });
21 |
22 | testIf(!isNode())(
23 | "can send multiple statements with embedded attachments",
24 | () => {
25 | return xapi
26 | .sendStatements({
27 | statements: [
28 | testStatementWithEmbeddedAttachments,
29 | testStatementWithEmbeddedAttachments,
30 | ],
31 | attachments: [
32 | testAttachmentArrayBuffer,
33 | testAttachmentArrayBuffer,
34 | ],
35 | })
36 | .then((result) => {
37 | return expect(result.data).toHaveLength(2);
38 | });
39 | }
40 | );
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/src/resources/statement/sendStatements/sendStatements.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../adapters";
2 | import { Resources } from "../../../constants";
3 | import { createMultiPart, MultiPart } from "../../../internal/multiPart";
4 | import XAPI from "../../../XAPI";
5 | import { SendStatementsParams } from "./SendStatementsParams";
6 |
7 | export function sendStatements(
8 | this: XAPI,
9 | params: SendStatementsParams
10 | ): AdapterPromise {
11 | const hasAttachments = params.attachments?.length;
12 | if (hasAttachments) {
13 | const multiPart: MultiPart = createMultiPart(
14 | params.statements,
15 | params.attachments
16 | );
17 | return this.requestResource({
18 | resource: Resources.STATEMENT,
19 | requestConfig: {
20 | method: "POST",
21 | headers: multiPart.header,
22 | data: multiPart.blob,
23 | },
24 | });
25 | } else {
26 | return this.requestResource({
27 | resource: Resources.STATEMENT,
28 | requestConfig: {
29 | method: "POST",
30 | data: params.statements,
31 | },
32 | });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/resources/statement/sendStatements/sendStatements.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../XAPI";
2 | import {
3 | testAttachmentArrayBuffer,
4 | testEndpoint,
5 | testStatement,
6 | testStatementWithEmbeddedAttachments,
7 | } from "../../../../test/constants";
8 | import { Resources } from "../../../constants";
9 | import { createMultiPart } from "../../../internal/multiPart";
10 | import { testIf, isNode } from "../../../../test/jestUtils";
11 |
12 | describe("statement resource", () => {
13 | beforeEach(() => {
14 | global.adapterFn.mockClear();
15 | global.adapterFn.mockResolvedValueOnce({
16 | headers: {
17 | "content-type": "application/json",
18 | },
19 | });
20 | });
21 |
22 | test("can send multiple statements", async () => {
23 | const xapi = new XAPI({
24 | endpoint: testEndpoint,
25 | adapter: global.adapter,
26 | });
27 | await xapi.sendStatements({
28 | statements: [testStatement, testStatement],
29 | });
30 | expect(global.adapterFn).toHaveBeenCalledWith(
31 | expect.objectContaining({
32 | method: "POST",
33 | url: `${testEndpoint}${Resources.STATEMENT}`,
34 | data: [testStatement, testStatement],
35 | })
36 | );
37 | });
38 |
39 | testIf(!isNode())(
40 | "can send multiple statements with embedded attachments",
41 | async () => {
42 | const xapi = new XAPI({
43 | endpoint: testEndpoint,
44 | adapter: global.adapter,
45 | });
46 | await xapi.sendStatements({
47 | statements: [
48 | testStatementWithEmbeddedAttachments,
49 | testStatementWithEmbeddedAttachments,
50 | ],
51 | attachments: [testAttachmentArrayBuffer, testAttachmentArrayBuffer],
52 | });
53 | expect(global.adapterFn).toHaveBeenCalledWith(
54 | expect.objectContaining({
55 | method: "POST",
56 | headers: expect.objectContaining({
57 | ["Content-Type"]: expect.stringContaining(
58 | "multipart/mixed; boundary="
59 | ),
60 | }),
61 | url: `${testEndpoint}${Resources.STATEMENT}`,
62 | data: createMultiPart(
63 | [
64 | testStatementWithEmbeddedAttachments,
65 | testStatementWithEmbeddedAttachments,
66 | ],
67 | [testAttachmentArrayBuffer, testAttachmentArrayBuffer]
68 | ).blob,
69 | })
70 | );
71 | }
72 | );
73 | });
74 |
--------------------------------------------------------------------------------
/src/resources/statement/voidStatement/VoidStatementParams.ts:
--------------------------------------------------------------------------------
1 | import { Actor } from "../../../XAPI";
2 |
3 | export interface VoidStatementParams {
4 | actor: Actor;
5 | statementId: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/resources/statement/voidStatement/voidStatement.int.test.ts:
--------------------------------------------------------------------------------
1 | import { testStatement, testAgent } from "../../../../test/constants";
2 | import { forEachLRS } from "../../../../test/getCredentials";
3 |
4 | forEachLRS((xapi) => {
5 | describe("statement resource", () => {
6 | describe("void statement", () => {
7 | test("can void a single statement", () => {
8 | return xapi
9 | .sendStatement({
10 | statement: testStatement,
11 | })
12 | .then((result) => {
13 | return xapi.voidStatement({
14 | actor: testAgent,
15 | statementId: result.data[0],
16 | });
17 | })
18 | .then((result) => {
19 | return expect(result.data).toHaveLength(1);
20 | });
21 | });
22 | });
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/resources/statement/voidStatement/voidStatement.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../adapters";
2 | import { Verbs } from "../../../constants";
3 | import XAPI from "../../../XAPI";
4 | import { Statement } from "..";
5 | import { VoidStatementParams } from "./VoidStatementParams";
6 |
7 | export function voidStatement(
8 | this: XAPI,
9 | params: VoidStatementParams
10 | ): AdapterPromise {
11 | const voidStatement: Statement = {
12 | actor: params.actor,
13 | verb: Verbs.VOIDED,
14 | object: {
15 | objectType: "StatementRef",
16 | id: params.statementId,
17 | },
18 | };
19 | return this.sendStatement({
20 | statement: voidStatement,
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/src/resources/statement/voidStatement/voidStatement.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../XAPI";
2 | import {
3 | testAgent,
4 | testEndpoint,
5 | testStatement,
6 | } from "../../../../test/constants";
7 | import { Resources, Verbs } from "../../../constants";
8 | import { Statement } from "..";
9 |
10 | describe("statement resource", () => {
11 | beforeEach(() => {
12 | global.adapterFn.mockClear();
13 | global.adapterFn.mockResolvedValueOnce({
14 | headers: {
15 | "content-type": "application/json",
16 | },
17 | });
18 | });
19 |
20 | test("can void a statement", async () => {
21 | const xapi = new XAPI({
22 | endpoint: testEndpoint,
23 | adapter: global.adapter,
24 | });
25 | await xapi.voidStatement({
26 | actor: testAgent,
27 | statementId: testStatement.id,
28 | });
29 | expect(global.adapterFn).toHaveBeenCalledWith(
30 | expect.objectContaining({
31 | method: "POST",
32 | url: `${testEndpoint}${Resources.STATEMENT}`,
33 | data: {
34 | actor: testAgent,
35 | verb: Verbs.VOIDED,
36 | object: {
37 | objectType: "StatementRef",
38 | id: testStatement.id,
39 | },
40 | } as Statement,
41 | })
42 | );
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/src/resources/statement/voidStatements/VoidStatementsParams.ts:
--------------------------------------------------------------------------------
1 | import { Actor } from "../../../XAPI";
2 |
3 | export interface VoidStatementsParams {
4 | actor: Actor;
5 | statementIds: string[];
6 | }
7 |
--------------------------------------------------------------------------------
/src/resources/statement/voidStatements/voidStatements.int.test.ts:
--------------------------------------------------------------------------------
1 | import { testStatement, testAgent } from "../../../../test/constants";
2 | import { forEachLRS } from "../../../../test/getCredentials";
3 |
4 | forEachLRS((xapi) => {
5 | describe("statement resource", () => {
6 | describe("void statements", () => {
7 | test("can void multiple statements", () => {
8 | return xapi
9 | .sendStatements({
10 | statements: [testStatement, testStatement],
11 | })
12 | .then((result) => {
13 | return xapi.voidStatements({
14 | actor: testAgent,
15 | statementIds: result.data,
16 | });
17 | })
18 | .then((result) => {
19 | return expect(result.data).toHaveLength(2);
20 | });
21 | });
22 | });
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/resources/statement/voidStatements/voidStatements.ts:
--------------------------------------------------------------------------------
1 | import { AdapterPromise } from "../../../adapters";
2 | import { Verbs } from "../../../constants";
3 | import XAPI from "../../../XAPI";
4 | import { Statement } from "..";
5 | import { VoidStatementsParams } from "./VoidStatementsParams";
6 |
7 | export function voidStatements(
8 | this: XAPI,
9 | params: VoidStatementsParams
10 | ): AdapterPromise {
11 | const voidStatements: Statement[] = params.statementIds.map((statementId) => {
12 | return {
13 | actor: params.actor,
14 | verb: Verbs.VOIDED,
15 | object: {
16 | objectType: "StatementRef",
17 | id: statementId,
18 | },
19 | };
20 | });
21 | return this.sendStatements({
22 | statements: voidStatements,
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/src/resources/statement/voidStatements/voidStatements.unit.test.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../../../XAPI";
2 | import {
3 | testAgent,
4 | testEndpoint,
5 | testStatement,
6 | testStatementWithEmbeddedAttachments,
7 | } from "../../../../test/constants";
8 | import { Resources, Verbs } from "../../../constants";
9 | import { Statement } from "..";
10 |
11 | describe("statement resource", () => {
12 | beforeEach(() => {
13 | global.adapterFn.mockClear();
14 | global.adapterFn.mockResolvedValueOnce({
15 | headers: {
16 | "content-type": "application/json",
17 | },
18 | });
19 | });
20 |
21 | test("can void multiple statements", async () => {
22 | const xapi = new XAPI({
23 | endpoint: testEndpoint,
24 | adapter: global.adapter,
25 | });
26 | await xapi.voidStatements({
27 | actor: testAgent,
28 | statementIds: [testStatement.id, testStatementWithEmbeddedAttachments.id],
29 | });
30 | expect(global.adapterFn).toHaveBeenCalledWith(
31 | expect.objectContaining({
32 | method: "POST",
33 | url: `${testEndpoint}${Resources.STATEMENT}`,
34 | data: [
35 | {
36 | actor: testAgent,
37 | verb: Verbs.VOIDED,
38 | object: {
39 | objectType: "StatementRef",
40 | id: testStatement.id,
41 | },
42 | },
43 | {
44 | actor: testAgent,
45 | verb: Verbs.VOIDED,
46 | object: {
47 | objectType: "StatementRef",
48 | id: testStatementWithEmbeddedAttachments.id,
49 | },
50 | },
51 | ] as Statement[],
52 | })
53 | );
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/test/arrayBufferToWordArray.ts:
--------------------------------------------------------------------------------
1 | import CryptoJS from "crypto-js";
2 |
3 | export function arrayBufferToWordArray(
4 | ab: ArrayBuffer
5 | ): CryptoJS.lib.WordArray {
6 | const i8a = new Uint8Array(ab);
7 | const a: number[] = [];
8 | for (let i = 0; i < i8a.length; i += 4) {
9 | a.push(
10 | (i8a[i] << 24) | (i8a[i + 1] << 16) | (i8a[i + 2] << 8) | i8a[i + 3]
11 | );
12 | }
13 | return CryptoJS.lib.WordArray.create(a, i8a.length);
14 | }
15 |
--------------------------------------------------------------------------------
/test/getCredentials.ts:
--------------------------------------------------------------------------------
1 | import XAPI from "../src/XAPI";
2 |
3 | interface Credential {
4 | endpoint: string;
5 | username: string;
6 | password: string;
7 | }
8 |
9 | function getLRSCredentialsArray(): Credential[] {
10 | return JSON.parse(process.env.LRS_CREDENTIALS_ARRAY);
11 | }
12 |
13 | export function forEachLRS(
14 | callbackfn: (xapi: XAPI, credential: Credential) => void
15 | ): void {
16 | const credentials = getLRSCredentialsArray();
17 | credentials.forEach((credential) => {
18 | describe(`LRS: ${credential.endpoint}`, () => {
19 | const auth: string = XAPI.toBasicAuth(
20 | credential.username,
21 | credential.password
22 | );
23 | const xapi: XAPI = new XAPI({
24 | endpoint: credential.endpoint,
25 | auth: auth,
26 | adapter: global.adapter,
27 | });
28 | callbackfn(xapi, credential);
29 | });
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/test/jestUtils.ts:
--------------------------------------------------------------------------------
1 | const testIf = (condition: boolean): jest.It => (condition ? test : test.skip);
2 |
3 | const isNode = (): boolean => typeof window === "undefined";
4 |
5 | // @ts-expect-error EdgeRuntime is not present in local environment
6 | const isEdgeRuntime = (): boolean => typeof EdgeRuntime !== "undefined";
7 |
8 | export { testIf, isNode, isEdgeRuntime };
9 |
--------------------------------------------------------------------------------
/test/mockAxios.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | jest.mock("axios");
4 | const mockedAxios = axios as jest.Mocked;
5 |
6 | mockedAxios.request.mockImplementation(() =>
7 | Promise.resolve({
8 | headers: {
9 | "content-type": "application/json",
10 | },
11 | })
12 | );
13 |
--------------------------------------------------------------------------------
/test/mockFetch.ts:
--------------------------------------------------------------------------------
1 | global.fetch = jest.fn(() =>
2 | Promise.resolve({
3 | json: () =>
4 | Promise.resolve({
5 | headers: {
6 | "content-type": "application/json",
7 | },
8 | }),
9 | text: () => Promise.resolve(""),
10 | ok: true,
11 | headers: new Headers(),
12 | } as Response)
13 | );
14 |
--------------------------------------------------------------------------------
/test/polyfillFetch.ts:
--------------------------------------------------------------------------------
1 | import "whatwg-fetch";
2 |
--------------------------------------------------------------------------------
/test/setupAxios.ts:
--------------------------------------------------------------------------------
1 | import * as axiosAdapter from "../src/adapters/axiosAdapter";
2 |
3 | global.adapterFn = jest.spyOn(axiosAdapter, "default");
4 |
--------------------------------------------------------------------------------
/test/setupFetch.ts:
--------------------------------------------------------------------------------
1 | import * as fetchAdapter from "../src/adapters/fetchAdapter";
2 |
3 | global.adapter = "fetch";
4 | global.adapterFn = jest.spyOn(fetchAdapter, "default");
5 |
--------------------------------------------------------------------------------
/test/setupInt.ts:
--------------------------------------------------------------------------------
1 | beforeEach(() => {
2 | // Add artificial delay between integration tests to avoid rate limit in SCORM Cloud
3 | return new Promise((resolve) => setTimeout(() => resolve(null), 250));
4 | });
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "allowSyntheticDefaultImports": true,
5 | "moduleResolution": "node",
6 | "target": "es2015",
7 | "module": "es2015",
8 | "declarationDir": "./dist/types",
9 | "declaration": true,
10 | "noImplicitThis": true
11 | },
12 | "include": ["src"],
13 | "exclude": ["**/*.test.ts"]
14 | }
15 |
--------------------------------------------------------------------------------