├── .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 | [![npm version](https://img.shields.io/npm/v/@xapi/xapi.svg)](https://www.npmjs.com/package/@xapi/xapi) 3 | [![test](https://github.com/xapijs/xapi/actions/workflows/test.yml/badge.svg?branch=develop)](https://github.com/xapijs/xapi/actions/workflows/test.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/xapijs/xapi/badge.svg?branch=develop)](https://coveralls.io/github/xapijs/xapi?branch=develop) 5 | [![maintainability](https://api.codeclimate.com/v1/badges/93dd4331b1fe39ab73d7/maintainability)](https://codeclimate.com/github/xapijs/xapi/maintainability) 6 | ![minified size](https://img.shields.io/bundlephobia/min/@xapi/xapi) 7 | ![minzipped size](https://img.shields.io/bundlephobia/minzip/@xapi/xapi) 8 | 9 | [xAPI.js logo](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 | --------------------------------------------------------------------------------