From 83b1d2ea2fbe1e3a8b1aa67225b25ed3e5fd6237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Fri, 21 Jul 2023 09:48:51 +0100 Subject: [PATCH 01/22] sss --- package-lock.json | 1205 +++++++---------- package.json | 1 + projects/ngx-xapi/course/index.ts | 1 + projects/ngx-xapi/course/ng-package.json | 6 + .../course/src/lib/xapi-course.module.ts | 10 + .../ngx-xapi/course/src/lib/xapi-course.ts | 99 ++ projects/ngx-xapi/course/src/public-api.ts | 6 + sonar-project.properties | 4 +- tsconfig.json | 3 + 9 files changed, 610 insertions(+), 725 deletions(-) create mode 100644 projects/ngx-xapi/course/index.ts create mode 100644 projects/ngx-xapi/course/ng-package.json create mode 100644 projects/ngx-xapi/course/src/lib/xapi-course.module.ts create mode 100644 projects/ngx-xapi/course/src/lib/xapi-course.ts create mode 100644 projects/ngx-xapi/course/src/public-api.ts diff --git a/package-lock.json b/package-lock.json index 039190b..ebd1e05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@angular/core": "^16.1.0", "@angular/platform-browser": "^16.1.0", "@angular/platform-browser-dynamic": "^16.1.0", + "@angular/router": "^16.1.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "uuid": "^9.0.0", @@ -183,18 +184,6 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", - "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/tslib": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", @@ -299,9 +288,9 @@ } }, "node_modules/@angular/common": { - "version": "16.1.4", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.1.4.tgz", - "integrity": "sha512-SDA8GZVY0nXCJaNUy13L22jAKuk1LZgQ6QzqOpqQc50C25bfBQbYv68PKjHCjQ62VxGKnDSTT85xCMNx+y/U4g==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.1.5.tgz", + "integrity": "sha512-XQVIpICniWXXMoXsr6X7Q3pVcYBeQ0FZF06BNNolkkkVuReYpqr3TwWrZfuB9TUmxdF6R5WZ+M3NAdXodDDUNA==", "dependencies": { "tslib": "^2.3.0" }, @@ -309,14 +298,14 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.1.4", + "@angular/core": "16.1.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "16.1.4", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.1.4.tgz", - "integrity": "sha512-5iKx8g+6/LtiRhbqMS2Jw1AshFUb4M8LO9WQKfRoE+5mZrDOYkAQYgOlAO7fk0mOCXeZcHJBbq2nuwDfwsZIiw==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.1.5.tgz", + "integrity": "sha512-QNyisdr9lEN43v/e/fjS0H1vrJBMY8lIGpxVY1OOERFjA1clfMhaz5fiPE3vWFV5TOm3/ym9z2xuRXM6UoyWoA==", "peer": true, "dependencies": { "tslib": "^2.3.0" @@ -325,7 +314,7 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.1.4" + "@angular/core": "16.1.5" }, "peerDependenciesMeta": { "@angular/core": { @@ -334,9 +323,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "16.1.4", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.1.4.tgz", - "integrity": "sha512-JerJOZeOLaHFHrfWMm4m9tEw+MdNNIMPj3TSauJ6uZPbFokGeqS2GsUBMjuQlwh5xY4duh1HtRsohvshpl306A==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.1.5.tgz", + "integrity": "sha512-j20hmPyM+rLJDU1y0ta9Uf7+o2oGjvGWGpyANbpuTlAfA1+VN5G3xD53FnNcmO6LZuAw0wDw6NDAyy+G55o8xQ==", "dev": true, "dependencies": { "@babel/core": "7.22.5", @@ -357,14 +346,14 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/compiler": "16.1.4", + "@angular/compiler": "16.1.5", "typescript": ">=4.9.3 <5.2" } }, "node_modules/@angular/core": { - "version": "16.1.4", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.1.4.tgz", - "integrity": "sha512-eWs++peAp+Lm2SHGfMsHAye2IOmlDKkVJ4dFf4TaZXW+AEev3FXKXLFp+dBUq8YkCKly7iAV26NXEUBOFFtplQ==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.1.5.tgz", + "integrity": "sha512-xmk+WeL3qtFb3BM2hsEq/kGHJinqaTNVJkK/m4TiGArY+hjJwfCOeuTss7nOkKXvhRkZxU9VP0tej1w3QV5Yzw==", "dependencies": { "tslib": "^2.3.0" }, @@ -377,9 +366,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "16.1.4", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.1.4.tgz", - "integrity": "sha512-eQ1dBh/6ZwJVeiNGrcW6ePFmWeS+Oheu1RpuZSsvM/fI6qfsZE+or9IJ61SFvsMs65SbrO90Akc+ZXmpEidPdA==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.1.5.tgz", + "integrity": "sha512-TLM29KPr0A0pQ0YEmSy0JUOkfBXfwfBFzXQSt9SOiUs0wgDVVLMdGOpR/tbvBx2QfrSU3qgOX8P1FXIPJch6TQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -387,9 +376,9 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/animations": "16.1.4", - "@angular/common": "16.1.4", - "@angular/core": "16.1.4" + "@angular/animations": "16.1.5", + "@angular/common": "16.1.5", + "@angular/core": "16.1.5" }, "peerDependenciesMeta": { "@angular/animations": { @@ -398,9 +387,26 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "16.1.4", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.1.4.tgz", - "integrity": "sha512-OIszPs3NLCZWL8BEvn458JotNMdXPGyEVToNa2cEVgtakVxkhrhmoFlwJTWJN4GRkHNL5h2Vb0JLEYICwr7sgg==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.1.5.tgz", + "integrity": "sha512-ugdIXeN5IVj9o15ywH32hxNI0ZLyakpBGqMTHZSeEhU/uN6ajAJX7z6okdMbJ7dlTyBO8eFV1KDX3aAz+sK9bg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "16.1.5", + "@angular/compiler": "16.1.5", + "@angular/core": "16.1.5", + "@angular/platform-browser": "16.1.5" + } + }, + "node_modules/@angular/router": { + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.1.5.tgz", + "integrity": "sha512-L1gyWA16U+XgcxWmemWjy08/OPCjch9sBEiHaikuW8i9Ys0nx9ic3wh8Fyu6cVKQE9aQZ7xLYT5CdPPwYxclTw==", "dependencies": { "tslib": "^2.3.0" }, @@ -408,10 +414,10 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.1.4", - "@angular/compiler": "16.1.4", - "@angular/core": "16.1.4", - "@angular/platform-browser": "16.1.4" + "@angular/common": "16.1.5", + "@angular/core": "16.1.5", + "@angular/platform-browser": "16.1.5", + "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@assemblyscript/loader": { @@ -433,9 +439,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", - "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -520,16 +526,16 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz", - "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz", + "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.6", + "@babel/compat-data": "^7.22.9", "@babel/helper-validator-option": "^7.22.5", - "@nicolo-ribaudo/semver-v6": "^6.3.3", "browserslist": "^4.21.9", - "lru-cache": "^5.1.1" + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -538,10 +544,19 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.6.tgz", - "integrity": "sha512-iwdzgtSiBxF6ni6mzVnZCF3xt5qE6cEA0J7nFt8QOAWZ0zjCFceEgpn3vtb2V7WFR6QzP2jmIFOHMTRo7eNJjQ==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.9.tgz", + "integrity": "sha512-Pwyi89uO4YrGKxL/eNJ8lfEH55DnRloGPOseaA8NFNL6jAUnn+KccaISiFazCj5IolPPDjGSdzQzXVzODVRqUQ==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", @@ -549,10 +564,10 @@ "@babel/helper-function-name": "^7.22.5", "@babel/helper-member-expression-to-functions": "^7.22.5", "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@nicolo-ribaudo/semver-v6": "^6.3.3" + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -561,15 +576,36 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-regexp-features-plugin": { + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-split-export-declaration": { "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.6.tgz", - "integrity": "sha512-nBookhLKxAWo/TUCmhnaEJyLz2dekjQvv5SRpE9epWQBcpedWLKt8aZdsuT9XV5ovzR3fENLjRXVT0GsSlGGhA==", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz", + "integrity": "sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@nicolo-ribaudo/semver-v6": "^6.3.3", - "regexpu-core": "^5.3.1" + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -578,6 +614,15 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-define-polyfill-provider": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.1.tgz", @@ -653,18 +698,30 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", - "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", + "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-module-imports": "^7.22.5", "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { "@babel/types": "^7.22.5" }, "engines": { @@ -693,15 +750,14 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.5.tgz", - "integrity": "sha512-cU0Sq1Rf4Z55fgz7haOakIyM7+x/uCFwXpLPaeRzfoUtAEAuUZjZvFPjL/rk5rW693dIgn2hng1W7xbT7lWT4g==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz", + "integrity": "sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-wrap-function": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/helper-wrap-function": "^7.22.9" }, "engines": { "node": ">=6.9.0" @@ -711,20 +767,20 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.5.tgz", - "integrity": "sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", + "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-member-expression-to-functions": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-simple-access": { @@ -752,9 +808,9 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", + "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", "dev": true, "dependencies": { "@babel/types": "^7.22.5" @@ -791,14 +847,13 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.5.tgz", - "integrity": "sha512-bYqLIBSEshYcYQyfks8ewYA8S30yaGSeRslcvKMvoUk6HHPySbxHq9YRi6ghhzEU+yhQv9bP/jXnygkStOcqZw==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.9.tgz", + "integrity": "sha512-sZ+QzfauuUEfxSEjKFmi3qDSHgLsTPK/pEpoD/qonZKOtTPTLbf59oabPQ4rKekt9lFcj/hTZaOhWwFYrgjk+Q==", "dev": true, "dependencies": { "@babel/helper-function-name": "^7.22.5", "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", "@babel/types": "^7.22.5" }, "engines": { @@ -1294,6 +1349,18 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", @@ -2131,372 +2198,48 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "node_modules/@babel/traverse/node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", - "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", - "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", - "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", - "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", - "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", - "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", - "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", - "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", - "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", - "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", - "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", - "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", - "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", - "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", - "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", - "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", - "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", - "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", - "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], + "node_modules/@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", - "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", - "cpu": [ - "arm64" - ], + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true, - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=12" + "node": ">=0.1.90" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", - "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", - "cpu": [ - "ia32" - ], + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true, - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=12" + "node": ">=10.0.0" } }, "node_modules/@esbuild/win32-x64": { @@ -3056,30 +2799,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/@tufjs/models/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -3134,9 +2853,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.40.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.2.tgz", - "integrity": "sha512-PRVjQ4Eh9z9pmmtaq8nTjZjQwKFk7YIHIud3lRoKRBgUQjgjRmoGxxGEPXQkF+lH7QkHJRNr5F4aBgYCW0lqpQ==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.0.tgz", + "integrity": "sha512-gsF+c/0XOguWgaOgvFs+xnnRqt9GwgTvIks36WpE6ueeI4KCEHHd8K/CKHqhOqrJKsYH8m27kRzQEvWXAwXUTw==", "dev": true, "dependencies": { "@types/estree": "*", @@ -3217,9 +2936,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.0.tgz", - "integrity": "sha512-jfT7iTf/4kOQ9S7CHV9BIyRaQqHu67mOjsIQBC3BKZvzvUB6zLxEwJ6sBE3ozcvP8kF6Uk5PXN0Q+c0dfhGX0g==", + "version": "20.4.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz", + "integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==", "dev": true }, "node_modules/@types/qs": { @@ -3970,13 +3689,12 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -4106,37 +3824,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.1.tgz", - "integrity": "sha512-9BKYcEeIs7QwlCYs+Y3GBvqAMISufUS0i2ELd11zpZjxI5V9iyRj0HgzB5/cLf2NY4vcYBTYzJ7GIui7j/4DOw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2", - "path-scurry": "^1.10.0" - }, - "bin": { - "glob": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/cacache/node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -4146,21 +3833,6 @@ "node": ">=12" } }, - "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -4193,9 +3865,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001513", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001513.tgz", - "integrity": "sha512-pnjGJo7SOOjAGytZZ203Em95MRM8Cr6jhCXNF/FAXTpCTRTECnqQWLpiTRqrFtdYcth8hf4WECUpkezuYsMVww==", + "version": "1.0.30001517", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz", + "integrity": "sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==", "dev": true, "funding": [ { @@ -4769,21 +4441,6 @@ "node": ">= 8" } }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/css-loader": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", @@ -5086,9 +4743,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.453", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.453.tgz", - "integrity": "sha512-BU8UtQz6CB3T7RIGhId4BjmjJVXQDujb0+amGL8jpcluFJr6lwspBOvkUbnttfpZCm4zFMHmjrX1QrdPWBBMjQ==", + "version": "1.4.464", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.464.tgz", + "integrity": "sha512-guZ84yoou4+ILNdj0XEbmGs6DEWj6zpVOWYpY09GU66yEb0DSYvP/biBPzHn0GuW/3RC/pnaYNUWlQE1fJYtgA==", "dev": true }, "node_modules/emoji-regex": { @@ -5439,6 +5096,12 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/exponential-backoff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", @@ -5796,18 +5459,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", - "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5877,20 +5528,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -5916,6 +5553,12 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5971,20 +5614,22 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "10.3.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz", + "integrity": "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/cjs/src/bin.js" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -6427,30 +6072,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/ignore-walk/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/ignore-walk/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -6465,9 +6086,9 @@ } }, "node_modules/immutable": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", - "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.1.tgz", + "integrity": "sha512-lj9cnmB/kVS0QHsJnYKD1uo3o39nrbKxszjnqS9Fr6NB7bZzW45U6WSGBPKXDL/CvDKqDNPA4r3DoDQ8GTxo2A==", "dev": true }, "node_modules/import-fresh": { @@ -7085,13 +6706,10 @@ } }, "node_modules/json-parse-even-better-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", - "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, "node_modules/json-schema-traverse": { "version": "1.0.0", @@ -7182,6 +6800,18 @@ "which": "^1.2.1" } }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/karma-coverage": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", @@ -7199,6 +6829,28 @@ "node": ">=10.0.0" } }, + "node_modules/karma-coverage/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/karma-jasmine": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", @@ -7234,15 +6886,57 @@ "source-map-support": "^0.5.5" } }, - "node_modules/karma/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, "node_modules/karma/node_modules/source-map": { @@ -7793,15 +7487,18 @@ "dev": true }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -8196,28 +7893,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, - "node_modules/nice-napi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", - "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "!win32" - ], - "dependencies": { - "node-addon-api": "^3.0.0", - "node-gyp-build": "^4.2.2" - } - }, - "node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "dev": true, - "optional": true - }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -8252,31 +7927,46 @@ "node": "^12.13 || ^14.13 || >=16" } }, - "node_modules/node-gyp-build": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", - "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/node-gyp/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "dependencies": { - "isexe": "^2.0.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, - "bin": { - "node-which": "bin/node-which" + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">= 8" + "node": "*" } }, "node_modules/node-releases": { @@ -8794,12 +8484,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse-json/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -8890,13 +8574,13 @@ "dev": true }, "node_modules/path-scurry": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.0.tgz", - "integrity": "sha512-tZFEaRQbMLjwrsmidsGJ6wDMv0iazJWk6SfIKnY4Xru8auXgmJkOBa5DUbYFcFD2Rzk2+KDlIiF0GVXNCbgC7g==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", "dev": true, "dependencies": { "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2" + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -9124,6 +8808,16 @@ "postcss": "^8.0.0" } }, + "node_modules/postcss-url/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/postcss-url/node_modules/mime": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", @@ -9343,50 +9037,22 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/read-package-json/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/read-package-json/node_modules/glob": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.1.tgz", - "integrity": "sha512-9BKYcEeIs7QwlCYs+Y3GBvqAMISufUS0i2ELd11zpZjxI5V9iyRj0HgzB5/cLf2NY4vcYBTYzJ7GIui7j/4DOw==", + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", + "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2", - "path-scurry": "^1.10.0" - }, - "bin": { - "glob": "dist/cjs/src/bin.js" - }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/read-package-json/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "node_modules/read-package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", + "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/readable-stream": { @@ -9600,6 +9266,12 @@ "node": ">=8" } }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -9640,10 +9312,52 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/rollup": { - "version": "3.26.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.26.2.tgz", - "integrity": "sha512-6umBIGVz93er97pMgQO08LuH3m6PUb3jlDUUGFsNJB6VgTCUaDFpupf5JfU30529m/UKOgmiX+uY6Sx8cOYpLA==", + "version": "3.26.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.26.3.tgz", + "integrity": "sha512-7Tin0C8l86TkpcMtXvQu6saWH93nhG3dGQ1/+l5V2TDMceTxO7kDiK6GzbfLWNNxqJXm591PcEZUozZm51ogwQ==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -10082,10 +9796,16 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", + "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/sigstore": { "version": "1.7.0", @@ -10681,6 +10401,48 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -11341,12 +11103,6 @@ "ajv": "^6.9.1" } }, - "node_modules/webpack/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/webpack/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -11395,15 +11151,18 @@ } }, "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "dependencies": { "isexe": "^2.0.0" }, "bin": { - "which": "bin/which" + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, "node_modules/wide-align": { diff --git a/package.json b/package.json index 5efc1f2..3e2e4bb 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@angular/core": "^16.1.0", "@angular/platform-browser": "^16.1.0", "@angular/platform-browser-dynamic": "^16.1.0", + "@angular/router": "^16.1.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "uuid": "^9.0.0", diff --git a/projects/ngx-xapi/course/index.ts b/projects/ngx-xapi/course/index.ts new file mode 100644 index 0000000..49f243d --- /dev/null +++ b/projects/ngx-xapi/course/index.ts @@ -0,0 +1 @@ +export * from './src/public-api'; diff --git a/projects/ngx-xapi/course/ng-package.json b/projects/ngx-xapi/course/ng-package.json new file mode 100644 index 0000000..1f7447f --- /dev/null +++ b/projects/ngx-xapi/course/ng-package.json @@ -0,0 +1,6 @@ +{ + "dest": "../../../dist/ngx-xapi/course", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.module.ts b/projects/ngx-xapi/course/src/lib/xapi-course.module.ts new file mode 100644 index 0000000..5e0e8cf --- /dev/null +++ b/projects/ngx-xapi/course/src/lib/xapi-course.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { XapiCourseService } from './xapi-course'; +import { RouterModule } from '@angular/router'; + +@NgModule({ + declarations: [], + imports: [RouterModule.forRoot([])], + exports: [RouterModule], +}) +export class LanguageMapPipeModule {} diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts new file mode 100644 index 0000000..dfe35c4 --- /dev/null +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -0,0 +1,99 @@ +import { HttpClient } from '@angular/common/http'; +import { ActivatedRoute, Params } from '@angular/router'; +import { + Activity, + ActivityDefinition, + Actor, + Context, + Statement, + XapiClient, + completed, +} from '@berry-cloud/ngx-xapi'; +import { Observable, filter, map, take } from 'rxjs'; + +export class XapiCourseService { + public client: XapiClient; + private course: Activity; + private context?: Context; + + constructor( + private httpClient: HttpClient, + endpoint: string, + authorization: string, + private actor: Actor, + activityId: string, + private registration?: string + ) { + this.client = new XapiClient(this.httpClient, { + endpoint, + authorization, + }); + this.course = { + id: activityId, + }; + } + + setCourseDefinition(courseDefinition: ActivityDefinition) { + this.course.definition = courseDefinition; + } + + getXapiClient() { + return this.client; + } + + postCompletedStatement() { + return this.postStatement({ verb: completed }); + } + + postStatement(statement: Partial) { + return this.client.postStatement(this.fillStatement(statement)); + } + + private fillStatement(partial: Partial): Statement { + const statement = { ...partial }; + if (!statement.verb) { + throw new Error('statement.verb is required'); + } + if (!statement.actor) { + statement.actor = this.actor; + } + if (!statement.object) { + statement.object = this.course; + } + if (!statement.context) { + statement.context = this.context ?? {}; + } + if (!statement.context.registration && this.registration) { + statement.context.registration = this.registration; + } + + return statement as Statement; + } +} + +export function xapiCourseServiceFactory( + httpClient: HttpClient, + activatedRoute: ActivatedRoute +): Observable { + return activatedRoute.queryParams.pipe( + filter( + (params: Params) => + params['endpoint'] && + params['auth'] && + params['actor'] && + params['activityId'] + ), + map( + (params: Params) => + new XapiCourseService( + httpClient, + params['endpoint'], + params['auth'], + JSON.parse(params['actor']), + params['activityId'], + params['registration'] + ) + ), + take(1) + ); +} diff --git a/projects/ngx-xapi/course/src/public-api.ts b/projects/ngx-xapi/course/src/public-api.ts new file mode 100644 index 0000000..8f5e3b1 --- /dev/null +++ b/projects/ngx-xapi/course/src/public-api.ts @@ -0,0 +1,6 @@ +/* + * Public API Surface of ngx-xapi/course + */ + +export * from './lib/xapi-course'; +export * from './lib/xapi-course.module'; diff --git a/sonar-project.properties b/sonar-project.properties index fd7f354..3a804eb 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -6,8 +6,8 @@ sonar.organization=berrycloud #sonar.projectVersion=1.0 # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. -sonar.sources=projects/ngx-xapi/client/src,projects/ngx-xapi/model/src,projects/ngx-xapi/profiles/cmi5/src -sonar.tests=projects/ngx-xapi/client/src,projects/ngx-xapi/model/src,projects/ngx-xapi/profiles/cmi5/src +sonar.sources=projects/ngx-xapi/client/src,projects/ngx-xapi/model/src,projects/ngx-xapi/course/src,projects/ngx-xapi/profiles/cmi5/src +sonar.tests=projects/ngx-xapi/client/src,projects/ngx-xapi/model/src,projects/ngx-xapi/course/src,projects/ngx-xapi/profiles/cmi5/src # Encoding of the source code. Default is default system encoding #sonar.sourceEncoding=UTF-8 diff --git a/tsconfig.json b/tsconfig.json index f1ad897..096b955 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,9 @@ "@berry-cloud/ngx-xapi/client": [ "dist/ngx-xapi/client" ], + "@berry-cloud/ngx-xapi/course": [ + "dist/ngx-xapi/course" + ], "@berry-cloud/ngx-xapi": [ "dist/ngx-xapi" ] From 3936b5cb7e9425b6b958ef64299ed3dd5a001a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Fri, 21 Jul 2023 17:02:51 +0100 Subject: [PATCH 02/22] more progress on cmi5 --- .../ngx-xapi/client/src/lib/state-options.ts | 7 + .../ngx-xapi/client/src/lib/xapi-client.ts | 9 +- projects/ngx-xapi/client/src/public-api.ts | 1 + .../ngx-xapi/course/src/lib/xapi-course.ts | 285 +++++++++++++++--- .../profiles/cmi5/src/lib/fetch-result.ts | 5 + .../ngx-xapi/profiles/cmi5/src/public-api.ts | 1 + 6 files changed, 270 insertions(+), 38 deletions(-) create mode 100644 projects/ngx-xapi/client/src/lib/state-options.ts create mode 100644 projects/ngx-xapi/profiles/cmi5/src/lib/fetch-result.ts diff --git a/projects/ngx-xapi/client/src/lib/state-options.ts b/projects/ngx-xapi/client/src/lib/state-options.ts new file mode 100644 index 0000000..b83e683 --- /dev/null +++ b/projects/ngx-xapi/client/src/lib/state-options.ts @@ -0,0 +1,7 @@ +export interface StateOptions { + contentType: string; + etag?: string; + match?: boolean; +} + +export type DeleteStateOptions = Omit; diff --git a/projects/ngx-xapi/client/src/lib/xapi-client.ts b/projects/ngx-xapi/client/src/lib/xapi-client.ts index 565b263..976ed4f 100644 --- a/projects/ngx-xapi/client/src/lib/xapi-client.ts +++ b/projects/ngx-xapi/client/src/lib/xapi-client.ts @@ -28,6 +28,7 @@ import { ActivityProfileParams, ActivityProfilesParams, } from './activity-profile-params'; +import { DeleteStateOptions, StateOptions } from './state-options'; export interface XapiConfig { endpoint: string; @@ -110,7 +111,7 @@ export class XapiClient { putState( object: any, stateParams: StateParams, - options: { contentType: string; etag?: string; match?: boolean } + options: StateOptions ): Observable> { return this.config$.pipe( mergeMap((config) => { @@ -134,7 +135,7 @@ export class XapiClient { postState( object: any, stateParams: StateParams, - options: { contentType: string; etag?: string; match?: boolean } + options: StateOptions ): Observable> { return this.config$.pipe( mergeMap((config) => { @@ -159,7 +160,7 @@ export class XapiClient { */ deleteState( stateParams: StateParams, - options: { etag?: string; match?: boolean } + options: DeleteStateOptions ): Observable> { return this.config$.pipe( mergeMap((config) => { @@ -183,7 +184,7 @@ export class XapiClient { */ deleteStates( statesParams: DeleteStatesParams, - options: { etag?: string; match?: boolean } + options: DeleteStateOptions ): Observable> { return this.config$.pipe( mergeMap((config) => { diff --git a/projects/ngx-xapi/client/src/public-api.ts b/projects/ngx-xapi/client/src/public-api.ts index fc4c669..8786e96 100644 --- a/projects/ngx-xapi/client/src/public-api.ts +++ b/projects/ngx-xapi/client/src/public-api.ts @@ -13,4 +13,5 @@ export { GetStatesParams, DeleteStatesParams, } from './lib/state-params'; +export * from './lib/state-options'; export * from './lib/statements-params'; diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index dfe35c4..68e7692 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -1,52 +1,224 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpResponse } from '@angular/common/http'; import { ActivatedRoute, Params } from '@angular/router'; import { Activity, ActivityDefinition, - Actor, + Agent, Context, Statement, - XapiClient, completed, -} from '@berry-cloud/ngx-xapi'; -import { Observable, filter, map, take } from 'rxjs'; + initialized, +} from '@berry-cloud/ngx-xapi/model'; +import { + XapiClient, + StateParams, + StateOptions, +} from '@berry-cloud/ngx-xapi/client'; +import { + Observable, + ReplaySubject, + map, + merge, + mergeMap, + of, + skip, + take, + tap, +} from 'rxjs'; +import { v4 } from 'uuid'; +import { FetchResult, LaunchData } from '@berry-cloud/ngx-xapi/profiles/cmi5'; + +export const DEFAULT_STATE_ID = 'state'; export class XapiCourseService { - public client: XapiClient; - private course: Activity; - private context?: Context; + public client?: ReplaySubject; + private course?: Activity; + private contextTemplate?: Context; + + private launchData?: LaunchData; constructor( private httpClient: HttpClient, - endpoint: string, - authorization: string, - private actor: Actor, - activityId: string, - private registration?: string + endpoint?: string, + authorization?: string, + private agent?: Agent, + private activityId?: string, + private registration?: string, + fetch?: string ) { - this.client = new XapiClient(this.httpClient, { - endpoint, - authorization, - }); - this.course = { - id: activityId, - }; + if (endpoint && agent && activityId && (authorization || fetch)) { + this.course = { + id: activityId, + }; + this.client = new ReplaySubject(1); + if (fetch) { + // fetch authorization from the LMS + fetchCmi5Authorization(httpClient, fetch!) + .pipe( + // create a client from the authorization token + map( + (authorization: string) => + new XapiClient(this.httpClient, { + endpoint, + authorization, + }) + ), + // load cmi5 launch data + mergeMap((client: XapiClient) => + this.getCmi5launchData(client).pipe( + map((launchData: LaunchData) => { + this.launchData = launchData; + this.contextTemplate = launchData.contextTemplate; + return client; + }) + ) + ), + // send cmi5 initialized statement + mergeMap((client: XapiClient) => + this.sendCmi5Initialization(client).pipe( + // initialize client only after the cmi5 initialized statement is sent + tap(() => { + this.client!.next(client); + }) + ) + ) + ) + // All subsequent observables come from httpClient, so they terminate automatically + .subscribe(); + } else if (authorization) { + // initialize only if all required parameters are provided + this.client!.next( + new XapiClient(this.httpClient, { + endpoint, + authorization, + }) + ); + } + this.client!.complete(); + } else if (endpoint || agent || activityId || authorization || fetch) { + throw new Error( + 'Cannot initialize XapiCourseService. Missing required launch parameters.' + ); + } + // If NONE of the launch parameters are provided, + // we assume that the service is used for testing purposes, + // (eg. was launched manually from the browser) + // or was launched from a non-cmi5/non-xapi LMS. + // In this case, the service will not get/send any statements or states. + } + + private getCmi5launchData(client: XapiClient): Observable { + return client + .getState({ + activityId: this.activityId!, + agent: this.agent!, + registration: this.registration, + stateId: 'LMS.LaunchData', + }) + .pipe( + map((response: HttpResponse) => { + if (response.body) { + return response.body; + } + throw new Error('Cannot fetch cmi5 launch data.'); + }) + ); + } + + private sendCmi5Initialization( + client: XapiClient + ): Observable> { + return client.postStatement(this.fillStatement({ verb: initialized })); } setCourseDefinition(courseDefinition: ActivityDefinition) { - this.course.definition = courseDefinition; + if (this.course) { + this.course.definition = courseDefinition; + } } getXapiClient() { return this.client; } - postCompletedStatement() { - return this.postStatement({ verb: completed }); + getLaunchData() { + return this.launchData; + } + + getState(stateId: string) { + if (this.client) { + return this.client.pipe( + mergeMap((client: XapiClient) => + client.getState({ + activityId: this.activityId!, + agent: this.agent!, + registration: this.registration, + stateId, + }) + ) + ); + } else { + // return not found if the client is not initialized + // (no launch parameters were provided) + return of(new HttpResponse({ body: null, status: 404 })); + } + } + + postState(state: T, options: StateOptions, params?: Partial) { + const stateParams = this.fillStateParams(params ?? {}); + if (this.client) { + return this.client.pipe( + mergeMap((client: XapiClient) => + client.postState(state, stateParams, options) + ) + ); + } else { + // emulate a successful response if the client is not initialized + return of(new HttpResponse({ status: 204 })); + } + } + putState(state: T, options: StateOptions, params?: Partial) { + const stateParams = this.fillStateParams(params ?? {}); + if (this.client) { + return this.client.pipe( + mergeMap((client: XapiClient) => + client.putState(state, stateParams, options) + ) + ); + } else { + // emulate a successful response if the client is not initialized + return of(new HttpResponse({ status: 204 })); + } + } + + private fillStateParams(partial: Partial): StateParams { + const stateParams = { ...partial }; + if (!stateParams.activityId) { + stateParams.activityId = this.activityId!; + } + if (!stateParams.agent) { + stateParams.agent = this.agent!; + } + if (!stateParams.registration && this.registration) { + stateParams.registration = this.registration; + } + if (!stateParams.stateId) { + stateParams.stateId = DEFAULT_STATE_ID; + } + return stateParams as StateParams; } postStatement(statement: Partial) { - return this.client.postStatement(this.fillStatement(statement)); + if (this.client) { + return this.client.pipe( + mergeMap((client: XapiClient) => + client.postStatement(this.fillStatement(statement)) + ) + ); + } else { + // emulate a successful response if the client is not initialized + return of(new HttpResponse({ status: 200, body: v4() })); + } } private fillStatement(partial: Partial): Statement { @@ -55,13 +227,20 @@ export class XapiCourseService { throw new Error('statement.verb is required'); } if (!statement.actor) { - statement.actor = this.actor; + statement.actor = this.agent; } if (!statement.object) { statement.object = this.course; } if (!statement.context) { - statement.context = this.context ?? {}; + statement.context = this.contextTemplate ?? {}; + } else { + // merge context template with the provided context + // TODO: this is not a deep merge + statement.context = { + ...this.contextTemplate, + ...statement.context, + }; } if (!statement.context.registration && this.registration) { statement.context.registration = this.registration; @@ -69,20 +248,57 @@ export class XapiCourseService { return statement as Statement; } + + /** + * Convenience method for sending a default completed statement. + */ + sendCompletedStatement() { + return this.postStatement({ verb: completed }); + } +} + +/** + * Convenience method for manually fetching cmi5 authorization token from the LMS. + * + * @remarks + * If this method is used, the XapiCourseService should be initialized with the returned token as an auth parameter. + * Also note that the XapiCourseService won't send the cmi5 initialized statement automatically. + * Using this method is not recommended, but it can be useful if some non-common initialization is required. + */ +export function fetchCmi5Authorization( + httpClient: HttpClient, + fetch: string +): Observable { + return httpClient.get(fetch).pipe( + map((response) => { + if (response['auth-token']) { + return response['auth-token'] as string; + } + if (response['error-text']) { + throw new Error(response['error-text'] as string); + } + if (response['error-code']) { + throw new Error( + 'Cannot fetch cmi5 authorization token. Error code:' + + response['error-code'] + ); + } + throw new Error('Cannot fetch cmi5 authorization token.'); + }) + ); } +/** + * Convenience factory for creating an XapiCourseService from the launch parameters. + * Can be used as a provider in the app-module. + */ export function xapiCourseServiceFactory( httpClient: HttpClient, activatedRoute: ActivatedRoute ): Observable { return activatedRoute.queryParams.pipe( - filter( - (params: Params) => - params['endpoint'] && - params['auth'] && - params['actor'] && - params['activityId'] - ), + // Theoretically the first value is always empty + skip(1), map( (params: Params) => new XapiCourseService( @@ -91,7 +307,8 @@ export function xapiCourseServiceFactory( params['auth'], JSON.parse(params['actor']), params['activityId'], - params['registration'] + params['registration'], + params['fetch'] ) ), take(1) diff --git a/projects/ngx-xapi/profiles/cmi5/src/lib/fetch-result.ts b/projects/ngx-xapi/profiles/cmi5/src/lib/fetch-result.ts new file mode 100644 index 0000000..df0d42a --- /dev/null +++ b/projects/ngx-xapi/profiles/cmi5/src/lib/fetch-result.ts @@ -0,0 +1,5 @@ +export interface FetchResult { + 'auth-token': string; + 'error-text': string; + 'error-code': number; +} diff --git a/projects/ngx-xapi/profiles/cmi5/src/public-api.ts b/projects/ngx-xapi/profiles/cmi5/src/public-api.ts index 178a316..e154755 100644 --- a/projects/ngx-xapi/profiles/cmi5/src/public-api.ts +++ b/projects/ngx-xapi/profiles/cmi5/src/public-api.ts @@ -3,6 +3,7 @@ */ export * from './lib/cmi5-statements'; +export * from './lib/fetch-result'; export * from './lib/launch-data'; export * from './lib/launch'; export * from './lib/learner-preferences'; From be349702b862bcb8b3440207a15ebafb9a64d986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Mon, 24 Jul 2023 21:56:17 +0100 Subject: [PATCH 03/22] refactor course project --- .../course/src/lib/xapi-course.module.ts | 10 - .../ngx-xapi/course/src/lib/xapi-course.ts | 478 +++++++++++------- projects/ngx-xapi/course/src/public-api.ts | 1 - .../ngx-xapi/profiles/cmi5/src/lib/launch.ts | 11 +- .../samples/src/app/app-routing.module.ts | 22 + projects/samples/src/app/app.component.html | 10 +- projects/samples/src/app/app.module.ts | 25 +- .../samples/src/app/samples.component.html | 27 + projects/samples/src/app/samples.component.ts | 7 + .../app/xapi-course/xapi-course.component.ts | 57 +++ 10 files changed, 450 insertions(+), 198 deletions(-) delete mode 100644 projects/ngx-xapi/course/src/lib/xapi-course.module.ts create mode 100644 projects/samples/src/app/app-routing.module.ts create mode 100644 projects/samples/src/app/samples.component.html create mode 100644 projects/samples/src/app/samples.component.ts create mode 100644 projects/samples/src/app/xapi-course/xapi-course.component.ts diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.module.ts b/projects/ngx-xapi/course/src/lib/xapi-course.module.ts deleted file mode 100644 index 5e0e8cf..0000000 --- a/projects/ngx-xapi/course/src/lib/xapi-course.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NgModule } from '@angular/core'; -import { XapiCourseService } from './xapi-course'; -import { RouterModule } from '@angular/router'; - -@NgModule({ - declarations: [], - imports: [RouterModule.forRoot([])], - exports: [RouterModule], -}) -export class LanguageMapPipeModule {} diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index 68e7692..1fa34cc 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -1,13 +1,15 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; -import { ActivatedRoute, Params } from '@angular/router'; import { Activity, - ActivityDefinition, - Agent, Context, Statement, + Verb, completed, + experienced, + failed, initialized, + passed, + progressed, } from '@berry-cloud/ngx-xapi/model'; import { XapiClient, @@ -17,102 +19,184 @@ import { import { Observable, ReplaySubject, + catchError, + forkJoin, map, - merge, mergeMap, of, - skip, take, tap, } from 'rxjs'; import { v4 } from 'uuid'; -import { FetchResult, LaunchData } from '@berry-cloud/ngx-xapi/profiles/cmi5'; +import { + FetchResult, + Launch, + LaunchData, +} from '@berry-cloud/ngx-xapi/profiles/cmi5'; +import { Inject, Injectable, InjectionToken, Optional } from '@angular/core'; +import { PlatformLocation } from '@angular/common'; export const DEFAULT_STATE_ID = 'state'; +export const XAPI_LAUNCH = new InjectionToken>( + 'xapi.launch' +); + +export const XAPI_ACTIVITY = new InjectionToken< + Activity | Observable +>('xapi.activity'); + +@Injectable({ + providedIn: 'root', +}) export class XapiCourseService { - public client?: ReplaySubject; + public client: ReplaySubject; private course?: Activity; private contextTemplate?: Context; + private launch?: Launch; private launchData?: LaunchData; constructor( private httpClient: HttpClient, - endpoint?: string, - authorization?: string, - private agent?: Agent, - private activityId?: string, - private registration?: string, - fetch?: string + @Inject(XAPI_ACTIVITY) + activity: Activity | Observable, + @Optional() + @Inject(XAPI_LAUNCH) + launchParameters?: Launch | Observable | null, + @Optional() + platformLocation?: PlatformLocation ) { - if (endpoint && agent && activityId && (authorization || fetch)) { - this.course = { - id: activityId, - }; - this.client = new ReplaySubject(1); - if (fetch) { - // fetch authorization from the LMS - fetchCmi5Authorization(httpClient, fetch!) - .pipe( - // create a client from the authorization token - map( - (authorization: string) => - new XapiClient(this.httpClient, { - endpoint, - authorization, - }) - ), - // load cmi5 launch data - mergeMap((client: XapiClient) => - this.getCmi5launchData(client).pipe( - map((launchData: LaunchData) => { - this.launchData = launchData; - this.contextTemplate = launchData.contextTemplate; - return client; - }) - ) - ), - // send cmi5 initialized statement - mergeMap((client: XapiClient) => - this.sendCmi5Initialization(client).pipe( - // initialize client only after the cmi5 initialized statement is sent - tap(() => { - this.client!.next(client); - }) - ) - ) - ) - // All subsequent observables come from httpClient, so they terminate automatically - .subscribe(); - } else if (authorization) { - // initialize only if all required parameters are provided - this.client!.next( - new XapiClient(this.httpClient, { - endpoint, - authorization, - }) - ); + this.client = new ReplaySubject(1); + + var launch$: Observable; + var activity$: Observable; + + if (activity instanceof Observable) { + activity$ = activity; + } else { + activity$ = of(activity); + } + + if (launchParameters) { + if (launchParameters instanceof Observable) { + launch$ = launchParameters; + } else { + launch$ = of(launchParameters); } - this.client!.complete(); - } else if (endpoint || agent || activityId || authorization || fetch) { - throw new Error( - 'Cannot initialize XapiCourseService. Missing required launch parameters.' - ); + } else if (platformLocation?.search) { + const params = new URLSearchParams(platformLocation.search); + launch$ = of({ + endpoint: params.get('endpoint'), + auth: params.get('auth'), + actor: params.get('actor') && JSON.parse(params.get('actor')!), + activityId: params.get('activityId'), + registration: params.get('registration'), + fetch: params.get('fetch'), + } as Launch); + } else { + launch$ = of({} as Launch); } - // If NONE of the launch parameters are provided, - // we assume that the service is used for testing purposes, - // (eg. was launched manually from the browser) - // or was launched from a non-cmi5/non-xapi LMS. - // In this case, the service will not get/send any statements or states. + + forkJoin([activity$, launch$]) + .pipe( + mergeMap(([activity, launch]) => { + this.course = { + ...activity, + }; + if (launch.activityId) { + this.course.id = launch.activityId; + } + if ( + launch.endpoint && + launch.actor && + (launch.auth || launch.fetch) + ) { + this.launch = launch; + + if (launch.fetch) { + // fetch authorization from the LMS + return fetchCmi5Authorization(httpClient, launch.fetch!).pipe( + // create a client from the authorization token + map( + (authorization: string) => + new XapiClient(this.httpClient, { + endpoint: launch.endpoint, + authorization, + }) + ), + // load cmi5 launch data + mergeMap((client: XapiClient) => + this.getCmi5launchData(client).pipe( + map((launchData: LaunchData) => { + this.launchData = launchData; + this.contextTemplate = launchData.contextTemplate; + return client; + }) + ) + ), + // send cmi5 initialized statement + mergeMap((client: XapiClient) => + this.sendCmi5Initialization(client).pipe( + // initialize client only after the cmi5 initialized statement is sent + tap(() => { + this.client!.next(client); + this.client!.complete(); + }) + ) + ) + ); + } else if (launch.auth) { + // initialize only if all required parameters are provided + this.client!.next( + new XapiClient(this.httpClient, { + endpoint: launch.endpoint, + authorization: launch.auth, + }) + ); + this.client!.complete(); + } + return of(undefined); + } else if ( + launch.endpoint || + launch.actor || + launch.auth || + launch.fetch + ) { + throw new Error( + 'Cannot initialize XapiCourseService. Missing required launch parameters.' + ); + } else { + // If NO launch parameters are provided, + // we assume that the service is used for testing purposes, + // (eg. was launched manually from the browser) + // or was launched from a non-cmi5/non-xapi LMS. + // In this case, the service will not get/send any statements or states. + this.client.next(undefined); + this.client.complete(); + return of(undefined); + } + }), + catchError((error) => { + // In case of error, the client will not be initialized. + // This means that the service will not get/send any statements or states. + // But otherwise it will work normally. + this.client.next(undefined); + this.client.complete(); + // We rethrow the error so that the app can handle it. + throw error; + }), + take(1) + ) + .subscribe(); } private getCmi5launchData(client: XapiClient): Observable { return client .getState({ - activityId: this.activityId!, - agent: this.agent!, - registration: this.registration, + activityId: this.course!.id, + agent: this.launch!.actor!, + registration: this.launch!.registration, stateId: 'LMS.LaunchData', }) .pipe( @@ -131,103 +215,129 @@ export class XapiCourseService { return client.postStatement(this.fillStatement({ verb: initialized })); } - setCourseDefinition(courseDefinition: ActivityDefinition) { - if (this.course) { - this.course.definition = courseDefinition; - } - } - - getXapiClient() { + getXapiClient(): Observable { return this.client; } - getLaunchData() { + getCmi5LaunchData() { return this.launchData; } getState(stateId: string) { - if (this.client) { - return this.client.pipe( - mergeMap((client: XapiClient) => - client.getState({ - activityId: this.activityId!, - agent: this.agent!, - registration: this.registration, - stateId, - }) - ) - ); - } else { - // return not found if the client is not initialized - // (no launch parameters were provided) - return of(new HttpResponse({ body: null, status: 404 })); - } + return this.client.pipe( + mergeMap((client) => + client + ? client.getState({ + activityId: this.launch!.activityId!, + agent: this.launch!.actor, + registration: this.launch!.registration, + stateId, + }) + : // return not found if the client is not initialized + // (no launch parameters were provided) + of(new HttpResponse({ body: null, status: 404 })) + ) + ); } postState(state: T, options: StateOptions, params?: Partial) { const stateParams = this.fillStateParams(params ?? {}); - if (this.client) { - return this.client.pipe( - mergeMap((client: XapiClient) => - client.postState(state, stateParams, options) - ) - ); - } else { - // emulate a successful response if the client is not initialized - return of(new HttpResponse({ status: 204 })); - } + return this.client.pipe( + mergeMap((client) => + client + ? client.postState(state, stateParams, options) + : of(new HttpResponse({ status: 204 })) + ) + ); } + putState(state: T, options: StateOptions, params?: Partial) { const stateParams = this.fillStateParams(params ?? {}); - if (this.client) { - return this.client.pipe( - mergeMap((client: XapiClient) => - client.putState(state, stateParams, options) - ) - ); - } else { - // emulate a successful response if the client is not initialized - return of(new HttpResponse({ status: 204 })); - } + return this.client.pipe( + mergeMap((client) => + client + ? client.putState(state, stateParams, options) + : of(new HttpResponse({ status: 204 })) + ) + ); } private fillStateParams(partial: Partial): StateParams { const stateParams = { ...partial }; if (!stateParams.activityId) { - stateParams.activityId = this.activityId!; + stateParams.activityId = this.course?.id; } if (!stateParams.agent) { - stateParams.agent = this.agent!; + stateParams.agent = this.launch?.actor; } - if (!stateParams.registration && this.registration) { - stateParams.registration = this.registration; + if (!stateParams.registration && this.launch?.registration) { + stateParams.registration = this.launch?.registration; } if (!stateParams.stateId) { stateParams.stateId = DEFAULT_STATE_ID; } + return stateParams as StateParams; } - postStatement(statement: Partial) { - if (this.client) { - return this.client.pipe( - mergeMap((client: XapiClient) => - client.postStatement(this.fillStatement(statement)) - ) - ); - } else { - // emulate a successful response if the client is not initialized - return of(new HttpResponse({ status: 200, body: v4() })); - } + /** + * Sends a statement to the LRS. + * If the client is not initialized (no launch parameters were provided), + * returns OK and a random uuid. + * + * @param param Either a partial statement which will be extended by the default statement, + * or a function that takes the default statement as a parameter and returns a partial statement. + * If not provided, a default statement will be sent. + * + * The default statement contains the following fields: + * + * actor: the actor from the launch parameters + * + * verb: experienced + * + * object: the XAPI_ACTIVITY + optional id from the launch parameters + * + * context: the context template from the launch parameters + * + * context.registration: the registration from the launch parameters + * + * If a partial statement is provided, the above fields will be overridden by the default statement. + * If a function is provided, the above fields will be overridden by the result of the function. + * + * @returns An observable of the HttpResponse from the LRS. (The body is the uuid of the statement) + */ + postStatement( + param?: Partial | ((defaultStatement: Statement) => Statement) + ): Observable> { + const verb = + typeof param === 'function' ? experienced : param?.verb || experienced; + return this.postStatementWithVerb(verb, param); + } + + private postStatementWithVerb( + verb: Verb, + param?: Partial | ((defaultStatement: Statement) => Statement) + ): Observable> { + const statement = + typeof param === 'function' + ? param(this.fillStatement({ verb })) + : this.fillStatement({ ...param, verb }); + + return this.client.pipe( + mergeMap((client) => + client + ? client.postStatement(statement) + : // return OK and a random uuid if the client is not initialized + // (no launch parameters were provided) + of(new HttpResponse({ status: 200, body: v4() })) + ) + ); } private fillStatement(partial: Partial): Statement { const statement = { ...partial }; - if (!statement.verb) { - throw new Error('statement.verb is required'); - } if (!statement.actor) { - statement.actor = this.agent; + statement.actor = this.launch?.actor; } if (!statement.object) { statement.object = this.course; @@ -242,8 +352,8 @@ export class XapiCourseService { ...statement.context, }; } - if (!statement.context.registration && this.registration) { - statement.context.registration = this.registration; + if (!statement.context.registration && this.launch?.registration) { + statement.context.registration = this.launch?.registration; } return statement as Statement; @@ -251,9 +361,66 @@ export class XapiCourseService { /** * Convenience method for sending a default completed statement. + * @see {@link postStatement} */ - sendCompletedStatement() { - return this.postStatement({ verb: completed }); + sendCompletedStatement( + param?: Partial | ((defaultStatement: Statement) => Statement) + ) { + return this.postStatementWithVerb(completed, param); + } + + /** + * Convenience method for sending a default passed statement. + * @see {@link postStatement} + */ + sendPassedStatement( + param?: Partial | ((defaultStatement: Statement) => Statement) + ) { + return this.postStatementWithVerb(passed, param); + } + + /** + * Convenience method for sending a default failed statement. + * @see {@link postStatement} + */ + sendFailedStatement( + param?: Partial | ((defaultStatement: Statement) => Statement) + ) { + return this.postStatementWithVerb(failed, param); + } + + /** + * Convenience method for sending a default progressed statement. + * @param progress The progress value (0-100) + * @see {@link postStatement} + */ + sendProgressedStatement( + progress: number, + param?: Partial | ((defaultStatement: Statement) => Statement) + ) { + let statement = + typeof param === 'function' + ? param(this.fillStatement({ verb: progressed })) + : this.fillStatement({ ...param, verb: progressed }); + + statement = { + ...statement, + result: { + ...statement?.result, + extensions: { + ...statement?.result?.extensions, + 'http://id.tincanapi.com/extension/progress': progress, + }, + }, + }; + + return this.client.pipe( + mergeMap((client) => + client + ? client.postStatement(statement) + : of(new HttpResponse({ status: 200, body: v4() })) + ) + ); } } @@ -262,14 +429,14 @@ export class XapiCourseService { * * @remarks * If this method is used, the XapiCourseService should be initialized with the returned token as an auth parameter. - * Also note that the XapiCourseService won't send the cmi5 initialized statement automatically. + * Also note that the XapiCourseService won't send the cmi5 initialized statement automatically in this case. * Using this method is not recommended, but it can be useful if some non-common initialization is required. */ export function fetchCmi5Authorization( httpClient: HttpClient, fetch: string ): Observable { - return httpClient.get(fetch).pipe( + return httpClient.post(fetch, null).pipe( map((response) => { if (response['auth-token']) { return response['auth-token'] as string; @@ -287,30 +454,3 @@ export function fetchCmi5Authorization( }) ); } - -/** - * Convenience factory for creating an XapiCourseService from the launch parameters. - * Can be used as a provider in the app-module. - */ -export function xapiCourseServiceFactory( - httpClient: HttpClient, - activatedRoute: ActivatedRoute -): Observable { - return activatedRoute.queryParams.pipe( - // Theoretically the first value is always empty - skip(1), - map( - (params: Params) => - new XapiCourseService( - httpClient, - params['endpoint'], - params['auth'], - JSON.parse(params['actor']), - params['activityId'], - params['registration'], - params['fetch'] - ) - ), - take(1) - ); -} diff --git a/projects/ngx-xapi/course/src/public-api.ts b/projects/ngx-xapi/course/src/public-api.ts index 8f5e3b1..fa91456 100644 --- a/projects/ngx-xapi/course/src/public-api.ts +++ b/projects/ngx-xapi/course/src/public-api.ts @@ -3,4 +3,3 @@ */ export * from './lib/xapi-course'; -export * from './lib/xapi-course.module'; diff --git a/projects/ngx-xapi/profiles/cmi5/src/lib/launch.ts b/projects/ngx-xapi/profiles/cmi5/src/lib/launch.ts index ccc7f9a..06fb4ab 100644 --- a/projects/ngx-xapi/profiles/cmi5/src/lib/launch.ts +++ b/projects/ngx-xapi/profiles/cmi5/src/lib/launch.ts @@ -1,9 +1,10 @@ -import { Actor } from '@berry-cloud/ngx-xapi/model'; +import { Agent } from '@berry-cloud/ngx-xapi/model'; export interface Launch { endpoint: string; // URL - fetch: string; // URL - actor: Actor; // Agent - registration: string; // UUID - activityId: string; // URL + fetch?: string; // URL + actor: Agent; // Agent + registration?: string; // UUID + activityId?: string; // URL + auth?: string; // Authorization for xAPI } diff --git a/projects/samples/src/app/app-routing.module.ts b/projects/samples/src/app/app-routing.module.ts new file mode 100644 index 0000000..89b6297 --- /dev/null +++ b/projects/samples/src/app/app-routing.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { XapiCourseComponent } from './xapi-course/xapi-course.component'; +import { AppComponent } from './app.component'; +import { SamplesComponent } from './samples.component'; + +const routes: Routes = [ + { + path: '', + component: SamplesComponent, + }, + { + path: 'xapi-course', + component: XapiCourseComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], +}) +export class AppRoutingModule {} diff --git a/projects/samples/src/app/app.component.html b/projects/samples/src/app/app.component.html index 83842d3..0680b43 100644 --- a/projects/samples/src/app/app.component.html +++ b/projects/samples/src/app/app.component.html @@ -1,9 +1 @@ -

Samples

- - - - - - - - + diff --git a/projects/samples/src/app/app.module.ts b/projects/samples/src/app/app.module.ts index 74fee46..25d125a 100644 --- a/projects/samples/src/app/app.module.ts +++ b/projects/samples/src/app/app.module.ts @@ -1,31 +1,48 @@ -import { NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { HttpClientModule } from '@angular/common/http'; -import { XAPI_CONFIG, XapiConfig } from '@berry-cloud/ngx-xapi'; +import { Activity, XAPI_CONFIG, XapiConfig } from '@berry-cloud/ngx-xapi'; import { AppComponent } from './app.component'; import { GetStateComponent } from './get-state/get-state.component'; import { GetStatementComponent } from './get-statement/get-statement.component'; import { PostStateComponent } from './post-state/post-state.component'; import { PostStatementComponent } from './post-statement/post-statement.component'; +import { AppRoutingModule } from './app-routing.module'; +import { SamplesComponent } from './samples.component'; +import { XapiCourseComponent } from './xapi-course/xapi-course.component'; +import { XAPI_ACTIVITY } from '@berry-cloud/ngx-xapi/course'; @NgModule({ declarations: [ AppComponent, + SamplesComponent, PostStatementComponent, GetStatementComponent, PostStateComponent, GetStateComponent, + XapiCourseComponent, ], - imports: [BrowserModule, HttpClientModule], + imports: [BrowserModule, HttpClientModule, AppRoutingModule], providers: [ { provide: XAPI_CONFIG, useValue: { endpoint: 'https://lrs.adlnet.gov/xapi/', - authorization: '', + authorization: 'Basic dGVzdDIzOnRlc3QyMzIz', } as XapiConfig, }, + { + provide: XAPI_ACTIVITY, + useValue: { + id: 'https://berrycloud.co.uk/xapi/sample', + definition: { + name: { + 'en-US': 'BerryCloud Sample Course', + }, + }, + } as Activity, + }, ], bootstrap: [AppComponent], }) diff --git a/projects/samples/src/app/samples.component.html b/projects/samples/src/app/samples.component.html new file mode 100644 index 0000000..154a753 --- /dev/null +++ b/projects/samples/src/app/samples.component.html @@ -0,0 +1,27 @@ + + +

xAPI Client Samples

+ + + + + + + + diff --git a/projects/samples/src/app/samples.component.ts b/projects/samples/src/app/samples.component.ts new file mode 100644 index 0000000..c4d40bb --- /dev/null +++ b/projects/samples/src/app/samples.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'samples-root', + templateUrl: './samples.component.html', +}) +export class SamplesComponent {} diff --git a/projects/samples/src/app/xapi-course/xapi-course.component.ts b/projects/samples/src/app/xapi-course/xapi-course.component.ts new file mode 100644 index 0000000..2ac312c --- /dev/null +++ b/projects/samples/src/app/xapi-course/xapi-course.component.ts @@ -0,0 +1,57 @@ +import { Component } from '@angular/core'; +import { Statement, answered, experienced } from '@berry-cloud/ngx-xapi'; +import { XapiCourseService } from '@berry-cloud/ngx-xapi/course'; +import { Observable } from 'rxjs'; + +@Component({ + selector: 'xapi-course', + template: ` +

xAPI Course Sample

+ +

+ Try deleting/modifiying/extending the launch parameters in the URL bar and + check the result in the developer tools' console and network tab

+ Mandatory parameters are: endpoint (URI), actor (JSON)
+ Authorization parameter is one of: auth (authorization header, tincan + only), fetch (URI, CMI5 only)
+ Optional parameters are: activityId (URI), registration (UUID) +

+ + Default Statement: {{ (defaultStatement$ | async)?.body }} +
+ Unique Activity: {{ (sampleStatement$ | async)?.body }} +
+ Completed Statement: {{ (completedStatement$ | async)?.body }} +
+ Callback Statement: {{ (callbackStatement$ | async)?.body }} + `, + providers: [], +}) +export class XapiCourseComponent { + defaultStatement$: Observable = this.courseService.postStatement(); + + sampleStatement$: Observable = this.courseService.postStatement({ + verb: experienced, + object: { + id: 'http://example.com/activities/sample-activity', + definition: { + name: { + 'en-US': 'Sample Activity', + }, + }, + }, + }); + + completedStatement$: Observable = + this.courseService.sendCompletedStatement(); + + callbackStatement$: Observable = this.courseService.postStatement( + (defaultStatement) => + ({ + ...defaultStatement, + verb: answered, + } as Statement) + ); + + constructor(public courseService: XapiCourseService) {} +} From 3a700650e8b3f5d7fc32e263ba491d2bec354277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Tue, 25 Jul 2023 10:33:26 +0100 Subject: [PATCH 04/22] improve statement merging --- .../ngx-xapi/course/src/lib/xapi-course.ts | 143 ++++++++++++++---- 1 file changed, 113 insertions(+), 30 deletions(-) diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index 1fa34cc..10219a6 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -10,6 +10,7 @@ import { initialized, passed, progressed, + scored, } from '@berry-cloud/ngx-xapi/model'; import { XapiClient, @@ -212,7 +213,7 @@ export class XapiCourseService { private sendCmi5Initialization( client: XapiClient ): Observable> { - return client.postStatement(this.fillStatement({ verb: initialized })); + return client.postStatement(this.fillStatement(initialized)); } getXapiClient(): Observable { @@ -301,17 +302,21 @@ export class XapiCourseService { * * context.registration: the registration from the launch parameters * - * If a partial statement is provided, the above fields will be overridden by the default statement. - * If a function is provided, the above fields will be overridden by the result of the function. + * @remarks Beware! If you use a callback function, you can override the mandatory properties + * come from the cmi5 context template or launch parameters. + * (eg. registration, contextActivities, extensions etc.) + * In common scenarios, this is a bad practice. However, it can be useful in some cases. + * (eg. if you want to send a statement about a side effect with a different properties + * than the cmi5 context template or launch parameters provide for the main statement) + * + * If you use a partial statement, these mandatory properties will be merged with the partial statement. * * @returns An observable of the HttpResponse from the LRS. (The body is the uuid of the statement) */ postStatement( param?: Partial | ((defaultStatement: Statement) => Statement) ): Observable> { - const verb = - typeof param === 'function' ? experienced : param?.verb || experienced; - return this.postStatementWithVerb(verb, param); + return this.postStatementWithVerb(experienced, param); } private postStatementWithVerb( @@ -320,8 +325,8 @@ export class XapiCourseService { ): Observable> { const statement = typeof param === 'function' - ? param(this.fillStatement({ verb })) - : this.fillStatement({ ...param, verb }); + ? param(this.fillStatement(verb)) + : this.fillStatement(verb, param); return this.client.pipe( mergeMap((client) => @@ -334,26 +339,69 @@ export class XapiCourseService { ); } - private fillStatement(partial: Partial): Statement { - const statement = { ...partial }; - if (!statement.actor) { - statement.actor = this.launch?.actor; - } - if (!statement.object) { - statement.object = this.course; - } - if (!statement.context) { - statement.context = this.contextTemplate ?? {}; - } else { - // merge context template with the provided context - // TODO: this is not a deep merge - statement.context = { - ...this.contextTemplate, - ...statement.context, - }; - } - if (!statement.context.registration && this.launch?.registration) { - statement.context.registration = this.launch?.registration; + private fillStatement(verb: Verb, partial?: Partial): Statement { + const statement = { + ...partial, + ...(!partial?.actor && { actor: this.launch?.actor }), + ...(!partial?.verb && { verb }), + ...(!partial?.object && { object: this.course }), + }; + + if (this.contextTemplate || this.launch?.registration) { + // if context template or registration is provided, we need to add a context + if (!partial?.context) { + statement.context = { + ...this.contextTemplate, + registration: this.launch?.registration, + }; + } else { + // merge context template with the provided context + statement.context = { + ...partial.context, + ...this.contextTemplate, + registration: this.launch?.registration, + ...(partial.context.extensions && { + extensions: { + ...partial.context?.extensions, + ...this.contextTemplate?.extensions, + }, + }), + ...(partial.context.contextActivities && { + contextActivities: { + ...partial.context.contextActivities, + ...this.contextTemplate?.contextActivities, + ...(partial.context.contextActivities.category && + this.contextTemplate?.contextActivities?.category && { + category: [ + ...partial.context.contextActivities.category, + ...this.contextTemplate.contextActivities.category, + ], + }), + ...(partial.context.contextActivities.parent && + this.contextTemplate?.contextActivities?.parent && { + parent: [ + ...partial.context.contextActivities.parent, + ...this.contextTemplate.contextActivities.parent, + ], + }), + ...(partial.context.contextActivities.grouping && + this.contextTemplate?.contextActivities?.grouping && { + grouping: [ + ...partial.context.contextActivities.grouping, + ...this.contextTemplate.contextActivities.grouping, + ], + }), + ...(partial.context.contextActivities.other && + this.contextTemplate?.contextActivities?.other && { + other: [ + ...partial.context.contextActivities.other, + ...this.contextTemplate.contextActivities.other, + ], + }), + }, + }), + }; + } } return statement as Statement; @@ -400,8 +448,8 @@ export class XapiCourseService { ) { let statement = typeof param === 'function' - ? param(this.fillStatement({ verb: progressed })) - : this.fillStatement({ ...param, verb: progressed }); + ? param(this.fillStatement(progressed)) + : this.fillStatement(progressed, param); statement = { ...statement, @@ -422,6 +470,41 @@ export class XapiCourseService { ) ); } + + /** + * Convenience method for sending a default scored statement. + * @param score The score value (0-100) + * @see {@link postStatement} + */ + sendScoredStatement( + score: number, + param?: Partial | ((defaultStatement: Statement) => Statement) + ) { + let statement = + typeof param === 'function' + ? param(this.fillStatement(scored)) + : this.fillStatement(scored, param); + + statement = { + ...statement, + result: { + ...statement?.result, + score: { + ...statement?.result?.score, + raw: score, + scaled: score / 100, + }, + }, + }; + + return this.client.pipe( + mergeMap((client) => + client + ? client.postStatement(statement) + : of(new HttpResponse({ status: 200, body: v4() })) + ) + ); + } } /** From 83d4c91f8e2c9bc4f3d22953c59eafa3dfc4c353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Tue, 25 Jul 2023 13:50:01 +0100 Subject: [PATCH 05/22] extends readme with course initialization --- projects/ngx-xapi/README.md | 131 ++++++++++++++++-- .../ngx-xapi/course/src/lib/xapi-course.ts | 40 ++---- projects/ngx-xapi/src/public-api.ts | 3 - projects/samples/src/app/app.module.ts | 6 +- .../app/xapi-course/xapi-course.component.ts | 2 +- 5 files changed, 140 insertions(+), 42 deletions(-) diff --git a/projects/ngx-xapi/README.md b/projects/ngx-xapi/README.md index 59a1494..da914e8 100644 --- a/projects/ngx-xapi/README.md +++ b/projects/ngx-xapi/README.md @@ -21,17 +21,25 @@ The package contains the following entry-points: @berry-cloud/ngx-xapi/model @berry-cloud/ngx-xapi/client @berry-cloud/ngx-xapi/profiles/cmi5 +@berry-cloud/ngx-xapi/course ``` `@berry-cloud/ngx-xapi/model` contains the core types for xAPI. (Statement, Actor, Verb, etc.) `@berry-cloud/ngx-xapi/client` contains utility methods for communicating with an LRS. `@berry-cloud/ngx-xapi/profiles/cmi5` contains types and extensions for the cmi5 profile. +`@berry-cloud/ngx-xapi/course` contains utility methods for a tincan or cmi5 course player. -All of the exported types and methods from model and client can be accessed directly from `@berry-cloud/ngx-xapi` entry point too. +## Samples + +See [BerryCloud/ngx-xapi GitHub repository](https://github.com/BerryCloud/ngx-xapi) for [Sample application](https://github.com/BerryCloud/ngx-xapi/tree/main/projects/samples) + +It contains simple examples for the client and the course utilities. + +## Client Utilities -## Configuration injection +The client utilities can be accessed via the injectable `XapiClient` service. -If you plan to use the client methods, you must provide an `XapiConfig` to be injected into the `XapiClient`. +If you plan to use this service, you must provide an `XapiConfig` to be injected into the `XapiClient`. The HttpClientModule must also be imported. For example: @@ -116,11 +124,7 @@ function xapiConfigFactory(userService: UserService) { export class AppModule {} ``` -## Samples - -See [BerryCloud/ngx-xapi GitHub repository](https://github.com/BerryCloud/ngx-xapi) for [Sample application](https://github.com/BerryCloud/ngx-xapi/tree/main/projects/samples) - -## Post Statement +### Post Statement ```TypeScript postPassedStatement() { @@ -144,7 +148,7 @@ postPassedStatement() { } ``` -## Post State +### Post State ```TypeScript postState(state: any) { @@ -184,7 +188,7 @@ Example:

``` -## Handling Responses +### Handling Responses Most of the `XapiClient` utility methods return a `Observable>` object. Although in most cases `Observable` would be enough, some important properties of the response can be gathered only from the response headers: @@ -192,3 +196,110 @@ Most of the `XapiClient` utility methods return a `Observable>` - X-Experience-API-Consistent-Through You can access the response object itself via `response.body`; + +## Course Utilities + +The course utilities can be accessed via the injectable `XapiCourse` service. +It contains useful utility methods for a tincan or cmi5 compatible course player. + +If you plan to use this service, you must provide an `Activity` object to be injected into the `XapiCourse`. +This is the default activity object of the tincan/cmi5 course. + +For example: + +```TypeScript +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { XAPI_ACTIVITY } from '@berry-cloud/ngx-xapi/course'; +import { Activity } from '@berry-cloud/ngx-xapi/model'; +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; + +@NgModule({ + declarations: [AppComponent], + imports: [ + BrowserModule, + // import HttpClientModule after BrowserModule. + HttpClientModule, + AppRoutingModule, + ], + providers: [ + provide: XAPI_ACTIVITY, + useValue: { + id: 'https://berrycloud.co.uk/xapi/sample', + definition: { + name: { + 'en-US': 'BerryCloud Sample Course', + }, + }, + } as Activity, + ], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +The `XapiCourse` service also automatically picks up the launch parameters from the URL when the it is initialized. If the launch parameters come from a different source, or you want to hide the URL parameters before the service is initialized, you can provide this parameters via injection too: + +```TypeScript +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { XAPI_ACTIVITY, XAPI_LAUNCH, } from '@berry-cloud/ngx-xapi/course'; +import { Launch } from '@berry-cloud/ngx-xapi/profiles/cmi5'; +import { Activity } from '@berry-cloud/ngx-xapi/model'; +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; + +@NgModule({ + declarations: [AppComponent], + imports: [ + BrowserModule, + // import HttpClientModule after BrowserModule. + HttpClientModule, + AppRoutingModule, + ], + providers: [ + provide: XAPI_ACTIVITY, + useValue: { + id: 'https://berrycloud.co.uk/xapi/sample', + definition: { + name: { + 'en-US': 'BerryCloud Sample Course', + }, + }, + } as Activity, + provide: XAPI_LAUNCH, + useValue: { + endpoint: 'https://mylrs.com/xapi'; + actor: { + name: 'test user' + mbox: 'mailto:test@example.com'; + }; + registration: 'bc6c2d1e-6f5e-4023-83fd-01d89d5bfa32'; + activityId: 'http://example.com/activity-from-launc-parameter'; + auth: 'Basic *******'; + fetch: 'https://mylms.com/token/12345678901234567890' + } as Launch, + ], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +`XAPI_LAUNCH` can be a `Launch` or `Observable`, so if can provide it via a factory method which loads it asynchronously from a file or the URL. + +The `XapiCourse` service can handle both the tincan `auth` parameter or the `cmi5` launch parameter. If the latter is provided it automatically fetches the auth token from the provided endpoint. In this case it also automatically loads the cmi5 launch-data, and will use it to decorate each further state or statement requests. It also automatically sends the mandatory cmi5 `initialized` statement during initialization. + +If the launch parameters are not provided and neither cannot be picked from the URL bar the `XapiCourse` service still initializes itself, but throw an `Error`. +This error must be handled by the application. If it's ignored then the course will still run but it will silently ignore any http requests to the LRS. It can be useful for testing or when a course is launched locally and no need to store progress data. + +If neither of the above initialization methods are suitable, you can create the `XapiCourse` manually: + +``` + const course = new XapiCourse(activity, launch); +``` + +The `XapiCourse` uses the `XapiClient` internally, but extends it functionality with some useful convenient methods. If these methods don't fit for usecase you can still get an `Observable` via the `getXapiClient()` method. +It is `undefined` if the launch parameters were not provided or were deficient. + +## Sending State diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index 10219a6..d9f9512 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -224,16 +224,12 @@ export class XapiCourseService { return this.launchData; } - getState(stateId: string) { + getState(stateId?: string) { + const stateParams = this.fillStateParams({ stateId }); return this.client.pipe( mergeMap((client) => client - ? client.getState({ - activityId: this.launch!.activityId!, - agent: this.launch!.actor, - registration: this.launch!.registration, - stateId, - }) + ? client.getState(stateParams) : // return not found if the client is not initialized // (no launch parameters were provided) of(new HttpResponse({ body: null, status: 404 })) @@ -242,7 +238,7 @@ export class XapiCourseService { } postState(state: T, options: StateOptions, params?: Partial) { - const stateParams = this.fillStateParams(params ?? {}); + const stateParams = this.fillStateParams(params); return this.client.pipe( mergeMap((client) => client @@ -253,7 +249,7 @@ export class XapiCourseService { } putState(state: T, options: StateOptions, params?: Partial) { - const stateParams = this.fillStateParams(params ?? {}); + const stateParams = this.fillStateParams(params); return this.client.pipe( mergeMap((client) => client @@ -263,22 +259,16 @@ export class XapiCourseService { ); } - private fillStateParams(partial: Partial): StateParams { - const stateParams = { ...partial }; - if (!stateParams.activityId) { - stateParams.activityId = this.course?.id; - } - if (!stateParams.agent) { - stateParams.agent = this.launch?.actor; - } - if (!stateParams.registration && this.launch?.registration) { - stateParams.registration = this.launch?.registration; - } - if (!stateParams.stateId) { - stateParams.stateId = DEFAULT_STATE_ID; - } - - return stateParams as StateParams; + private fillStateParams(partial?: Partial): StateParams { + return { + ...partial, + ...(!partial?.activityId && { activityId: this.course?.id }), + ...(!partial?.agent && { agent: this.launch?.actor }), + ...(!partial?.registration && { + registration: this.launch?.registration, + }), + ...(!partial?.stateId && { stateId: DEFAULT_STATE_ID }), + } as StateParams; } /** diff --git a/projects/ngx-xapi/src/public-api.ts b/projects/ngx-xapi/src/public-api.ts index 49cdfef..4037539 100644 --- a/projects/ngx-xapi/src/public-api.ts +++ b/projects/ngx-xapi/src/public-api.ts @@ -3,6 +3,3 @@ */ export const XAPI_VERSION = '1.0.3'; - -export * from '@berry-cloud/ngx-xapi/model'; -export * from '@berry-cloud/ngx-xapi/client'; diff --git a/projects/samples/src/app/app.module.ts b/projects/samples/src/app/app.module.ts index 25d125a..853ba75 100644 --- a/projects/samples/src/app/app.module.ts +++ b/projects/samples/src/app/app.module.ts @@ -1,8 +1,6 @@ -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; - import { HttpClientModule } from '@angular/common/http'; -import { Activity, XAPI_CONFIG, XapiConfig } from '@berry-cloud/ngx-xapi'; import { AppComponent } from './app.component'; import { GetStateComponent } from './get-state/get-state.component'; import { GetStatementComponent } from './get-statement/get-statement.component'; @@ -12,6 +10,8 @@ import { AppRoutingModule } from './app-routing.module'; import { SamplesComponent } from './samples.component'; import { XapiCourseComponent } from './xapi-course/xapi-course.component'; import { XAPI_ACTIVITY } from '@berry-cloud/ngx-xapi/course'; +import { XAPI_CONFIG, XapiConfig } from '@berry-cloud/ngx-xapi/client'; +import { Activity } from '@berry-cloud/ngx-xapi/model'; @NgModule({ declarations: [ diff --git a/projects/samples/src/app/xapi-course/xapi-course.component.ts b/projects/samples/src/app/xapi-course/xapi-course.component.ts index 2ac312c..c473e9b 100644 --- a/projects/samples/src/app/xapi-course/xapi-course.component.ts +++ b/projects/samples/src/app/xapi-course/xapi-course.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { Statement, answered, experienced } from '@berry-cloud/ngx-xapi'; +import { Statement, answered, experienced } from '@berry-cloud/ngx-xapi/model'; import { XapiCourseService } from '@berry-cloud/ngx-xapi/course'; import { Observable } from 'rxjs'; From f4b54e9cae41be6ae71c3303da753d6545ba0e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Tue, 25 Jul 2023 15:11:00 +0100 Subject: [PATCH 06/22] more README, + small fixes and upgrades --- projects/ngx-xapi/README.md | 147 +++++++++++++++++- .../ngx-xapi/client/src/lib/xapi-client.ts | 2 +- .../ngx-xapi/course/src/lib/xapi-course.ts | 36 +++-- 3 files changed, 172 insertions(+), 13 deletions(-) diff --git a/projects/ngx-xapi/README.md b/projects/ngx-xapi/README.md index da914e8..f880b32 100644 --- a/projects/ngx-xapi/README.md +++ b/projects/ngx-xapi/README.md @@ -300,6 +300,149 @@ If neither of the above initialization methods are suitable, you can create the ``` The `XapiCourse` uses the `XapiClient` internally, but extends it functionality with some useful convenient methods. If these methods don't fit for usecase you can still get an `Observable` via the `getXapiClient()` method. -It is `undefined` if the launch parameters were not provided or were deficient. +It is `undefined` if the launch parameters were not provided or were deficient. See the `XapiClient` examples above. -## Sending State +## Sending a State + +After the `XapiCourse` was successfully initialized, you can send a state with the `putState` or `postState` method. It has only one mandatory argument, the state to be sent. All parameters needed for the LRS request are filled or defaulted in by the `XapiCourse`. + +default stateId: `progress` + +default content-type: `application/json` + +If you want to use different `stateId` or send a state with any other properties, you can override any of the defaults: + +```TypeScript +this.courseService.putState( + state, + { + stateId: 'sample-state', + activityId: 'http://example.com/activities/sample-activity', + registration: '123456789-1234-1234-1234-123456789012', + agent: { + mbox: 'mailto:test@example.com', + }, + }, + { + contentType: 'application/json', + etag: '"123456789012345678901234567890123456789012"', + match: true + } +).subscribe( ... ); +``` + +## Getting a State + +The same way as above you can use the `getState` method. Without any arguments it will try to get the state by the default parameters, but you can override any of them: + +```TypeScript +this.xapiCourse.getState( + { + stateId: 'sample-state', + activityId: 'http://example.com/activities/sample-activity', + registration: '123456789-1234-1234-1234-123456789012', + agent: { + mbox: 'mailto:test@example.com', + }, + } +).subscribe( ... ); +``` + +## Sending a Statement + +You can send a default statement using the `postStatement` method: + +```TypeScript +this.xapiCourse.postStatement(); +``` + +The deafault verb is `experienced`, the actor, object and registration properties are picked up from the launch parameters. +You can provide a `Partial` argument to the `postStatement` method where you can override any of these parameters: + +```TypeScript +this.courseService.postStatement( + { + verb: experienced, + object: { + id: 'http://example.com/activities/sample-activity', + definition: { + name: { + 'en-US': 'Sample Activity', + }, + }, + }, + actor: { + mbox: 'mailto:other@example.com', + }, + context: { + registration: '00000000-0000-0000-0000-000000000000', + language: 'en-US', + extensions: { + 'http://example.com/profiles/meetings/context/extensions/meeting-id': + '123456789', + 'http://example.com/profiles/meetings/context/extensions/meeting-name': + 'Example Meeting', + } + }, + } +) +``` + +If the `XapiCourse` was initialized as a cmi5 course, then the properties from cmi5 `contextTemplate` are also added to the `Statement`. The mandatory parameters from the `contextTemplate` cannot be overridden. (these are the sessionId extension and the contextActivities arrays) +If you want full control over the `Statement`, you can configure it via a callback method. The incoming `defaultStatement` argument contains the prefilled Statement, but you can override any or all of the properties. (Do it only if you **really know** what are you doing) + +```TypeScript +this.courseService.postStatement( + (defaultStatement) => + ({ + ...defaultStatement, + verb: attempted, + context: { + contextActivities: { + grouping: [ + { + id: 'http://example.com/activities/world-domination', + definition: { + name: { + 'en-US': 'Nothing to see here', + }, + }, + }, + ], + parent: undefined + }, + extensions: undefined + }, + } as Statement) +); +``` + +## Convenience methods for sending Statements + +You can use the following method for sending the most common tincan or cmi5 statements. All of them can be used with an extra statement-template or a callback method argument, like the `postStatement` method above: + +```TypeScript +sendCompletedStatement(); +sendPassedStatement(); +sendFailedStatement(); +sendProgressedStatement(progressValue); +sendScoredStatement(scoreValue, scaledValue); +``` + +In the latter methods the progress and score values are merged into the statement after the callback method was used. + +eg. sending a score for a test which ha its own activity: + +```TypeScript +// For an IQ test, the scaled score is not applicable, so we send undefined +this.courseService.sendScoredStatement(152, undefined, { + object: { + id: 'http://example.com/activities/iq-test', + definition: { + name: { + 'en-US': 'IQ Test', + }, + }, + }, +}); +``` diff --git a/projects/ngx-xapi/client/src/lib/xapi-client.ts b/projects/ngx-xapi/client/src/lib/xapi-client.ts index 976ed4f..dd987ef 100644 --- a/projects/ngx-xapi/client/src/lib/xapi-client.ts +++ b/projects/ngx-xapi/client/src/lib/xapi-client.ts @@ -700,7 +700,7 @@ export class XapiClient { } if (params.since) { - httpParams = httpParams.set('registration', params.since); + httpParams = httpParams.set('since', params.since); } if (params.stateId) { diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index d9f9512..23d5f9d 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -224,12 +224,11 @@ export class XapiCourseService { return this.launchData; } - getState(stateId?: string) { - const stateParams = this.fillStateParams({ stateId }); + getState(params?: Partial) { return this.client.pipe( mergeMap((client) => client - ? client.getState(stateParams) + ? client.getState(this.fillStateParams(params)) : // return not found if the client is not initialized // (no launch parameters were provided) of(new HttpResponse({ body: null, status: 404 })) @@ -237,23 +236,33 @@ export class XapiCourseService { ); } - postState(state: T, options: StateOptions, params?: Partial) { - const stateParams = this.fillStateParams(params); + postState( + state: T, + params?: Partial, + options?: StateOptions + ) { return this.client.pipe( mergeMap((client) => client - ? client.postState(state, stateParams, options) + ? client.postState( + state, + this.fillStateParams(params), + options || { contentType: 'application/json' } + ) : of(new HttpResponse({ status: 204 })) ) ); } - putState(state: T, options: StateOptions, params?: Partial) { - const stateParams = this.fillStateParams(params); + putState(state: T, params?: Partial, options?: StateOptions) { return this.client.pipe( mergeMap((client) => client - ? client.putState(state, stateParams, options) + ? client.putState( + state, + this.fillStateParams(params), + options || { contentType: 'application/json' } + ) : of(new HttpResponse({ status: 204 })) ) ); @@ -468,6 +477,7 @@ export class XapiCourseService { */ sendScoredStatement( score: number, + scaled?: number, param?: Partial | ((defaultStatement: Statement) => Statement) ) { let statement = @@ -475,6 +485,12 @@ export class XapiCourseService { ? param(this.fillStatement(scored)) : this.fillStatement(scored, param); + const calculatedScaled = + statement?.result?.score?.min && statement?.result?.score?.max + ? (score - statement.result.score.min) / + (statement.result.score.max - statement.result.score.min) + : undefined; + statement = { ...statement, result: { @@ -482,7 +498,7 @@ export class XapiCourseService { score: { ...statement?.result?.score, raw: score, - scaled: score / 100, + scaled: scaled || calculatedScaled, }, }, }; From 953ef4e56f7b649df8e54c7937577c26d16b4a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Tue, 25 Jul 2023 15:33:30 +0100 Subject: [PATCH 07/22] fsi --- .../ngx-xapi/course/src/lib/xapi-course.ts | 83 ++++++++++--------- .../samples/src/app/app-routing.module.ts | 1 - projects/samples/src/app/app.module.ts | 3 +- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index 23d5f9d..79aabea 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -51,7 +51,7 @@ export const XAPI_ACTIVITY = new InjectionToken< providedIn: 'root', }) export class XapiCourseService { - public client: ReplaySubject; + public client = new ReplaySubject(1); private course?: Activity; private contextTemplate?: Context; @@ -68,36 +68,8 @@ export class XapiCourseService { @Optional() platformLocation?: PlatformLocation ) { - this.client = new ReplaySubject(1); - - var launch$: Observable; - var activity$: Observable; - - if (activity instanceof Observable) { - activity$ = activity; - } else { - activity$ = of(activity); - } - - if (launchParameters) { - if (launchParameters instanceof Observable) { - launch$ = launchParameters; - } else { - launch$ = of(launchParameters); - } - } else if (platformLocation?.search) { - const params = new URLSearchParams(platformLocation.search); - launch$ = of({ - endpoint: params.get('endpoint'), - auth: params.get('auth'), - actor: params.get('actor') && JSON.parse(params.get('actor')!), - activityId: params.get('activityId'), - registration: params.get('registration'), - fetch: params.get('fetch'), - } as Launch); - } else { - launch$ = of({} as Launch); - } + const activity$ = this.calculateActivity(activity); + const launch$ = this.calculateLaunch(launchParameters, platformLocation); forkJoin([activity$, launch$]) .pipe( @@ -117,7 +89,7 @@ export class XapiCourseService { if (launch.fetch) { // fetch authorization from the LMS - return fetchCmi5Authorization(httpClient, launch.fetch!).pipe( + return fetchCmi5Authorization(httpClient, launch.fetch).pipe( // create a client from the authorization token map( (authorization: string) => @@ -141,21 +113,21 @@ export class XapiCourseService { this.sendCmi5Initialization(client).pipe( // initialize client only after the cmi5 initialized statement is sent tap(() => { - this.client!.next(client); - this.client!.complete(); + this.client.next(client); + this.client.complete(); }) ) ) ); } else if (launch.auth) { // initialize only if all required parameters are provided - this.client!.next( + this.client.next( new XapiClient(this.httpClient, { endpoint: launch.endpoint, authorization: launch.auth, }) ); - this.client!.complete(); + this.client.complete(); } return of(undefined); } else if ( @@ -192,11 +164,42 @@ export class XapiCourseService { .subscribe(); } + calculateLaunch( + launchParameters: Launch | Observable | null | undefined, + platformLocation: PlatformLocation | undefined + ): Observable { + if (launchParameters) { + if (launchParameters instanceof Observable) { + return launchParameters; + } else { + return of(launchParameters); + } + } else if (platformLocation?.search) { + const params = new URLSearchParams(platformLocation.search); + return of({ + endpoint: params.get('endpoint'), + auth: params.get('auth'), + actor: params.get('actor') && JSON.parse(params.get('actor')!), + activityId: params.get('activityId'), + registration: params.get('registration'), + fetch: params.get('fetch'), + } as Launch); + } else { + return of({} as Launch); + } + } + + calculateActivity( + activity: Activity | Observable + ): Observable { + return activity instanceof Observable ? activity : of(activity); + } + private getCmi5launchData(client: XapiClient): Observable { return client .getState({ activityId: this.course!.id, - agent: this.launch!.actor!, + agent: this.launch!.actor, registration: this.launch!.registration, stateId: 'LMS.LaunchData', }) @@ -456,7 +459,7 @@ export class XapiCourseService { ...statement?.result, extensions: { ...statement?.result?.extensions, - 'http://id.tincanapi.com/extension/progress': progress, + 'https://w3id.org/xapi/cmi5/result/extensions/progress': progress, }, }, }; @@ -528,10 +531,10 @@ export function fetchCmi5Authorization( return httpClient.post(fetch, null).pipe( map((response) => { if (response['auth-token']) { - return response['auth-token'] as string; + return response['auth-token']; } if (response['error-text']) { - throw new Error(response['error-text'] as string); + throw new Error(response['error-text']); } if (response['error-code']) { throw new Error( diff --git a/projects/samples/src/app/app-routing.module.ts b/projects/samples/src/app/app-routing.module.ts index 89b6297..5dfc1a4 100644 --- a/projects/samples/src/app/app-routing.module.ts +++ b/projects/samples/src/app/app-routing.module.ts @@ -1,7 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { XapiCourseComponent } from './xapi-course/xapi-course.component'; -import { AppComponent } from './app.component'; import { SamplesComponent } from './samples.component'; const routes: Routes = [ diff --git a/projects/samples/src/app/app.module.ts b/projects/samples/src/app/app.module.ts index 853ba75..81e0cdd 100644 --- a/projects/samples/src/app/app.module.ts +++ b/projects/samples/src/app/app.module.ts @@ -28,8 +28,9 @@ import { Activity } from '@berry-cloud/ngx-xapi/model'; { provide: XAPI_CONFIG, useValue: { + // You can create a free account here for testing endpoint: 'https://lrs.adlnet.gov/xapi/', - authorization: 'Basic dGVzdDIzOnRlc3QyMzIz', + authorization: 'Basic ***************', } as XapiConfig, }, { From 3df9d81ac3fa449901ac68f71f2c441832fb95ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Tue, 25 Jul 2023 15:59:25 +0100 Subject: [PATCH 08/22] fix more sonar issues --- projects/ngx-xapi/course/src/lib/xapi-course.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index 79aabea..e35ee1f 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -250,7 +250,7 @@ export class XapiCourseService { ? client.postState( state, this.fillStateParams(params), - options || { contentType: 'application/json' } + options ?? { contentType: 'application/json' } ) : of(new HttpResponse({ status: 204 })) ) @@ -264,7 +264,7 @@ export class XapiCourseService { ? client.putState( state, this.fillStateParams(params), - options || { contentType: 'application/json' } + options ?? { contentType: 'application/json' } ) : of(new HttpResponse({ status: 204 })) ) @@ -501,7 +501,7 @@ export class XapiCourseService { score: { ...statement?.result?.score, raw: score, - scaled: scaled || calculatedScaled, + scaled: scaled ?? calculatedScaled, }, }, }; From a9fa2d210b7be9ffbb00ef881f4993c3292f761c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Wed, 26 Jul 2023 10:22:42 +0100 Subject: [PATCH 09/22] imprve README.md --- projects/ngx-xapi/README.md | 66 ++++++++++--------- .../ngx-xapi/course/src/lib/xapi-course.ts | 26 ++++---- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/projects/ngx-xapi/README.md b/projects/ngx-xapi/README.md index f880b32..edb8241 100644 --- a/projects/ngx-xapi/README.md +++ b/projects/ngx-xapi/README.md @@ -25,9 +25,9 @@ The package contains the following entry-points: ``` `@berry-cloud/ngx-xapi/model` contains the core types for xAPI. (Statement, Actor, Verb, etc.) -`@berry-cloud/ngx-xapi/client` contains utility methods for communicating with an LRS. +`@berry-cloud/ngx-xapi/client` contains utility functions for communicating with an LRS. `@berry-cloud/ngx-xapi/profiles/cmi5` contains types and extensions for the cmi5 profile. -`@berry-cloud/ngx-xapi/course` contains utility methods for a tincan or cmi5 course player. +`@berry-cloud/ngx-xapi/course` contains utility functions for a tincan or cmi5 course player. ## Samples @@ -190,7 +190,7 @@ Example: ### Handling Responses -Most of the `XapiClient` utility methods return a `Observable>` object. Although in most cases `Observable` would be enough, some important properties of the response can be gathered only from the response headers: +Most of the `XapiClient` utility functions return a `Observable>` object. Although in most cases `Observable` would be enough, some important properties of the response can be gathered only from the response headers: - ETag - X-Experience-API-Consistent-Through @@ -199,11 +199,12 @@ You can access the response object itself via `response.body`; ## Course Utilities -The course utilities can be accessed via the injectable `XapiCourse` service. -It contains useful utility methods for a tincan or cmi5 compatible course player. +The course utilities can be accessed via the injectable `XapiCourseService`. +It contains useful utility functions for a tincan or cmi5 compatible course player. +You can turn an angular application into a tincan and/or cmi5 compatible course within minutes. -If you plan to use this service, you must provide an `Activity` object to be injected into the `XapiCourse`. -This is the default activity object of the tincan/cmi5 course. +If you plan to use this service, you must provide an `Activity` object to be injected into the `XapiCourseService`. +This will be the activity object of the tincan/cmi5 course. For example: @@ -228,6 +229,7 @@ import { AppComponent } from './app.component'; useValue: { id: 'https://berrycloud.co.uk/xapi/sample', definition: { + type: "http://adlnet.gov/expapi/activities/course", name: { 'en-US': 'BerryCloud Sample Course', }, @@ -239,7 +241,7 @@ import { AppComponent } from './app.component'; export class AppModule {} ``` -The `XapiCourse` service also automatically picks up the launch parameters from the URL when the it is initialized. If the launch parameters come from a different source, or you want to hide the URL parameters before the service is initialized, you can provide this parameters via injection too: +The `XapiCourseService` also automatically picks up the launch parameters from the URL when it is initialized. If the launch parameters come from a different source, or you want to hide the URL parameters before the service is initialized, you can provide this parameters via injection too: ```TypeScript import { NgModule } from '@angular/core'; @@ -286,31 +288,31 @@ import { AppComponent } from './app.component'; export class AppModule {} ``` -`XAPI_LAUNCH` can be a `Launch` or `Observable`, so if can provide it via a factory method which loads it asynchronously from a file or the URL. +`XAPI_LAUNCH` can be a `Launch` or `Observable`, so you can also provide it via a factory function which loads it asynchronously from a file or the URL. -The `XapiCourse` service can handle both the tincan `auth` parameter or the `cmi5` launch parameter. If the latter is provided it automatically fetches the auth token from the provided endpoint. In this case it also automatically loads the cmi5 launch-data, and will use it to decorate each further state or statement requests. It also automatically sends the mandatory cmi5 `initialized` statement during initialization. +The `XapiCourseService` can handle both the tincan `auth` parameter or the `cmi5` launch parameter. If the latter is provided it automatically fetches the auth token from the provided endpoint. In this case it also automatically loads the cmi5 launch-data, and will use it to decorate each further statements to be sent to the LRS. It also automatically sends the mandatory cmi5 `initialized` statement during initialization. -If the launch parameters are not provided and neither cannot be picked from the URL bar the `XapiCourse` service still initializes itself, but throw an `Error`. -This error must be handled by the application. If it's ignored then the course will still run but it will silently ignore any http requests to the LRS. It can be useful for testing or when a course is launched locally and no need to store progress data. +If the launch parameters are not provided and neither cannot be picked from the URL bar the `XapiCourseService` service still initializes itself, but also throws an `Error`. +This error is logged into the console, but does not cease the application. It must be handled manually if needed. +If it's ignored then the course will still run, but it will silently ignore any http requests to the LRS. (Get functions will return 404, put/post functions will return 200 or 204.) It can be useful for testing or when a course is launched locally and no need to store progress data. -If neither of the above initialization methods are suitable, you can create the `XapiCourse` manually: +If neither of the above initialization methods are suitable, you can create the `XapiCourseService` manually: ``` - const course = new XapiCourse(activity, launch); + const course = new XapiCourseService(activity, launch); ``` -The `XapiCourse` uses the `XapiClient` internally, but extends it functionality with some useful convenient methods. If these methods don't fit for usecase you can still get an `Observable` via the `getXapiClient()` method. -It is `undefined` if the launch parameters were not provided or were deficient. See the `XapiClient` examples above. +The `XapiCourseService` uses the `XapiClient` internally, but extends its functionality with some useful convenience functions. +If these functions don't fit for your usecase, you can still get an `Observable` via the `getXapiClient()` function. It returns `undefined` if the launch parameters were not provided or were deficient. See the `XapiClient` examples above. ## Sending a State -After the `XapiCourse` was successfully initialized, you can send a state with the `putState` or `postState` method. It has only one mandatory argument, the state to be sent. All parameters needed for the LRS request are filled or defaulted in by the `XapiCourse`. +After the `XapiCourseService` was successfully initialized, you can send a state with the `putState` or `postState` functions. They have only one mandatory argument, the state to be sent. All parameters needed for the LRS request are filled or defaulted in by the `XapiCourseService`. default stateId: `progress` - default content-type: `application/json` -If you want to use different `stateId` or send a state with any other properties, you can override any of the defaults: +If you want to use different `stateId` or send a state with any other properties, you can override any of the defaults by providing the `StateParams` and `StateOptions` arguments: ```TypeScript this.courseService.putState( @@ -333,10 +335,10 @@ this.courseService.putState( ## Getting a State -The same way as above you can use the `getState` method. Without any arguments it will try to get the state by the default parameters, but you can override any of them: +You can use the `getState` function the same way as the above functions. Without any arguments it will try to get the state by the default parameters, but you can override any of them: ```TypeScript -this.xapiCourse.getState( +this.xapiCourseService.getState( { stateId: 'sample-state', activityId: 'http://example.com/activities/sample-activity', @@ -350,19 +352,19 @@ this.xapiCourse.getState( ## Sending a Statement -You can send a default statement using the `postStatement` method: +You can send a default statement using the `postStatement` function: ```TypeScript -this.xapiCourse.postStatement(); +this.xapiCourseService.postStatement(); ``` -The deafault verb is `experienced`, the actor, object and registration properties are picked up from the launch parameters. -You can provide a `Partial` argument to the `postStatement` method where you can override any of these parameters: +The default verb is `experienced`, the actor, object and registration properties are picked up from the launch parameters. +You can provide a `Partial` argument to the `postStatement` function where you can override any of these parameters: ```TypeScript this.courseService.postStatement( { - verb: experienced, + verb: attempted, object: { id: 'http://example.com/activities/sample-activity', definition: { @@ -388,8 +390,8 @@ this.courseService.postStatement( ) ``` -If the `XapiCourse` was initialized as a cmi5 course, then the properties from cmi5 `contextTemplate` are also added to the `Statement`. The mandatory parameters from the `contextTemplate` cannot be overridden. (these are the sessionId extension and the contextActivities arrays) -If you want full control over the `Statement`, you can configure it via a callback method. The incoming `defaultStatement` argument contains the prefilled Statement, but you can override any or all of the properties. (Do it only if you **really know** what are you doing) +If the `XapiCourseService` was initialized as a cmi5 course, then the properties from cmi5 `contextTemplate` are also added to the `Statement`. The mandatory parameters from the `contextTemplate` cannot be overridden. (these are the sessionId extension and the contextActivities arrays) +If you want full control over the `Statement`, you can configure it via a callback function. The incoming `defaultStatement` argument contains the prefilled Statement, but you can override any or all of the properties. (Do it only if you **really know** what are you doing) ```TypeScript this.courseService.postStatement( @@ -417,9 +419,9 @@ this.courseService.postStatement( ); ``` -## Convenience methods for sending Statements +## Convenience functions for sending Statements -You can use the following method for sending the most common tincan or cmi5 statements. All of them can be used with an extra statement-template or a callback method argument, like the `postStatement` method above: +You can use the following functions for sending the most common tincan or cmi5 statements. All of them can be used with an extra statement-template or a callback function argument, like the `postStatement` function above: ```TypeScript sendCompletedStatement(); @@ -429,9 +431,9 @@ sendProgressedStatement(progressValue); sendScoredStatement(scoreValue, scaledValue); ``` -In the latter methods the progress and score values are merged into the statement after the callback method was used. +In the latter functions the progress and score values are merged into the statement after the callback function was used. -eg. sending a score for a test which ha its own activity: +eg. sending a score for a test which has its own activity: ```TypeScript // For an IQ test, the scaled score is not applicable, so we send undefined diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index e35ee1f..8995c36 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -242,30 +242,32 @@ export class XapiCourseService { postState( state: T, params?: Partial, - options?: StateOptions + options?: Partial ) { return this.client.pipe( mergeMap((client) => client - ? client.postState( - state, - this.fillStateParams(params), - options ?? { contentType: 'application/json' } - ) + ? client.postState(state, this.fillStateParams(params), { + ...options, + contentType: options?.contentType ?? 'application/json', + }) : of(new HttpResponse({ status: 204 })) ) ); } - putState(state: T, params?: Partial, options?: StateOptions) { + putState( + state: T, + params?: Partial, + options?: Partial + ) { return this.client.pipe( mergeMap((client) => client - ? client.putState( - state, - this.fillStateParams(params), - options ?? { contentType: 'application/json' } - ) + ? client.putState(state, this.fillStateParams(params), { + ...options, + contentType: options?.contentType ?? 'application/json', + }) : of(new HttpResponse({ status: 204 })) ) ); From 70f1ac58446ce8ccf8ae84b30fe7ae3059deceff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Wed, 26 Jul 2023 13:01:54 +0100 Subject: [PATCH 10/22] sss --- projects/ngx-xapi/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/ngx-xapi/README.md b/projects/ngx-xapi/README.md index edb8241..5135c1d 100644 --- a/projects/ngx-xapi/README.md +++ b/projects/ngx-xapi/README.md @@ -299,7 +299,7 @@ If it's ignored then the course will still run, but it will silently ignore any If neither of the above initialization methods are suitable, you can create the `XapiCourseService` manually: ``` - const course = new XapiCourseService(activity, launch); + const courseService = new XapiCourseService(activity, launch); ``` The `XapiCourseService` uses the `XapiClient` internally, but extends its functionality with some useful convenience functions. From 5e81fb92b9c60ba02f9b86647893e35e07ef2be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Fri, 21 Jul 2023 09:48:51 +0100 Subject: [PATCH 11/22] sss --- package-lock.json | 1132 +++++++---------- package.json | 1 + projects/ngx-xapi/course/index.ts | 1 + projects/ngx-xapi/course/ng-package.json | 6 + .../course/src/lib/xapi-course.module.ts | 10 + .../ngx-xapi/course/src/lib/xapi-course.ts | 99 ++ projects/ngx-xapi/course/src/public-api.ts | 6 + sonar-project.properties | 4 +- tsconfig.json | 3 + 9 files changed, 565 insertions(+), 697 deletions(-) create mode 100644 projects/ngx-xapi/course/index.ts create mode 100644 projects/ngx-xapi/course/ng-package.json create mode 100644 projects/ngx-xapi/course/src/lib/xapi-course.module.ts create mode 100644 projects/ngx-xapi/course/src/lib/xapi-course.ts create mode 100644 projects/ngx-xapi/course/src/public-api.ts diff --git a/package-lock.json b/package-lock.json index 5a29954..47d3235 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@angular/core": "^16.1.0", "@angular/platform-browser": "^16.1.0", "@angular/platform-browser-dynamic": "^16.1.0", + "@angular/router": "^16.1.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "uuid": "^9.0.0", @@ -183,18 +184,6 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", - "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/tslib": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", @@ -433,9 +422,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", - "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -520,16 +509,16 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz", - "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz", + "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.6", + "@babel/compat-data": "^7.22.9", "@babel/helper-validator-option": "^7.22.5", - "@nicolo-ribaudo/semver-v6": "^6.3.3", "browserslist": "^4.21.9", - "lru-cache": "^5.1.1" + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -538,10 +527,19 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.6.tgz", - "integrity": "sha512-iwdzgtSiBxF6ni6mzVnZCF3xt5qE6cEA0J7nFt8QOAWZ0zjCFceEgpn3vtb2V7WFR6QzP2jmIFOHMTRo7eNJjQ==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.9.tgz", + "integrity": "sha512-Pwyi89uO4YrGKxL/eNJ8lfEH55DnRloGPOseaA8NFNL6jAUnn+KccaISiFazCj5IolPPDjGSdzQzXVzODVRqUQ==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", @@ -549,10 +547,10 @@ "@babel/helper-function-name": "^7.22.5", "@babel/helper-member-expression-to-functions": "^7.22.5", "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@nicolo-ribaudo/semver-v6": "^6.3.3" + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -561,15 +559,36 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-regexp-features-plugin": { + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-split-export-declaration": { "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.6.tgz", - "integrity": "sha512-nBookhLKxAWo/TUCmhnaEJyLz2dekjQvv5SRpE9epWQBcpedWLKt8aZdsuT9XV5ovzR3fENLjRXVT0GsSlGGhA==", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz", + "integrity": "sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@nicolo-ribaudo/semver-v6": "^6.3.3", - "regexpu-core": "^5.3.1" + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -578,6 +597,15 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-define-polyfill-provider": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.1.tgz", @@ -653,18 +681,30 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", - "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", + "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-module-imports": "^7.22.5", "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { "@babel/types": "^7.22.5" }, "engines": { @@ -693,15 +733,14 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.5.tgz", - "integrity": "sha512-cU0Sq1Rf4Z55fgz7haOakIyM7+x/uCFwXpLPaeRzfoUtAEAuUZjZvFPjL/rk5rW693dIgn2hng1W7xbT7lWT4g==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz", + "integrity": "sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-wrap-function": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/helper-wrap-function": "^7.22.9" }, "engines": { "node": ">=6.9.0" @@ -711,20 +750,20 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.5.tgz", - "integrity": "sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", + "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-member-expression-to-functions": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-simple-access": { @@ -752,9 +791,9 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", + "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", "dev": true, "dependencies": { "@babel/types": "^7.22.5" @@ -791,14 +830,13 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.5.tgz", - "integrity": "sha512-bYqLIBSEshYcYQyfks8ewYA8S30yaGSeRslcvKMvoUk6HHPySbxHq9YRi6ghhzEU+yhQv9bP/jXnygkStOcqZw==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.9.tgz", + "integrity": "sha512-sZ+QzfauuUEfxSEjKFmi3qDSHgLsTPK/pEpoD/qonZKOtTPTLbf59oabPQ4rKekt9lFcj/hTZaOhWwFYrgjk+Q==", "dev": true, "dependencies": { "@babel/helper-function-name": "^7.22.5", "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", "@babel/types": "^7.22.5" }, "engines": { @@ -1294,6 +1332,18 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", @@ -2131,372 +2181,48 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "node_modules/@babel/traverse/node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", - "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", - "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", - "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", - "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", - "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", - "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", - "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", - "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", - "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", - "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", - "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", - "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", - "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", - "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", - "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", - "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", - "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", - "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", - "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], + "node_modules/@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", - "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", - "cpu": [ - "arm64" - ], + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true, - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=12" + "node": ">=0.1.90" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", - "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", - "cpu": [ - "ia32" - ], + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true, - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=12" + "node": ">=10.0.0" } }, "node_modules/@esbuild/win32-x64": { @@ -3056,30 +2782,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/@tufjs/models/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -3134,9 +2836,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.40.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.2.tgz", - "integrity": "sha512-PRVjQ4Eh9z9pmmtaq8nTjZjQwKFk7YIHIud3lRoKRBgUQjgjRmoGxxGEPXQkF+lH7QkHJRNr5F4aBgYCW0lqpQ==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.0.tgz", + "integrity": "sha512-gsF+c/0XOguWgaOgvFs+xnnRqt9GwgTvIks36WpE6ueeI4KCEHHd8K/CKHqhOqrJKsYH8m27kRzQEvWXAwXUTw==", "dev": true, "dependencies": { "@types/estree": "*", @@ -3217,9 +2919,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.0.tgz", - "integrity": "sha512-jfT7iTf/4kOQ9S7CHV9BIyRaQqHu67mOjsIQBC3BKZvzvUB6zLxEwJ6sBE3ozcvP8kF6Uk5PXN0Q+c0dfhGX0g==", + "version": "20.4.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz", + "integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==", "dev": true }, "node_modules/@types/qs": { @@ -3970,13 +3672,12 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -4106,37 +3807,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.1.tgz", - "integrity": "sha512-9BKYcEeIs7QwlCYs+Y3GBvqAMISufUS0i2ELd11zpZjxI5V9iyRj0HgzB5/cLf2NY4vcYBTYzJ7GIui7j/4DOw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2", - "path-scurry": "^1.10.0" - }, - "bin": { - "glob": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/cacache/node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -4146,21 +3816,6 @@ "node": ">=12" } }, - "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -4193,9 +3848,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001513", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001513.tgz", - "integrity": "sha512-pnjGJo7SOOjAGytZZ203Em95MRM8Cr6jhCXNF/FAXTpCTRTECnqQWLpiTRqrFtdYcth8hf4WECUpkezuYsMVww==", + "version": "1.0.30001517", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz", + "integrity": "sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==", "dev": true, "funding": [ { @@ -4769,21 +4424,6 @@ "node": ">= 8" } }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/css-loader": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", @@ -5086,9 +4726,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.453", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.453.tgz", - "integrity": "sha512-BU8UtQz6CB3T7RIGhId4BjmjJVXQDujb0+amGL8jpcluFJr6lwspBOvkUbnttfpZCm4zFMHmjrX1QrdPWBBMjQ==", + "version": "1.4.464", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.464.tgz", + "integrity": "sha512-guZ84yoou4+ILNdj0XEbmGs6DEWj6zpVOWYpY09GU66yEb0DSYvP/biBPzHn0GuW/3RC/pnaYNUWlQE1fJYtgA==", "dev": true }, "node_modules/emoji-regex": { @@ -5439,6 +5079,12 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/exponential-backoff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", @@ -5796,18 +5442,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", - "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5877,20 +5511,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -5916,6 +5536,12 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5971,20 +5597,22 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "10.3.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz", + "integrity": "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/cjs/src/bin.js" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -6427,30 +6055,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/ignore-walk/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/ignore-walk/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -6465,9 +6069,9 @@ } }, "node_modules/immutable": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", - "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.1.tgz", + "integrity": "sha512-lj9cnmB/kVS0QHsJnYKD1uo3o39nrbKxszjnqS9Fr6NB7bZzW45U6WSGBPKXDL/CvDKqDNPA4r3DoDQ8GTxo2A==", "dev": true }, "node_modules/import-fresh": { @@ -7085,13 +6689,10 @@ } }, "node_modules/json-parse-even-better-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", - "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, "node_modules/json-schema-traverse": { "version": "1.0.0", @@ -7182,6 +6783,18 @@ "which": "^1.2.1" } }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/karma-coverage": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", @@ -7199,6 +6812,28 @@ "node": ">=10.0.0" } }, + "node_modules/karma-coverage/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/karma-jasmine": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", @@ -7234,15 +6869,57 @@ "source-map-support": "^0.5.5" } }, - "node_modules/karma/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, "node_modules/karma/node_modules/source-map": { @@ -7793,15 +7470,18 @@ "dev": true }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -8196,28 +7876,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, - "node_modules/nice-napi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", - "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "!win32" - ], - "dependencies": { - "node-addon-api": "^3.0.0", - "node-gyp-build": "^4.2.2" - } - }, - "node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "dev": true, - "optional": true - }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -8252,31 +7910,46 @@ "node": "^12.13 || ^14.13 || >=16" } }, - "node_modules/node-gyp-build": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", - "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/node-gyp/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "dependencies": { - "isexe": "^2.0.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, - "bin": { - "node-which": "bin/node-which" + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">= 8" + "node": "*" } }, "node_modules/node-releases": { @@ -8794,12 +8467,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse-json/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -8890,13 +8557,13 @@ "dev": true }, "node_modules/path-scurry": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.0.tgz", - "integrity": "sha512-tZFEaRQbMLjwrsmidsGJ6wDMv0iazJWk6SfIKnY4Xru8auXgmJkOBa5DUbYFcFD2Rzk2+KDlIiF0GVXNCbgC7g==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", "dev": true, "dependencies": { "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2" + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -9124,6 +8791,16 @@ "postcss": "^8.0.0" } }, + "node_modules/postcss-url/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/postcss-url/node_modules/mime": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", @@ -9343,50 +9020,22 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/read-package-json/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/read-package-json/node_modules/glob": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.1.tgz", - "integrity": "sha512-9BKYcEeIs7QwlCYs+Y3GBvqAMISufUS0i2ELd11zpZjxI5V9iyRj0HgzB5/cLf2NY4vcYBTYzJ7GIui7j/4DOw==", + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", + "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2", - "path-scurry": "^1.10.0" - }, - "bin": { - "glob": "dist/cjs/src/bin.js" - }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/read-package-json/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "node_modules/read-package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", + "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/readable-stream": { @@ -9600,6 +9249,12 @@ "node": ">=8" } }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -9640,10 +9295,52 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/rollup": { - "version": "3.26.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.26.2.tgz", - "integrity": "sha512-6umBIGVz93er97pMgQO08LuH3m6PUb3jlDUUGFsNJB6VgTCUaDFpupf5JfU30529m/UKOgmiX+uY6Sx8cOYpLA==", + "version": "3.26.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.26.3.tgz", + "integrity": "sha512-7Tin0C8l86TkpcMtXvQu6saWH93nhG3dGQ1/+l5V2TDMceTxO7kDiK6GzbfLWNNxqJXm591PcEZUozZm51ogwQ==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -10082,10 +9779,16 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", + "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/sigstore": { "version": "1.7.0", @@ -10681,6 +10384,48 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -11341,12 +11086,6 @@ "ajv": "^6.9.1" } }, - "node_modules/webpack/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/webpack/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -11395,15 +11134,18 @@ } }, "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "dependencies": { "isexe": "^2.0.0" }, "bin": { - "which": "bin/which" + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, "node_modules/wide-align": { diff --git a/package.json b/package.json index 5efc1f2..3e2e4bb 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@angular/core": "^16.1.0", "@angular/platform-browser": "^16.1.0", "@angular/platform-browser-dynamic": "^16.1.0", + "@angular/router": "^16.1.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "uuid": "^9.0.0", diff --git a/projects/ngx-xapi/course/index.ts b/projects/ngx-xapi/course/index.ts new file mode 100644 index 0000000..49f243d --- /dev/null +++ b/projects/ngx-xapi/course/index.ts @@ -0,0 +1 @@ +export * from './src/public-api'; diff --git a/projects/ngx-xapi/course/ng-package.json b/projects/ngx-xapi/course/ng-package.json new file mode 100644 index 0000000..1f7447f --- /dev/null +++ b/projects/ngx-xapi/course/ng-package.json @@ -0,0 +1,6 @@ +{ + "dest": "../../../dist/ngx-xapi/course", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.module.ts b/projects/ngx-xapi/course/src/lib/xapi-course.module.ts new file mode 100644 index 0000000..5e0e8cf --- /dev/null +++ b/projects/ngx-xapi/course/src/lib/xapi-course.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { XapiCourseService } from './xapi-course'; +import { RouterModule } from '@angular/router'; + +@NgModule({ + declarations: [], + imports: [RouterModule.forRoot([])], + exports: [RouterModule], +}) +export class LanguageMapPipeModule {} diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts new file mode 100644 index 0000000..dfe35c4 --- /dev/null +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -0,0 +1,99 @@ +import { HttpClient } from '@angular/common/http'; +import { ActivatedRoute, Params } from '@angular/router'; +import { + Activity, + ActivityDefinition, + Actor, + Context, + Statement, + XapiClient, + completed, +} from '@berry-cloud/ngx-xapi'; +import { Observable, filter, map, take } from 'rxjs'; + +export class XapiCourseService { + public client: XapiClient; + private course: Activity; + private context?: Context; + + constructor( + private httpClient: HttpClient, + endpoint: string, + authorization: string, + private actor: Actor, + activityId: string, + private registration?: string + ) { + this.client = new XapiClient(this.httpClient, { + endpoint, + authorization, + }); + this.course = { + id: activityId, + }; + } + + setCourseDefinition(courseDefinition: ActivityDefinition) { + this.course.definition = courseDefinition; + } + + getXapiClient() { + return this.client; + } + + postCompletedStatement() { + return this.postStatement({ verb: completed }); + } + + postStatement(statement: Partial) { + return this.client.postStatement(this.fillStatement(statement)); + } + + private fillStatement(partial: Partial): Statement { + const statement = { ...partial }; + if (!statement.verb) { + throw new Error('statement.verb is required'); + } + if (!statement.actor) { + statement.actor = this.actor; + } + if (!statement.object) { + statement.object = this.course; + } + if (!statement.context) { + statement.context = this.context ?? {}; + } + if (!statement.context.registration && this.registration) { + statement.context.registration = this.registration; + } + + return statement as Statement; + } +} + +export function xapiCourseServiceFactory( + httpClient: HttpClient, + activatedRoute: ActivatedRoute +): Observable { + return activatedRoute.queryParams.pipe( + filter( + (params: Params) => + params['endpoint'] && + params['auth'] && + params['actor'] && + params['activityId'] + ), + map( + (params: Params) => + new XapiCourseService( + httpClient, + params['endpoint'], + params['auth'], + JSON.parse(params['actor']), + params['activityId'], + params['registration'] + ) + ), + take(1) + ); +} diff --git a/projects/ngx-xapi/course/src/public-api.ts b/projects/ngx-xapi/course/src/public-api.ts new file mode 100644 index 0000000..8f5e3b1 --- /dev/null +++ b/projects/ngx-xapi/course/src/public-api.ts @@ -0,0 +1,6 @@ +/* + * Public API Surface of ngx-xapi/course + */ + +export * from './lib/xapi-course'; +export * from './lib/xapi-course.module'; diff --git a/sonar-project.properties b/sonar-project.properties index fc592f4..44278b5 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -6,8 +6,8 @@ sonar.organization=berrycloud #sonar.projectVersion=1.0 # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. -sonar.sources=projects/ngx-xapi/client/src,projects/ngx-xapi/model/src,projects/ngx-xapi/profiles/cmi5/src,projects/samples/src -sonar.tests=projects/ngx-xapi/client/src,projects/ngx-xapi/model/src,projects/ngx-xapi/profiles/cmi5/src +sonar.sources=projects/ngx-xapi/client/src,projects/ngx-xapi/model/src,projects/ngx-xapi/course/src,projects/ngx-xapi/profiles/cmi5/src,projects/samples/src +sonar.tests=projects/ngx-xapi/client/src,projects/ngx-xapi/model/src,projects/ngx-xapi/course/src,projects/ngx-xapi/profiles/cmi5/src # Encoding of the source code. Default is default system encoding #sonar.sourceEncoding=UTF-8 diff --git a/tsconfig.json b/tsconfig.json index f1ad897..096b955 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,9 @@ "@berry-cloud/ngx-xapi/client": [ "dist/ngx-xapi/client" ], + "@berry-cloud/ngx-xapi/course": [ + "dist/ngx-xapi/course" + ], "@berry-cloud/ngx-xapi": [ "dist/ngx-xapi" ] From 19a8abb2907f2dc7e3407040ca5c6ada673258c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Fri, 21 Jul 2023 17:02:51 +0100 Subject: [PATCH 12/22] more progress on cmi5 --- .../ngx-xapi/client/src/lib/state-options.ts | 7 + .../ngx-xapi/client/src/lib/xapi-client.ts | 9 +- projects/ngx-xapi/client/src/public-api.ts | 1 + .../ngx-xapi/course/src/lib/xapi-course.ts | 285 +++++++++++++++--- .../profiles/cmi5/src/lib/fetch-result.ts | 5 + .../ngx-xapi/profiles/cmi5/src/public-api.ts | 1 + 6 files changed, 270 insertions(+), 38 deletions(-) create mode 100644 projects/ngx-xapi/client/src/lib/state-options.ts create mode 100644 projects/ngx-xapi/profiles/cmi5/src/lib/fetch-result.ts diff --git a/projects/ngx-xapi/client/src/lib/state-options.ts b/projects/ngx-xapi/client/src/lib/state-options.ts new file mode 100644 index 0000000..b83e683 --- /dev/null +++ b/projects/ngx-xapi/client/src/lib/state-options.ts @@ -0,0 +1,7 @@ +export interface StateOptions { + contentType: string; + etag?: string; + match?: boolean; +} + +export type DeleteStateOptions = Omit; diff --git a/projects/ngx-xapi/client/src/lib/xapi-client.ts b/projects/ngx-xapi/client/src/lib/xapi-client.ts index 565b263..976ed4f 100644 --- a/projects/ngx-xapi/client/src/lib/xapi-client.ts +++ b/projects/ngx-xapi/client/src/lib/xapi-client.ts @@ -28,6 +28,7 @@ import { ActivityProfileParams, ActivityProfilesParams, } from './activity-profile-params'; +import { DeleteStateOptions, StateOptions } from './state-options'; export interface XapiConfig { endpoint: string; @@ -110,7 +111,7 @@ export class XapiClient { putState( object: any, stateParams: StateParams, - options: { contentType: string; etag?: string; match?: boolean } + options: StateOptions ): Observable> { return this.config$.pipe( mergeMap((config) => { @@ -134,7 +135,7 @@ export class XapiClient { postState( object: any, stateParams: StateParams, - options: { contentType: string; etag?: string; match?: boolean } + options: StateOptions ): Observable> { return this.config$.pipe( mergeMap((config) => { @@ -159,7 +160,7 @@ export class XapiClient { */ deleteState( stateParams: StateParams, - options: { etag?: string; match?: boolean } + options: DeleteStateOptions ): Observable> { return this.config$.pipe( mergeMap((config) => { @@ -183,7 +184,7 @@ export class XapiClient { */ deleteStates( statesParams: DeleteStatesParams, - options: { etag?: string; match?: boolean } + options: DeleteStateOptions ): Observable> { return this.config$.pipe( mergeMap((config) => { diff --git a/projects/ngx-xapi/client/src/public-api.ts b/projects/ngx-xapi/client/src/public-api.ts index fc4c669..8786e96 100644 --- a/projects/ngx-xapi/client/src/public-api.ts +++ b/projects/ngx-xapi/client/src/public-api.ts @@ -13,4 +13,5 @@ export { GetStatesParams, DeleteStatesParams, } from './lib/state-params'; +export * from './lib/state-options'; export * from './lib/statements-params'; diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index dfe35c4..68e7692 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -1,52 +1,224 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpResponse } from '@angular/common/http'; import { ActivatedRoute, Params } from '@angular/router'; import { Activity, ActivityDefinition, - Actor, + Agent, Context, Statement, - XapiClient, completed, -} from '@berry-cloud/ngx-xapi'; -import { Observable, filter, map, take } from 'rxjs'; + initialized, +} from '@berry-cloud/ngx-xapi/model'; +import { + XapiClient, + StateParams, + StateOptions, +} from '@berry-cloud/ngx-xapi/client'; +import { + Observable, + ReplaySubject, + map, + merge, + mergeMap, + of, + skip, + take, + tap, +} from 'rxjs'; +import { v4 } from 'uuid'; +import { FetchResult, LaunchData } from '@berry-cloud/ngx-xapi/profiles/cmi5'; + +export const DEFAULT_STATE_ID = 'state'; export class XapiCourseService { - public client: XapiClient; - private course: Activity; - private context?: Context; + public client?: ReplaySubject; + private course?: Activity; + private contextTemplate?: Context; + + private launchData?: LaunchData; constructor( private httpClient: HttpClient, - endpoint: string, - authorization: string, - private actor: Actor, - activityId: string, - private registration?: string + endpoint?: string, + authorization?: string, + private agent?: Agent, + private activityId?: string, + private registration?: string, + fetch?: string ) { - this.client = new XapiClient(this.httpClient, { - endpoint, - authorization, - }); - this.course = { - id: activityId, - }; + if (endpoint && agent && activityId && (authorization || fetch)) { + this.course = { + id: activityId, + }; + this.client = new ReplaySubject(1); + if (fetch) { + // fetch authorization from the LMS + fetchCmi5Authorization(httpClient, fetch!) + .pipe( + // create a client from the authorization token + map( + (authorization: string) => + new XapiClient(this.httpClient, { + endpoint, + authorization, + }) + ), + // load cmi5 launch data + mergeMap((client: XapiClient) => + this.getCmi5launchData(client).pipe( + map((launchData: LaunchData) => { + this.launchData = launchData; + this.contextTemplate = launchData.contextTemplate; + return client; + }) + ) + ), + // send cmi5 initialized statement + mergeMap((client: XapiClient) => + this.sendCmi5Initialization(client).pipe( + // initialize client only after the cmi5 initialized statement is sent + tap(() => { + this.client!.next(client); + }) + ) + ) + ) + // All subsequent observables come from httpClient, so they terminate automatically + .subscribe(); + } else if (authorization) { + // initialize only if all required parameters are provided + this.client!.next( + new XapiClient(this.httpClient, { + endpoint, + authorization, + }) + ); + } + this.client!.complete(); + } else if (endpoint || agent || activityId || authorization || fetch) { + throw new Error( + 'Cannot initialize XapiCourseService. Missing required launch parameters.' + ); + } + // If NONE of the launch parameters are provided, + // we assume that the service is used for testing purposes, + // (eg. was launched manually from the browser) + // or was launched from a non-cmi5/non-xapi LMS. + // In this case, the service will not get/send any statements or states. + } + + private getCmi5launchData(client: XapiClient): Observable { + return client + .getState({ + activityId: this.activityId!, + agent: this.agent!, + registration: this.registration, + stateId: 'LMS.LaunchData', + }) + .pipe( + map((response: HttpResponse) => { + if (response.body) { + return response.body; + } + throw new Error('Cannot fetch cmi5 launch data.'); + }) + ); + } + + private sendCmi5Initialization( + client: XapiClient + ): Observable> { + return client.postStatement(this.fillStatement({ verb: initialized })); } setCourseDefinition(courseDefinition: ActivityDefinition) { - this.course.definition = courseDefinition; + if (this.course) { + this.course.definition = courseDefinition; + } } getXapiClient() { return this.client; } - postCompletedStatement() { - return this.postStatement({ verb: completed }); + getLaunchData() { + return this.launchData; + } + + getState(stateId: string) { + if (this.client) { + return this.client.pipe( + mergeMap((client: XapiClient) => + client.getState({ + activityId: this.activityId!, + agent: this.agent!, + registration: this.registration, + stateId, + }) + ) + ); + } else { + // return not found if the client is not initialized + // (no launch parameters were provided) + return of(new HttpResponse({ body: null, status: 404 })); + } + } + + postState(state: T, options: StateOptions, params?: Partial) { + const stateParams = this.fillStateParams(params ?? {}); + if (this.client) { + return this.client.pipe( + mergeMap((client: XapiClient) => + client.postState(state, stateParams, options) + ) + ); + } else { + // emulate a successful response if the client is not initialized + return of(new HttpResponse({ status: 204 })); + } + } + putState(state: T, options: StateOptions, params?: Partial) { + const stateParams = this.fillStateParams(params ?? {}); + if (this.client) { + return this.client.pipe( + mergeMap((client: XapiClient) => + client.putState(state, stateParams, options) + ) + ); + } else { + // emulate a successful response if the client is not initialized + return of(new HttpResponse({ status: 204 })); + } + } + + private fillStateParams(partial: Partial): StateParams { + const stateParams = { ...partial }; + if (!stateParams.activityId) { + stateParams.activityId = this.activityId!; + } + if (!stateParams.agent) { + stateParams.agent = this.agent!; + } + if (!stateParams.registration && this.registration) { + stateParams.registration = this.registration; + } + if (!stateParams.stateId) { + stateParams.stateId = DEFAULT_STATE_ID; + } + return stateParams as StateParams; } postStatement(statement: Partial) { - return this.client.postStatement(this.fillStatement(statement)); + if (this.client) { + return this.client.pipe( + mergeMap((client: XapiClient) => + client.postStatement(this.fillStatement(statement)) + ) + ); + } else { + // emulate a successful response if the client is not initialized + return of(new HttpResponse({ status: 200, body: v4() })); + } } private fillStatement(partial: Partial): Statement { @@ -55,13 +227,20 @@ export class XapiCourseService { throw new Error('statement.verb is required'); } if (!statement.actor) { - statement.actor = this.actor; + statement.actor = this.agent; } if (!statement.object) { statement.object = this.course; } if (!statement.context) { - statement.context = this.context ?? {}; + statement.context = this.contextTemplate ?? {}; + } else { + // merge context template with the provided context + // TODO: this is not a deep merge + statement.context = { + ...this.contextTemplate, + ...statement.context, + }; } if (!statement.context.registration && this.registration) { statement.context.registration = this.registration; @@ -69,20 +248,57 @@ export class XapiCourseService { return statement as Statement; } + + /** + * Convenience method for sending a default completed statement. + */ + sendCompletedStatement() { + return this.postStatement({ verb: completed }); + } +} + +/** + * Convenience method for manually fetching cmi5 authorization token from the LMS. + * + * @remarks + * If this method is used, the XapiCourseService should be initialized with the returned token as an auth parameter. + * Also note that the XapiCourseService won't send the cmi5 initialized statement automatically. + * Using this method is not recommended, but it can be useful if some non-common initialization is required. + */ +export function fetchCmi5Authorization( + httpClient: HttpClient, + fetch: string +): Observable { + return httpClient.get(fetch).pipe( + map((response) => { + if (response['auth-token']) { + return response['auth-token'] as string; + } + if (response['error-text']) { + throw new Error(response['error-text'] as string); + } + if (response['error-code']) { + throw new Error( + 'Cannot fetch cmi5 authorization token. Error code:' + + response['error-code'] + ); + } + throw new Error('Cannot fetch cmi5 authorization token.'); + }) + ); } +/** + * Convenience factory for creating an XapiCourseService from the launch parameters. + * Can be used as a provider in the app-module. + */ export function xapiCourseServiceFactory( httpClient: HttpClient, activatedRoute: ActivatedRoute ): Observable { return activatedRoute.queryParams.pipe( - filter( - (params: Params) => - params['endpoint'] && - params['auth'] && - params['actor'] && - params['activityId'] - ), + // Theoretically the first value is always empty + skip(1), map( (params: Params) => new XapiCourseService( @@ -91,7 +307,8 @@ export function xapiCourseServiceFactory( params['auth'], JSON.parse(params['actor']), params['activityId'], - params['registration'] + params['registration'], + params['fetch'] ) ), take(1) diff --git a/projects/ngx-xapi/profiles/cmi5/src/lib/fetch-result.ts b/projects/ngx-xapi/profiles/cmi5/src/lib/fetch-result.ts new file mode 100644 index 0000000..df0d42a --- /dev/null +++ b/projects/ngx-xapi/profiles/cmi5/src/lib/fetch-result.ts @@ -0,0 +1,5 @@ +export interface FetchResult { + 'auth-token': string; + 'error-text': string; + 'error-code': number; +} diff --git a/projects/ngx-xapi/profiles/cmi5/src/public-api.ts b/projects/ngx-xapi/profiles/cmi5/src/public-api.ts index 178a316..e154755 100644 --- a/projects/ngx-xapi/profiles/cmi5/src/public-api.ts +++ b/projects/ngx-xapi/profiles/cmi5/src/public-api.ts @@ -3,6 +3,7 @@ */ export * from './lib/cmi5-statements'; +export * from './lib/fetch-result'; export * from './lib/launch-data'; export * from './lib/launch'; export * from './lib/learner-preferences'; From 151d42bbc02bd45831f0f64a35ad930ced9071ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Mon, 24 Jul 2023 21:56:17 +0100 Subject: [PATCH 13/22] refactor course project --- .../course/src/lib/xapi-course.module.ts | 10 - .../ngx-xapi/course/src/lib/xapi-course.ts | 478 +++++++++++------- projects/ngx-xapi/course/src/public-api.ts | 1 - .../ngx-xapi/profiles/cmi5/src/lib/launch.ts | 11 +- .../samples/src/app/app-routing.module.ts | 22 + projects/samples/src/app/app.component.html | 10 +- projects/samples/src/app/app.module.ts | 25 +- .../samples/src/app/samples.component.html | 27 + projects/samples/src/app/samples.component.ts | 7 + .../app/xapi-course/xapi-course.component.ts | 57 +++ 10 files changed, 450 insertions(+), 198 deletions(-) delete mode 100644 projects/ngx-xapi/course/src/lib/xapi-course.module.ts create mode 100644 projects/samples/src/app/app-routing.module.ts create mode 100644 projects/samples/src/app/samples.component.html create mode 100644 projects/samples/src/app/samples.component.ts create mode 100644 projects/samples/src/app/xapi-course/xapi-course.component.ts diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.module.ts b/projects/ngx-xapi/course/src/lib/xapi-course.module.ts deleted file mode 100644 index 5e0e8cf..0000000 --- a/projects/ngx-xapi/course/src/lib/xapi-course.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NgModule } from '@angular/core'; -import { XapiCourseService } from './xapi-course'; -import { RouterModule } from '@angular/router'; - -@NgModule({ - declarations: [], - imports: [RouterModule.forRoot([])], - exports: [RouterModule], -}) -export class LanguageMapPipeModule {} diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index 68e7692..1fa34cc 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -1,13 +1,15 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; -import { ActivatedRoute, Params } from '@angular/router'; import { Activity, - ActivityDefinition, - Agent, Context, Statement, + Verb, completed, + experienced, + failed, initialized, + passed, + progressed, } from '@berry-cloud/ngx-xapi/model'; import { XapiClient, @@ -17,102 +19,184 @@ import { import { Observable, ReplaySubject, + catchError, + forkJoin, map, - merge, mergeMap, of, - skip, take, tap, } from 'rxjs'; import { v4 } from 'uuid'; -import { FetchResult, LaunchData } from '@berry-cloud/ngx-xapi/profiles/cmi5'; +import { + FetchResult, + Launch, + LaunchData, +} from '@berry-cloud/ngx-xapi/profiles/cmi5'; +import { Inject, Injectable, InjectionToken, Optional } from '@angular/core'; +import { PlatformLocation } from '@angular/common'; export const DEFAULT_STATE_ID = 'state'; +export const XAPI_LAUNCH = new InjectionToken>( + 'xapi.launch' +); + +export const XAPI_ACTIVITY = new InjectionToken< + Activity | Observable +>('xapi.activity'); + +@Injectable({ + providedIn: 'root', +}) export class XapiCourseService { - public client?: ReplaySubject; + public client: ReplaySubject; private course?: Activity; private contextTemplate?: Context; + private launch?: Launch; private launchData?: LaunchData; constructor( private httpClient: HttpClient, - endpoint?: string, - authorization?: string, - private agent?: Agent, - private activityId?: string, - private registration?: string, - fetch?: string + @Inject(XAPI_ACTIVITY) + activity: Activity | Observable, + @Optional() + @Inject(XAPI_LAUNCH) + launchParameters?: Launch | Observable | null, + @Optional() + platformLocation?: PlatformLocation ) { - if (endpoint && agent && activityId && (authorization || fetch)) { - this.course = { - id: activityId, - }; - this.client = new ReplaySubject(1); - if (fetch) { - // fetch authorization from the LMS - fetchCmi5Authorization(httpClient, fetch!) - .pipe( - // create a client from the authorization token - map( - (authorization: string) => - new XapiClient(this.httpClient, { - endpoint, - authorization, - }) - ), - // load cmi5 launch data - mergeMap((client: XapiClient) => - this.getCmi5launchData(client).pipe( - map((launchData: LaunchData) => { - this.launchData = launchData; - this.contextTemplate = launchData.contextTemplate; - return client; - }) - ) - ), - // send cmi5 initialized statement - mergeMap((client: XapiClient) => - this.sendCmi5Initialization(client).pipe( - // initialize client only after the cmi5 initialized statement is sent - tap(() => { - this.client!.next(client); - }) - ) - ) - ) - // All subsequent observables come from httpClient, so they terminate automatically - .subscribe(); - } else if (authorization) { - // initialize only if all required parameters are provided - this.client!.next( - new XapiClient(this.httpClient, { - endpoint, - authorization, - }) - ); + this.client = new ReplaySubject(1); + + var launch$: Observable; + var activity$: Observable; + + if (activity instanceof Observable) { + activity$ = activity; + } else { + activity$ = of(activity); + } + + if (launchParameters) { + if (launchParameters instanceof Observable) { + launch$ = launchParameters; + } else { + launch$ = of(launchParameters); } - this.client!.complete(); - } else if (endpoint || agent || activityId || authorization || fetch) { - throw new Error( - 'Cannot initialize XapiCourseService. Missing required launch parameters.' - ); + } else if (platformLocation?.search) { + const params = new URLSearchParams(platformLocation.search); + launch$ = of({ + endpoint: params.get('endpoint'), + auth: params.get('auth'), + actor: params.get('actor') && JSON.parse(params.get('actor')!), + activityId: params.get('activityId'), + registration: params.get('registration'), + fetch: params.get('fetch'), + } as Launch); + } else { + launch$ = of({} as Launch); } - // If NONE of the launch parameters are provided, - // we assume that the service is used for testing purposes, - // (eg. was launched manually from the browser) - // or was launched from a non-cmi5/non-xapi LMS. - // In this case, the service will not get/send any statements or states. + + forkJoin([activity$, launch$]) + .pipe( + mergeMap(([activity, launch]) => { + this.course = { + ...activity, + }; + if (launch.activityId) { + this.course.id = launch.activityId; + } + if ( + launch.endpoint && + launch.actor && + (launch.auth || launch.fetch) + ) { + this.launch = launch; + + if (launch.fetch) { + // fetch authorization from the LMS + return fetchCmi5Authorization(httpClient, launch.fetch!).pipe( + // create a client from the authorization token + map( + (authorization: string) => + new XapiClient(this.httpClient, { + endpoint: launch.endpoint, + authorization, + }) + ), + // load cmi5 launch data + mergeMap((client: XapiClient) => + this.getCmi5launchData(client).pipe( + map((launchData: LaunchData) => { + this.launchData = launchData; + this.contextTemplate = launchData.contextTemplate; + return client; + }) + ) + ), + // send cmi5 initialized statement + mergeMap((client: XapiClient) => + this.sendCmi5Initialization(client).pipe( + // initialize client only after the cmi5 initialized statement is sent + tap(() => { + this.client!.next(client); + this.client!.complete(); + }) + ) + ) + ); + } else if (launch.auth) { + // initialize only if all required parameters are provided + this.client!.next( + new XapiClient(this.httpClient, { + endpoint: launch.endpoint, + authorization: launch.auth, + }) + ); + this.client!.complete(); + } + return of(undefined); + } else if ( + launch.endpoint || + launch.actor || + launch.auth || + launch.fetch + ) { + throw new Error( + 'Cannot initialize XapiCourseService. Missing required launch parameters.' + ); + } else { + // If NO launch parameters are provided, + // we assume that the service is used for testing purposes, + // (eg. was launched manually from the browser) + // or was launched from a non-cmi5/non-xapi LMS. + // In this case, the service will not get/send any statements or states. + this.client.next(undefined); + this.client.complete(); + return of(undefined); + } + }), + catchError((error) => { + // In case of error, the client will not be initialized. + // This means that the service will not get/send any statements or states. + // But otherwise it will work normally. + this.client.next(undefined); + this.client.complete(); + // We rethrow the error so that the app can handle it. + throw error; + }), + take(1) + ) + .subscribe(); } private getCmi5launchData(client: XapiClient): Observable { return client .getState({ - activityId: this.activityId!, - agent: this.agent!, - registration: this.registration, + activityId: this.course!.id, + agent: this.launch!.actor!, + registration: this.launch!.registration, stateId: 'LMS.LaunchData', }) .pipe( @@ -131,103 +215,129 @@ export class XapiCourseService { return client.postStatement(this.fillStatement({ verb: initialized })); } - setCourseDefinition(courseDefinition: ActivityDefinition) { - if (this.course) { - this.course.definition = courseDefinition; - } - } - - getXapiClient() { + getXapiClient(): Observable { return this.client; } - getLaunchData() { + getCmi5LaunchData() { return this.launchData; } getState(stateId: string) { - if (this.client) { - return this.client.pipe( - mergeMap((client: XapiClient) => - client.getState({ - activityId: this.activityId!, - agent: this.agent!, - registration: this.registration, - stateId, - }) - ) - ); - } else { - // return not found if the client is not initialized - // (no launch parameters were provided) - return of(new HttpResponse({ body: null, status: 404 })); - } + return this.client.pipe( + mergeMap((client) => + client + ? client.getState({ + activityId: this.launch!.activityId!, + agent: this.launch!.actor, + registration: this.launch!.registration, + stateId, + }) + : // return not found if the client is not initialized + // (no launch parameters were provided) + of(new HttpResponse({ body: null, status: 404 })) + ) + ); } postState(state: T, options: StateOptions, params?: Partial) { const stateParams = this.fillStateParams(params ?? {}); - if (this.client) { - return this.client.pipe( - mergeMap((client: XapiClient) => - client.postState(state, stateParams, options) - ) - ); - } else { - // emulate a successful response if the client is not initialized - return of(new HttpResponse({ status: 204 })); - } + return this.client.pipe( + mergeMap((client) => + client + ? client.postState(state, stateParams, options) + : of(new HttpResponse({ status: 204 })) + ) + ); } + putState(state: T, options: StateOptions, params?: Partial) { const stateParams = this.fillStateParams(params ?? {}); - if (this.client) { - return this.client.pipe( - mergeMap((client: XapiClient) => - client.putState(state, stateParams, options) - ) - ); - } else { - // emulate a successful response if the client is not initialized - return of(new HttpResponse({ status: 204 })); - } + return this.client.pipe( + mergeMap((client) => + client + ? client.putState(state, stateParams, options) + : of(new HttpResponse({ status: 204 })) + ) + ); } private fillStateParams(partial: Partial): StateParams { const stateParams = { ...partial }; if (!stateParams.activityId) { - stateParams.activityId = this.activityId!; + stateParams.activityId = this.course?.id; } if (!stateParams.agent) { - stateParams.agent = this.agent!; + stateParams.agent = this.launch?.actor; } - if (!stateParams.registration && this.registration) { - stateParams.registration = this.registration; + if (!stateParams.registration && this.launch?.registration) { + stateParams.registration = this.launch?.registration; } if (!stateParams.stateId) { stateParams.stateId = DEFAULT_STATE_ID; } + return stateParams as StateParams; } - postStatement(statement: Partial) { - if (this.client) { - return this.client.pipe( - mergeMap((client: XapiClient) => - client.postStatement(this.fillStatement(statement)) - ) - ); - } else { - // emulate a successful response if the client is not initialized - return of(new HttpResponse({ status: 200, body: v4() })); - } + /** + * Sends a statement to the LRS. + * If the client is not initialized (no launch parameters were provided), + * returns OK and a random uuid. + * + * @param param Either a partial statement which will be extended by the default statement, + * or a function that takes the default statement as a parameter and returns a partial statement. + * If not provided, a default statement will be sent. + * + * The default statement contains the following fields: + * + * actor: the actor from the launch parameters + * + * verb: experienced + * + * object: the XAPI_ACTIVITY + optional id from the launch parameters + * + * context: the context template from the launch parameters + * + * context.registration: the registration from the launch parameters + * + * If a partial statement is provided, the above fields will be overridden by the default statement. + * If a function is provided, the above fields will be overridden by the result of the function. + * + * @returns An observable of the HttpResponse from the LRS. (The body is the uuid of the statement) + */ + postStatement( + param?: Partial | ((defaultStatement: Statement) => Statement) + ): Observable> { + const verb = + typeof param === 'function' ? experienced : param?.verb || experienced; + return this.postStatementWithVerb(verb, param); + } + + private postStatementWithVerb( + verb: Verb, + param?: Partial | ((defaultStatement: Statement) => Statement) + ): Observable> { + const statement = + typeof param === 'function' + ? param(this.fillStatement({ verb })) + : this.fillStatement({ ...param, verb }); + + return this.client.pipe( + mergeMap((client) => + client + ? client.postStatement(statement) + : // return OK and a random uuid if the client is not initialized + // (no launch parameters were provided) + of(new HttpResponse({ status: 200, body: v4() })) + ) + ); } private fillStatement(partial: Partial): Statement { const statement = { ...partial }; - if (!statement.verb) { - throw new Error('statement.verb is required'); - } if (!statement.actor) { - statement.actor = this.agent; + statement.actor = this.launch?.actor; } if (!statement.object) { statement.object = this.course; @@ -242,8 +352,8 @@ export class XapiCourseService { ...statement.context, }; } - if (!statement.context.registration && this.registration) { - statement.context.registration = this.registration; + if (!statement.context.registration && this.launch?.registration) { + statement.context.registration = this.launch?.registration; } return statement as Statement; @@ -251,9 +361,66 @@ export class XapiCourseService { /** * Convenience method for sending a default completed statement. + * @see {@link postStatement} */ - sendCompletedStatement() { - return this.postStatement({ verb: completed }); + sendCompletedStatement( + param?: Partial | ((defaultStatement: Statement) => Statement) + ) { + return this.postStatementWithVerb(completed, param); + } + + /** + * Convenience method for sending a default passed statement. + * @see {@link postStatement} + */ + sendPassedStatement( + param?: Partial | ((defaultStatement: Statement) => Statement) + ) { + return this.postStatementWithVerb(passed, param); + } + + /** + * Convenience method for sending a default failed statement. + * @see {@link postStatement} + */ + sendFailedStatement( + param?: Partial | ((defaultStatement: Statement) => Statement) + ) { + return this.postStatementWithVerb(failed, param); + } + + /** + * Convenience method for sending a default progressed statement. + * @param progress The progress value (0-100) + * @see {@link postStatement} + */ + sendProgressedStatement( + progress: number, + param?: Partial | ((defaultStatement: Statement) => Statement) + ) { + let statement = + typeof param === 'function' + ? param(this.fillStatement({ verb: progressed })) + : this.fillStatement({ ...param, verb: progressed }); + + statement = { + ...statement, + result: { + ...statement?.result, + extensions: { + ...statement?.result?.extensions, + 'http://id.tincanapi.com/extension/progress': progress, + }, + }, + }; + + return this.client.pipe( + mergeMap((client) => + client + ? client.postStatement(statement) + : of(new HttpResponse({ status: 200, body: v4() })) + ) + ); } } @@ -262,14 +429,14 @@ export class XapiCourseService { * * @remarks * If this method is used, the XapiCourseService should be initialized with the returned token as an auth parameter. - * Also note that the XapiCourseService won't send the cmi5 initialized statement automatically. + * Also note that the XapiCourseService won't send the cmi5 initialized statement automatically in this case. * Using this method is not recommended, but it can be useful if some non-common initialization is required. */ export function fetchCmi5Authorization( httpClient: HttpClient, fetch: string ): Observable { - return httpClient.get(fetch).pipe( + return httpClient.post(fetch, null).pipe( map((response) => { if (response['auth-token']) { return response['auth-token'] as string; @@ -287,30 +454,3 @@ export function fetchCmi5Authorization( }) ); } - -/** - * Convenience factory for creating an XapiCourseService from the launch parameters. - * Can be used as a provider in the app-module. - */ -export function xapiCourseServiceFactory( - httpClient: HttpClient, - activatedRoute: ActivatedRoute -): Observable { - return activatedRoute.queryParams.pipe( - // Theoretically the first value is always empty - skip(1), - map( - (params: Params) => - new XapiCourseService( - httpClient, - params['endpoint'], - params['auth'], - JSON.parse(params['actor']), - params['activityId'], - params['registration'], - params['fetch'] - ) - ), - take(1) - ); -} diff --git a/projects/ngx-xapi/course/src/public-api.ts b/projects/ngx-xapi/course/src/public-api.ts index 8f5e3b1..fa91456 100644 --- a/projects/ngx-xapi/course/src/public-api.ts +++ b/projects/ngx-xapi/course/src/public-api.ts @@ -3,4 +3,3 @@ */ export * from './lib/xapi-course'; -export * from './lib/xapi-course.module'; diff --git a/projects/ngx-xapi/profiles/cmi5/src/lib/launch.ts b/projects/ngx-xapi/profiles/cmi5/src/lib/launch.ts index ccc7f9a..06fb4ab 100644 --- a/projects/ngx-xapi/profiles/cmi5/src/lib/launch.ts +++ b/projects/ngx-xapi/profiles/cmi5/src/lib/launch.ts @@ -1,9 +1,10 @@ -import { Actor } from '@berry-cloud/ngx-xapi/model'; +import { Agent } from '@berry-cloud/ngx-xapi/model'; export interface Launch { endpoint: string; // URL - fetch: string; // URL - actor: Actor; // Agent - registration: string; // UUID - activityId: string; // URL + fetch?: string; // URL + actor: Agent; // Agent + registration?: string; // UUID + activityId?: string; // URL + auth?: string; // Authorization for xAPI } diff --git a/projects/samples/src/app/app-routing.module.ts b/projects/samples/src/app/app-routing.module.ts new file mode 100644 index 0000000..89b6297 --- /dev/null +++ b/projects/samples/src/app/app-routing.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { XapiCourseComponent } from './xapi-course/xapi-course.component'; +import { AppComponent } from './app.component'; +import { SamplesComponent } from './samples.component'; + +const routes: Routes = [ + { + path: '', + component: SamplesComponent, + }, + { + path: 'xapi-course', + component: XapiCourseComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], +}) +export class AppRoutingModule {} diff --git a/projects/samples/src/app/app.component.html b/projects/samples/src/app/app.component.html index 83842d3..0680b43 100644 --- a/projects/samples/src/app/app.component.html +++ b/projects/samples/src/app/app.component.html @@ -1,9 +1 @@ -

Samples

- - - - - - - - + diff --git a/projects/samples/src/app/app.module.ts b/projects/samples/src/app/app.module.ts index 74fee46..25d125a 100644 --- a/projects/samples/src/app/app.module.ts +++ b/projects/samples/src/app/app.module.ts @@ -1,31 +1,48 @@ -import { NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { HttpClientModule } from '@angular/common/http'; -import { XAPI_CONFIG, XapiConfig } from '@berry-cloud/ngx-xapi'; +import { Activity, XAPI_CONFIG, XapiConfig } from '@berry-cloud/ngx-xapi'; import { AppComponent } from './app.component'; import { GetStateComponent } from './get-state/get-state.component'; import { GetStatementComponent } from './get-statement/get-statement.component'; import { PostStateComponent } from './post-state/post-state.component'; import { PostStatementComponent } from './post-statement/post-statement.component'; +import { AppRoutingModule } from './app-routing.module'; +import { SamplesComponent } from './samples.component'; +import { XapiCourseComponent } from './xapi-course/xapi-course.component'; +import { XAPI_ACTIVITY } from '@berry-cloud/ngx-xapi/course'; @NgModule({ declarations: [ AppComponent, + SamplesComponent, PostStatementComponent, GetStatementComponent, PostStateComponent, GetStateComponent, + XapiCourseComponent, ], - imports: [BrowserModule, HttpClientModule], + imports: [BrowserModule, HttpClientModule, AppRoutingModule], providers: [ { provide: XAPI_CONFIG, useValue: { endpoint: 'https://lrs.adlnet.gov/xapi/', - authorization: '', + authorization: 'Basic dGVzdDIzOnRlc3QyMzIz', } as XapiConfig, }, + { + provide: XAPI_ACTIVITY, + useValue: { + id: 'https://berrycloud.co.uk/xapi/sample', + definition: { + name: { + 'en-US': 'BerryCloud Sample Course', + }, + }, + } as Activity, + }, ], bootstrap: [AppComponent], }) diff --git a/projects/samples/src/app/samples.component.html b/projects/samples/src/app/samples.component.html new file mode 100644 index 0000000..154a753 --- /dev/null +++ b/projects/samples/src/app/samples.component.html @@ -0,0 +1,27 @@ + + +

xAPI Client Samples

+ + + + + + + + diff --git a/projects/samples/src/app/samples.component.ts b/projects/samples/src/app/samples.component.ts new file mode 100644 index 0000000..c4d40bb --- /dev/null +++ b/projects/samples/src/app/samples.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'samples-root', + templateUrl: './samples.component.html', +}) +export class SamplesComponent {} diff --git a/projects/samples/src/app/xapi-course/xapi-course.component.ts b/projects/samples/src/app/xapi-course/xapi-course.component.ts new file mode 100644 index 0000000..2ac312c --- /dev/null +++ b/projects/samples/src/app/xapi-course/xapi-course.component.ts @@ -0,0 +1,57 @@ +import { Component } from '@angular/core'; +import { Statement, answered, experienced } from '@berry-cloud/ngx-xapi'; +import { XapiCourseService } from '@berry-cloud/ngx-xapi/course'; +import { Observable } from 'rxjs'; + +@Component({ + selector: 'xapi-course', + template: ` +

xAPI Course Sample

+ +

+ Try deleting/modifiying/extending the launch parameters in the URL bar and + check the result in the developer tools' console and network tab

+ Mandatory parameters are: endpoint (URI), actor (JSON)
+ Authorization parameter is one of: auth (authorization header, tincan + only), fetch (URI, CMI5 only)
+ Optional parameters are: activityId (URI), registration (UUID) +

+ + Default Statement: {{ (defaultStatement$ | async)?.body }} +
+ Unique Activity: {{ (sampleStatement$ | async)?.body }} +
+ Completed Statement: {{ (completedStatement$ | async)?.body }} +
+ Callback Statement: {{ (callbackStatement$ | async)?.body }} + `, + providers: [], +}) +export class XapiCourseComponent { + defaultStatement$: Observable = this.courseService.postStatement(); + + sampleStatement$: Observable = this.courseService.postStatement({ + verb: experienced, + object: { + id: 'http://example.com/activities/sample-activity', + definition: { + name: { + 'en-US': 'Sample Activity', + }, + }, + }, + }); + + completedStatement$: Observable = + this.courseService.sendCompletedStatement(); + + callbackStatement$: Observable = this.courseService.postStatement( + (defaultStatement) => + ({ + ...defaultStatement, + verb: answered, + } as Statement) + ); + + constructor(public courseService: XapiCourseService) {} +} From 84ce4424fa89cfed4ca1f0fdb5aa0e05e7841251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Tue, 25 Jul 2023 10:33:26 +0100 Subject: [PATCH 14/22] improve statement merging --- .../ngx-xapi/course/src/lib/xapi-course.ts | 143 ++++++++++++++---- 1 file changed, 113 insertions(+), 30 deletions(-) diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index 1fa34cc..10219a6 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -10,6 +10,7 @@ import { initialized, passed, progressed, + scored, } from '@berry-cloud/ngx-xapi/model'; import { XapiClient, @@ -212,7 +213,7 @@ export class XapiCourseService { private sendCmi5Initialization( client: XapiClient ): Observable> { - return client.postStatement(this.fillStatement({ verb: initialized })); + return client.postStatement(this.fillStatement(initialized)); } getXapiClient(): Observable { @@ -301,17 +302,21 @@ export class XapiCourseService { * * context.registration: the registration from the launch parameters * - * If a partial statement is provided, the above fields will be overridden by the default statement. - * If a function is provided, the above fields will be overridden by the result of the function. + * @remarks Beware! If you use a callback function, you can override the mandatory properties + * come from the cmi5 context template or launch parameters. + * (eg. registration, contextActivities, extensions etc.) + * In common scenarios, this is a bad practice. However, it can be useful in some cases. + * (eg. if you want to send a statement about a side effect with a different properties + * than the cmi5 context template or launch parameters provide for the main statement) + * + * If you use a partial statement, these mandatory properties will be merged with the partial statement. * * @returns An observable of the HttpResponse from the LRS. (The body is the uuid of the statement) */ postStatement( param?: Partial | ((defaultStatement: Statement) => Statement) ): Observable> { - const verb = - typeof param === 'function' ? experienced : param?.verb || experienced; - return this.postStatementWithVerb(verb, param); + return this.postStatementWithVerb(experienced, param); } private postStatementWithVerb( @@ -320,8 +325,8 @@ export class XapiCourseService { ): Observable> { const statement = typeof param === 'function' - ? param(this.fillStatement({ verb })) - : this.fillStatement({ ...param, verb }); + ? param(this.fillStatement(verb)) + : this.fillStatement(verb, param); return this.client.pipe( mergeMap((client) => @@ -334,26 +339,69 @@ export class XapiCourseService { ); } - private fillStatement(partial: Partial): Statement { - const statement = { ...partial }; - if (!statement.actor) { - statement.actor = this.launch?.actor; - } - if (!statement.object) { - statement.object = this.course; - } - if (!statement.context) { - statement.context = this.contextTemplate ?? {}; - } else { - // merge context template with the provided context - // TODO: this is not a deep merge - statement.context = { - ...this.contextTemplate, - ...statement.context, - }; - } - if (!statement.context.registration && this.launch?.registration) { - statement.context.registration = this.launch?.registration; + private fillStatement(verb: Verb, partial?: Partial): Statement { + const statement = { + ...partial, + ...(!partial?.actor && { actor: this.launch?.actor }), + ...(!partial?.verb && { verb }), + ...(!partial?.object && { object: this.course }), + }; + + if (this.contextTemplate || this.launch?.registration) { + // if context template or registration is provided, we need to add a context + if (!partial?.context) { + statement.context = { + ...this.contextTemplate, + registration: this.launch?.registration, + }; + } else { + // merge context template with the provided context + statement.context = { + ...partial.context, + ...this.contextTemplate, + registration: this.launch?.registration, + ...(partial.context.extensions && { + extensions: { + ...partial.context?.extensions, + ...this.contextTemplate?.extensions, + }, + }), + ...(partial.context.contextActivities && { + contextActivities: { + ...partial.context.contextActivities, + ...this.contextTemplate?.contextActivities, + ...(partial.context.contextActivities.category && + this.contextTemplate?.contextActivities?.category && { + category: [ + ...partial.context.contextActivities.category, + ...this.contextTemplate.contextActivities.category, + ], + }), + ...(partial.context.contextActivities.parent && + this.contextTemplate?.contextActivities?.parent && { + parent: [ + ...partial.context.contextActivities.parent, + ...this.contextTemplate.contextActivities.parent, + ], + }), + ...(partial.context.contextActivities.grouping && + this.contextTemplate?.contextActivities?.grouping && { + grouping: [ + ...partial.context.contextActivities.grouping, + ...this.contextTemplate.contextActivities.grouping, + ], + }), + ...(partial.context.contextActivities.other && + this.contextTemplate?.contextActivities?.other && { + other: [ + ...partial.context.contextActivities.other, + ...this.contextTemplate.contextActivities.other, + ], + }), + }, + }), + }; + } } return statement as Statement; @@ -400,8 +448,8 @@ export class XapiCourseService { ) { let statement = typeof param === 'function' - ? param(this.fillStatement({ verb: progressed })) - : this.fillStatement({ ...param, verb: progressed }); + ? param(this.fillStatement(progressed)) + : this.fillStatement(progressed, param); statement = { ...statement, @@ -422,6 +470,41 @@ export class XapiCourseService { ) ); } + + /** + * Convenience method for sending a default scored statement. + * @param score The score value (0-100) + * @see {@link postStatement} + */ + sendScoredStatement( + score: number, + param?: Partial | ((defaultStatement: Statement) => Statement) + ) { + let statement = + typeof param === 'function' + ? param(this.fillStatement(scored)) + : this.fillStatement(scored, param); + + statement = { + ...statement, + result: { + ...statement?.result, + score: { + ...statement?.result?.score, + raw: score, + scaled: score / 100, + }, + }, + }; + + return this.client.pipe( + mergeMap((client) => + client + ? client.postStatement(statement) + : of(new HttpResponse({ status: 200, body: v4() })) + ) + ); + } } /** From 5fc59c2c399954c514663ea725f85a57a3c1c76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Tue, 25 Jul 2023 13:50:01 +0100 Subject: [PATCH 15/22] extends readme with course initialization --- projects/ngx-xapi/README.md | 131 ++++++++++++++++-- .../ngx-xapi/course/src/lib/xapi-course.ts | 40 ++---- projects/ngx-xapi/src/public-api.ts | 3 - projects/samples/src/app/app.module.ts | 6 +- .../app/xapi-course/xapi-course.component.ts | 2 +- 5 files changed, 140 insertions(+), 42 deletions(-) diff --git a/projects/ngx-xapi/README.md b/projects/ngx-xapi/README.md index 59a1494..da914e8 100644 --- a/projects/ngx-xapi/README.md +++ b/projects/ngx-xapi/README.md @@ -21,17 +21,25 @@ The package contains the following entry-points: @berry-cloud/ngx-xapi/model @berry-cloud/ngx-xapi/client @berry-cloud/ngx-xapi/profiles/cmi5 +@berry-cloud/ngx-xapi/course ``` `@berry-cloud/ngx-xapi/model` contains the core types for xAPI. (Statement, Actor, Verb, etc.) `@berry-cloud/ngx-xapi/client` contains utility methods for communicating with an LRS. `@berry-cloud/ngx-xapi/profiles/cmi5` contains types and extensions for the cmi5 profile. +`@berry-cloud/ngx-xapi/course` contains utility methods for a tincan or cmi5 course player. -All of the exported types and methods from model and client can be accessed directly from `@berry-cloud/ngx-xapi` entry point too. +## Samples + +See [BerryCloud/ngx-xapi GitHub repository](https://github.com/BerryCloud/ngx-xapi) for [Sample application](https://github.com/BerryCloud/ngx-xapi/tree/main/projects/samples) + +It contains simple examples for the client and the course utilities. + +## Client Utilities -## Configuration injection +The client utilities can be accessed via the injectable `XapiClient` service. -If you plan to use the client methods, you must provide an `XapiConfig` to be injected into the `XapiClient`. +If you plan to use this service, you must provide an `XapiConfig` to be injected into the `XapiClient`. The HttpClientModule must also be imported. For example: @@ -116,11 +124,7 @@ function xapiConfigFactory(userService: UserService) { export class AppModule {} ``` -## Samples - -See [BerryCloud/ngx-xapi GitHub repository](https://github.com/BerryCloud/ngx-xapi) for [Sample application](https://github.com/BerryCloud/ngx-xapi/tree/main/projects/samples) - -## Post Statement +### Post Statement ```TypeScript postPassedStatement() { @@ -144,7 +148,7 @@ postPassedStatement() { } ``` -## Post State +### Post State ```TypeScript postState(state: any) { @@ -184,7 +188,7 @@ Example:

``` -## Handling Responses +### Handling Responses Most of the `XapiClient` utility methods return a `Observable>` object. Although in most cases `Observable` would be enough, some important properties of the response can be gathered only from the response headers: @@ -192,3 +196,110 @@ Most of the `XapiClient` utility methods return a `Observable>` - X-Experience-API-Consistent-Through You can access the response object itself via `response.body`; + +## Course Utilities + +The course utilities can be accessed via the injectable `XapiCourse` service. +It contains useful utility methods for a tincan or cmi5 compatible course player. + +If you plan to use this service, you must provide an `Activity` object to be injected into the `XapiCourse`. +This is the default activity object of the tincan/cmi5 course. + +For example: + +```TypeScript +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { XAPI_ACTIVITY } from '@berry-cloud/ngx-xapi/course'; +import { Activity } from '@berry-cloud/ngx-xapi/model'; +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; + +@NgModule({ + declarations: [AppComponent], + imports: [ + BrowserModule, + // import HttpClientModule after BrowserModule. + HttpClientModule, + AppRoutingModule, + ], + providers: [ + provide: XAPI_ACTIVITY, + useValue: { + id: 'https://berrycloud.co.uk/xapi/sample', + definition: { + name: { + 'en-US': 'BerryCloud Sample Course', + }, + }, + } as Activity, + ], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +The `XapiCourse` service also automatically picks up the launch parameters from the URL when the it is initialized. If the launch parameters come from a different source, or you want to hide the URL parameters before the service is initialized, you can provide this parameters via injection too: + +```TypeScript +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { XAPI_ACTIVITY, XAPI_LAUNCH, } from '@berry-cloud/ngx-xapi/course'; +import { Launch } from '@berry-cloud/ngx-xapi/profiles/cmi5'; +import { Activity } from '@berry-cloud/ngx-xapi/model'; +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; + +@NgModule({ + declarations: [AppComponent], + imports: [ + BrowserModule, + // import HttpClientModule after BrowserModule. + HttpClientModule, + AppRoutingModule, + ], + providers: [ + provide: XAPI_ACTIVITY, + useValue: { + id: 'https://berrycloud.co.uk/xapi/sample', + definition: { + name: { + 'en-US': 'BerryCloud Sample Course', + }, + }, + } as Activity, + provide: XAPI_LAUNCH, + useValue: { + endpoint: 'https://mylrs.com/xapi'; + actor: { + name: 'test user' + mbox: 'mailto:test@example.com'; + }; + registration: 'bc6c2d1e-6f5e-4023-83fd-01d89d5bfa32'; + activityId: 'http://example.com/activity-from-launc-parameter'; + auth: 'Basic *******'; + fetch: 'https://mylms.com/token/12345678901234567890' + } as Launch, + ], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +`XAPI_LAUNCH` can be a `Launch` or `Observable`, so if can provide it via a factory method which loads it asynchronously from a file or the URL. + +The `XapiCourse` service can handle both the tincan `auth` parameter or the `cmi5` launch parameter. If the latter is provided it automatically fetches the auth token from the provided endpoint. In this case it also automatically loads the cmi5 launch-data, and will use it to decorate each further state or statement requests. It also automatically sends the mandatory cmi5 `initialized` statement during initialization. + +If the launch parameters are not provided and neither cannot be picked from the URL bar the `XapiCourse` service still initializes itself, but throw an `Error`. +This error must be handled by the application. If it's ignored then the course will still run but it will silently ignore any http requests to the LRS. It can be useful for testing or when a course is launched locally and no need to store progress data. + +If neither of the above initialization methods are suitable, you can create the `XapiCourse` manually: + +``` + const course = new XapiCourse(activity, launch); +``` + +The `XapiCourse` uses the `XapiClient` internally, but extends it functionality with some useful convenient methods. If these methods don't fit for usecase you can still get an `Observable` via the `getXapiClient()` method. +It is `undefined` if the launch parameters were not provided or were deficient. + +## Sending State diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index 10219a6..d9f9512 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -224,16 +224,12 @@ export class XapiCourseService { return this.launchData; } - getState(stateId: string) { + getState(stateId?: string) { + const stateParams = this.fillStateParams({ stateId }); return this.client.pipe( mergeMap((client) => client - ? client.getState({ - activityId: this.launch!.activityId!, - agent: this.launch!.actor, - registration: this.launch!.registration, - stateId, - }) + ? client.getState(stateParams) : // return not found if the client is not initialized // (no launch parameters were provided) of(new HttpResponse({ body: null, status: 404 })) @@ -242,7 +238,7 @@ export class XapiCourseService { } postState(state: T, options: StateOptions, params?: Partial) { - const stateParams = this.fillStateParams(params ?? {}); + const stateParams = this.fillStateParams(params); return this.client.pipe( mergeMap((client) => client @@ -253,7 +249,7 @@ export class XapiCourseService { } putState(state: T, options: StateOptions, params?: Partial) { - const stateParams = this.fillStateParams(params ?? {}); + const stateParams = this.fillStateParams(params); return this.client.pipe( mergeMap((client) => client @@ -263,22 +259,16 @@ export class XapiCourseService { ); } - private fillStateParams(partial: Partial): StateParams { - const stateParams = { ...partial }; - if (!stateParams.activityId) { - stateParams.activityId = this.course?.id; - } - if (!stateParams.agent) { - stateParams.agent = this.launch?.actor; - } - if (!stateParams.registration && this.launch?.registration) { - stateParams.registration = this.launch?.registration; - } - if (!stateParams.stateId) { - stateParams.stateId = DEFAULT_STATE_ID; - } - - return stateParams as StateParams; + private fillStateParams(partial?: Partial): StateParams { + return { + ...partial, + ...(!partial?.activityId && { activityId: this.course?.id }), + ...(!partial?.agent && { agent: this.launch?.actor }), + ...(!partial?.registration && { + registration: this.launch?.registration, + }), + ...(!partial?.stateId && { stateId: DEFAULT_STATE_ID }), + } as StateParams; } /** diff --git a/projects/ngx-xapi/src/public-api.ts b/projects/ngx-xapi/src/public-api.ts index 49cdfef..4037539 100644 --- a/projects/ngx-xapi/src/public-api.ts +++ b/projects/ngx-xapi/src/public-api.ts @@ -3,6 +3,3 @@ */ export const XAPI_VERSION = '1.0.3'; - -export * from '@berry-cloud/ngx-xapi/model'; -export * from '@berry-cloud/ngx-xapi/client'; diff --git a/projects/samples/src/app/app.module.ts b/projects/samples/src/app/app.module.ts index 25d125a..853ba75 100644 --- a/projects/samples/src/app/app.module.ts +++ b/projects/samples/src/app/app.module.ts @@ -1,8 +1,6 @@ -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; - import { HttpClientModule } from '@angular/common/http'; -import { Activity, XAPI_CONFIG, XapiConfig } from '@berry-cloud/ngx-xapi'; import { AppComponent } from './app.component'; import { GetStateComponent } from './get-state/get-state.component'; import { GetStatementComponent } from './get-statement/get-statement.component'; @@ -12,6 +10,8 @@ import { AppRoutingModule } from './app-routing.module'; import { SamplesComponent } from './samples.component'; import { XapiCourseComponent } from './xapi-course/xapi-course.component'; import { XAPI_ACTIVITY } from '@berry-cloud/ngx-xapi/course'; +import { XAPI_CONFIG, XapiConfig } from '@berry-cloud/ngx-xapi/client'; +import { Activity } from '@berry-cloud/ngx-xapi/model'; @NgModule({ declarations: [ diff --git a/projects/samples/src/app/xapi-course/xapi-course.component.ts b/projects/samples/src/app/xapi-course/xapi-course.component.ts index 2ac312c..c473e9b 100644 --- a/projects/samples/src/app/xapi-course/xapi-course.component.ts +++ b/projects/samples/src/app/xapi-course/xapi-course.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { Statement, answered, experienced } from '@berry-cloud/ngx-xapi'; +import { Statement, answered, experienced } from '@berry-cloud/ngx-xapi/model'; import { XapiCourseService } from '@berry-cloud/ngx-xapi/course'; import { Observable } from 'rxjs'; From 69047c2d96b623b3b81cc070f06a175e5bbadec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Tue, 25 Jul 2023 15:11:00 +0100 Subject: [PATCH 16/22] more README, + small fixes and upgrades --- projects/ngx-xapi/README.md | 147 +++++++++++++++++- .../ngx-xapi/client/src/lib/xapi-client.ts | 2 +- .../ngx-xapi/course/src/lib/xapi-course.ts | 36 +++-- 3 files changed, 172 insertions(+), 13 deletions(-) diff --git a/projects/ngx-xapi/README.md b/projects/ngx-xapi/README.md index da914e8..f880b32 100644 --- a/projects/ngx-xapi/README.md +++ b/projects/ngx-xapi/README.md @@ -300,6 +300,149 @@ If neither of the above initialization methods are suitable, you can create the ``` The `XapiCourse` uses the `XapiClient` internally, but extends it functionality with some useful convenient methods. If these methods don't fit for usecase you can still get an `Observable` via the `getXapiClient()` method. -It is `undefined` if the launch parameters were not provided or were deficient. +It is `undefined` if the launch parameters were not provided or were deficient. See the `XapiClient` examples above. -## Sending State +## Sending a State + +After the `XapiCourse` was successfully initialized, you can send a state with the `putState` or `postState` method. It has only one mandatory argument, the state to be sent. All parameters needed for the LRS request are filled or defaulted in by the `XapiCourse`. + +default stateId: `progress` + +default content-type: `application/json` + +If you want to use different `stateId` or send a state with any other properties, you can override any of the defaults: + +```TypeScript +this.courseService.putState( + state, + { + stateId: 'sample-state', + activityId: 'http://example.com/activities/sample-activity', + registration: '123456789-1234-1234-1234-123456789012', + agent: { + mbox: 'mailto:test@example.com', + }, + }, + { + contentType: 'application/json', + etag: '"123456789012345678901234567890123456789012"', + match: true + } +).subscribe( ... ); +``` + +## Getting a State + +The same way as above you can use the `getState` method. Without any arguments it will try to get the state by the default parameters, but you can override any of them: + +```TypeScript +this.xapiCourse.getState( + { + stateId: 'sample-state', + activityId: 'http://example.com/activities/sample-activity', + registration: '123456789-1234-1234-1234-123456789012', + agent: { + mbox: 'mailto:test@example.com', + }, + } +).subscribe( ... ); +``` + +## Sending a Statement + +You can send a default statement using the `postStatement` method: + +```TypeScript +this.xapiCourse.postStatement(); +``` + +The deafault verb is `experienced`, the actor, object and registration properties are picked up from the launch parameters. +You can provide a `Partial` argument to the `postStatement` method where you can override any of these parameters: + +```TypeScript +this.courseService.postStatement( + { + verb: experienced, + object: { + id: 'http://example.com/activities/sample-activity', + definition: { + name: { + 'en-US': 'Sample Activity', + }, + }, + }, + actor: { + mbox: 'mailto:other@example.com', + }, + context: { + registration: '00000000-0000-0000-0000-000000000000', + language: 'en-US', + extensions: { + 'http://example.com/profiles/meetings/context/extensions/meeting-id': + '123456789', + 'http://example.com/profiles/meetings/context/extensions/meeting-name': + 'Example Meeting', + } + }, + } +) +``` + +If the `XapiCourse` was initialized as a cmi5 course, then the properties from cmi5 `contextTemplate` are also added to the `Statement`. The mandatory parameters from the `contextTemplate` cannot be overridden. (these are the sessionId extension and the contextActivities arrays) +If you want full control over the `Statement`, you can configure it via a callback method. The incoming `defaultStatement` argument contains the prefilled Statement, but you can override any or all of the properties. (Do it only if you **really know** what are you doing) + +```TypeScript +this.courseService.postStatement( + (defaultStatement) => + ({ + ...defaultStatement, + verb: attempted, + context: { + contextActivities: { + grouping: [ + { + id: 'http://example.com/activities/world-domination', + definition: { + name: { + 'en-US': 'Nothing to see here', + }, + }, + }, + ], + parent: undefined + }, + extensions: undefined + }, + } as Statement) +); +``` + +## Convenience methods for sending Statements + +You can use the following method for sending the most common tincan or cmi5 statements. All of them can be used with an extra statement-template or a callback method argument, like the `postStatement` method above: + +```TypeScript +sendCompletedStatement(); +sendPassedStatement(); +sendFailedStatement(); +sendProgressedStatement(progressValue); +sendScoredStatement(scoreValue, scaledValue); +``` + +In the latter methods the progress and score values are merged into the statement after the callback method was used. + +eg. sending a score for a test which ha its own activity: + +```TypeScript +// For an IQ test, the scaled score is not applicable, so we send undefined +this.courseService.sendScoredStatement(152, undefined, { + object: { + id: 'http://example.com/activities/iq-test', + definition: { + name: { + 'en-US': 'IQ Test', + }, + }, + }, +}); +``` diff --git a/projects/ngx-xapi/client/src/lib/xapi-client.ts b/projects/ngx-xapi/client/src/lib/xapi-client.ts index 976ed4f..dd987ef 100644 --- a/projects/ngx-xapi/client/src/lib/xapi-client.ts +++ b/projects/ngx-xapi/client/src/lib/xapi-client.ts @@ -700,7 +700,7 @@ export class XapiClient { } if (params.since) { - httpParams = httpParams.set('registration', params.since); + httpParams = httpParams.set('since', params.since); } if (params.stateId) { diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index d9f9512..23d5f9d 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -224,12 +224,11 @@ export class XapiCourseService { return this.launchData; } - getState(stateId?: string) { - const stateParams = this.fillStateParams({ stateId }); + getState(params?: Partial) { return this.client.pipe( mergeMap((client) => client - ? client.getState(stateParams) + ? client.getState(this.fillStateParams(params)) : // return not found if the client is not initialized // (no launch parameters were provided) of(new HttpResponse({ body: null, status: 404 })) @@ -237,23 +236,33 @@ export class XapiCourseService { ); } - postState(state: T, options: StateOptions, params?: Partial) { - const stateParams = this.fillStateParams(params); + postState( + state: T, + params?: Partial, + options?: StateOptions + ) { return this.client.pipe( mergeMap((client) => client - ? client.postState(state, stateParams, options) + ? client.postState( + state, + this.fillStateParams(params), + options || { contentType: 'application/json' } + ) : of(new HttpResponse({ status: 204 })) ) ); } - putState(state: T, options: StateOptions, params?: Partial) { - const stateParams = this.fillStateParams(params); + putState(state: T, params?: Partial, options?: StateOptions) { return this.client.pipe( mergeMap((client) => client - ? client.putState(state, stateParams, options) + ? client.putState( + state, + this.fillStateParams(params), + options || { contentType: 'application/json' } + ) : of(new HttpResponse({ status: 204 })) ) ); @@ -468,6 +477,7 @@ export class XapiCourseService { */ sendScoredStatement( score: number, + scaled?: number, param?: Partial | ((defaultStatement: Statement) => Statement) ) { let statement = @@ -475,6 +485,12 @@ export class XapiCourseService { ? param(this.fillStatement(scored)) : this.fillStatement(scored, param); + const calculatedScaled = + statement?.result?.score?.min && statement?.result?.score?.max + ? (score - statement.result.score.min) / + (statement.result.score.max - statement.result.score.min) + : undefined; + statement = { ...statement, result: { @@ -482,7 +498,7 @@ export class XapiCourseService { score: { ...statement?.result?.score, raw: score, - scaled: score / 100, + scaled: scaled || calculatedScaled, }, }, }; From f4608ac0f06a5c695d05cc6cee11f77304ddf63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Tue, 25 Jul 2023 15:33:30 +0100 Subject: [PATCH 17/22] fsi --- .../ngx-xapi/course/src/lib/xapi-course.ts | 83 ++++++++++--------- .../samples/src/app/app-routing.module.ts | 1 - projects/samples/src/app/app.module.ts | 3 +- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index 23d5f9d..79aabea 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -51,7 +51,7 @@ export const XAPI_ACTIVITY = new InjectionToken< providedIn: 'root', }) export class XapiCourseService { - public client: ReplaySubject; + public client = new ReplaySubject(1); private course?: Activity; private contextTemplate?: Context; @@ -68,36 +68,8 @@ export class XapiCourseService { @Optional() platformLocation?: PlatformLocation ) { - this.client = new ReplaySubject(1); - - var launch$: Observable; - var activity$: Observable; - - if (activity instanceof Observable) { - activity$ = activity; - } else { - activity$ = of(activity); - } - - if (launchParameters) { - if (launchParameters instanceof Observable) { - launch$ = launchParameters; - } else { - launch$ = of(launchParameters); - } - } else if (platformLocation?.search) { - const params = new URLSearchParams(platformLocation.search); - launch$ = of({ - endpoint: params.get('endpoint'), - auth: params.get('auth'), - actor: params.get('actor') && JSON.parse(params.get('actor')!), - activityId: params.get('activityId'), - registration: params.get('registration'), - fetch: params.get('fetch'), - } as Launch); - } else { - launch$ = of({} as Launch); - } + const activity$ = this.calculateActivity(activity); + const launch$ = this.calculateLaunch(launchParameters, platformLocation); forkJoin([activity$, launch$]) .pipe( @@ -117,7 +89,7 @@ export class XapiCourseService { if (launch.fetch) { // fetch authorization from the LMS - return fetchCmi5Authorization(httpClient, launch.fetch!).pipe( + return fetchCmi5Authorization(httpClient, launch.fetch).pipe( // create a client from the authorization token map( (authorization: string) => @@ -141,21 +113,21 @@ export class XapiCourseService { this.sendCmi5Initialization(client).pipe( // initialize client only after the cmi5 initialized statement is sent tap(() => { - this.client!.next(client); - this.client!.complete(); + this.client.next(client); + this.client.complete(); }) ) ) ); } else if (launch.auth) { // initialize only if all required parameters are provided - this.client!.next( + this.client.next( new XapiClient(this.httpClient, { endpoint: launch.endpoint, authorization: launch.auth, }) ); - this.client!.complete(); + this.client.complete(); } return of(undefined); } else if ( @@ -192,11 +164,42 @@ export class XapiCourseService { .subscribe(); } + calculateLaunch( + launchParameters: Launch | Observable | null | undefined, + platformLocation: PlatformLocation | undefined + ): Observable { + if (launchParameters) { + if (launchParameters instanceof Observable) { + return launchParameters; + } else { + return of(launchParameters); + } + } else if (platformLocation?.search) { + const params = new URLSearchParams(platformLocation.search); + return of({ + endpoint: params.get('endpoint'), + auth: params.get('auth'), + actor: params.get('actor') && JSON.parse(params.get('actor')!), + activityId: params.get('activityId'), + registration: params.get('registration'), + fetch: params.get('fetch'), + } as Launch); + } else { + return of({} as Launch); + } + } + + calculateActivity( + activity: Activity | Observable + ): Observable { + return activity instanceof Observable ? activity : of(activity); + } + private getCmi5launchData(client: XapiClient): Observable { return client .getState({ activityId: this.course!.id, - agent: this.launch!.actor!, + agent: this.launch!.actor, registration: this.launch!.registration, stateId: 'LMS.LaunchData', }) @@ -456,7 +459,7 @@ export class XapiCourseService { ...statement?.result, extensions: { ...statement?.result?.extensions, - 'http://id.tincanapi.com/extension/progress': progress, + 'https://w3id.org/xapi/cmi5/result/extensions/progress': progress, }, }, }; @@ -528,10 +531,10 @@ export function fetchCmi5Authorization( return httpClient.post(fetch, null).pipe( map((response) => { if (response['auth-token']) { - return response['auth-token'] as string; + return response['auth-token']; } if (response['error-text']) { - throw new Error(response['error-text'] as string); + throw new Error(response['error-text']); } if (response['error-code']) { throw new Error( diff --git a/projects/samples/src/app/app-routing.module.ts b/projects/samples/src/app/app-routing.module.ts index 89b6297..5dfc1a4 100644 --- a/projects/samples/src/app/app-routing.module.ts +++ b/projects/samples/src/app/app-routing.module.ts @@ -1,7 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { XapiCourseComponent } from './xapi-course/xapi-course.component'; -import { AppComponent } from './app.component'; import { SamplesComponent } from './samples.component'; const routes: Routes = [ diff --git a/projects/samples/src/app/app.module.ts b/projects/samples/src/app/app.module.ts index 853ba75..81e0cdd 100644 --- a/projects/samples/src/app/app.module.ts +++ b/projects/samples/src/app/app.module.ts @@ -28,8 +28,9 @@ import { Activity } from '@berry-cloud/ngx-xapi/model'; { provide: XAPI_CONFIG, useValue: { + // You can create a free account here for testing endpoint: 'https://lrs.adlnet.gov/xapi/', - authorization: 'Basic dGVzdDIzOnRlc3QyMzIz', + authorization: 'Basic ***************', } as XapiConfig, }, { From cf343fef877b3cebfd4f25b53840f9857d83ec19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Tue, 25 Jul 2023 15:59:25 +0100 Subject: [PATCH 18/22] fix more sonar issues --- projects/ngx-xapi/course/src/lib/xapi-course.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index 79aabea..e35ee1f 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -250,7 +250,7 @@ export class XapiCourseService { ? client.postState( state, this.fillStateParams(params), - options || { contentType: 'application/json' } + options ?? { contentType: 'application/json' } ) : of(new HttpResponse({ status: 204 })) ) @@ -264,7 +264,7 @@ export class XapiCourseService { ? client.putState( state, this.fillStateParams(params), - options || { contentType: 'application/json' } + options ?? { contentType: 'application/json' } ) : of(new HttpResponse({ status: 204 })) ) @@ -501,7 +501,7 @@ export class XapiCourseService { score: { ...statement?.result?.score, raw: score, - scaled: scaled || calculatedScaled, + scaled: scaled ?? calculatedScaled, }, }, }; From 56a23baefa1ed7ac0e3a1c31e4aeac355943aa84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Wed, 26 Jul 2023 10:22:42 +0100 Subject: [PATCH 19/22] imprve README.md --- projects/ngx-xapi/README.md | 66 ++++++++++--------- .../ngx-xapi/course/src/lib/xapi-course.ts | 26 ++++---- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/projects/ngx-xapi/README.md b/projects/ngx-xapi/README.md index f880b32..edb8241 100644 --- a/projects/ngx-xapi/README.md +++ b/projects/ngx-xapi/README.md @@ -25,9 +25,9 @@ The package contains the following entry-points: ``` `@berry-cloud/ngx-xapi/model` contains the core types for xAPI. (Statement, Actor, Verb, etc.) -`@berry-cloud/ngx-xapi/client` contains utility methods for communicating with an LRS. +`@berry-cloud/ngx-xapi/client` contains utility functions for communicating with an LRS. `@berry-cloud/ngx-xapi/profiles/cmi5` contains types and extensions for the cmi5 profile. -`@berry-cloud/ngx-xapi/course` contains utility methods for a tincan or cmi5 course player. +`@berry-cloud/ngx-xapi/course` contains utility functions for a tincan or cmi5 course player. ## Samples @@ -190,7 +190,7 @@ Example: ### Handling Responses -Most of the `XapiClient` utility methods return a `Observable>` object. Although in most cases `Observable` would be enough, some important properties of the response can be gathered only from the response headers: +Most of the `XapiClient` utility functions return a `Observable>` object. Although in most cases `Observable` would be enough, some important properties of the response can be gathered only from the response headers: - ETag - X-Experience-API-Consistent-Through @@ -199,11 +199,12 @@ You can access the response object itself via `response.body`; ## Course Utilities -The course utilities can be accessed via the injectable `XapiCourse` service. -It contains useful utility methods for a tincan or cmi5 compatible course player. +The course utilities can be accessed via the injectable `XapiCourseService`. +It contains useful utility functions for a tincan or cmi5 compatible course player. +You can turn an angular application into a tincan and/or cmi5 compatible course within minutes. -If you plan to use this service, you must provide an `Activity` object to be injected into the `XapiCourse`. -This is the default activity object of the tincan/cmi5 course. +If you plan to use this service, you must provide an `Activity` object to be injected into the `XapiCourseService`. +This will be the activity object of the tincan/cmi5 course. For example: @@ -228,6 +229,7 @@ import { AppComponent } from './app.component'; useValue: { id: 'https://berrycloud.co.uk/xapi/sample', definition: { + type: "http://adlnet.gov/expapi/activities/course", name: { 'en-US': 'BerryCloud Sample Course', }, @@ -239,7 +241,7 @@ import { AppComponent } from './app.component'; export class AppModule {} ``` -The `XapiCourse` service also automatically picks up the launch parameters from the URL when the it is initialized. If the launch parameters come from a different source, or you want to hide the URL parameters before the service is initialized, you can provide this parameters via injection too: +The `XapiCourseService` also automatically picks up the launch parameters from the URL when it is initialized. If the launch parameters come from a different source, or you want to hide the URL parameters before the service is initialized, you can provide this parameters via injection too: ```TypeScript import { NgModule } from '@angular/core'; @@ -286,31 +288,31 @@ import { AppComponent } from './app.component'; export class AppModule {} ``` -`XAPI_LAUNCH` can be a `Launch` or `Observable`, so if can provide it via a factory method which loads it asynchronously from a file or the URL. +`XAPI_LAUNCH` can be a `Launch` or `Observable`, so you can also provide it via a factory function which loads it asynchronously from a file or the URL. -The `XapiCourse` service can handle both the tincan `auth` parameter or the `cmi5` launch parameter. If the latter is provided it automatically fetches the auth token from the provided endpoint. In this case it also automatically loads the cmi5 launch-data, and will use it to decorate each further state or statement requests. It also automatically sends the mandatory cmi5 `initialized` statement during initialization. +The `XapiCourseService` can handle both the tincan `auth` parameter or the `cmi5` launch parameter. If the latter is provided it automatically fetches the auth token from the provided endpoint. In this case it also automatically loads the cmi5 launch-data, and will use it to decorate each further statements to be sent to the LRS. It also automatically sends the mandatory cmi5 `initialized` statement during initialization. -If the launch parameters are not provided and neither cannot be picked from the URL bar the `XapiCourse` service still initializes itself, but throw an `Error`. -This error must be handled by the application. If it's ignored then the course will still run but it will silently ignore any http requests to the LRS. It can be useful for testing or when a course is launched locally and no need to store progress data. +If the launch parameters are not provided and neither cannot be picked from the URL bar the `XapiCourseService` service still initializes itself, but also throws an `Error`. +This error is logged into the console, but does not cease the application. It must be handled manually if needed. +If it's ignored then the course will still run, but it will silently ignore any http requests to the LRS. (Get functions will return 404, put/post functions will return 200 or 204.) It can be useful for testing or when a course is launched locally and no need to store progress data. -If neither of the above initialization methods are suitable, you can create the `XapiCourse` manually: +If neither of the above initialization methods are suitable, you can create the `XapiCourseService` manually: ``` - const course = new XapiCourse(activity, launch); + const course = new XapiCourseService(activity, launch); ``` -The `XapiCourse` uses the `XapiClient` internally, but extends it functionality with some useful convenient methods. If these methods don't fit for usecase you can still get an `Observable` via the `getXapiClient()` method. -It is `undefined` if the launch parameters were not provided or were deficient. See the `XapiClient` examples above. +The `XapiCourseService` uses the `XapiClient` internally, but extends its functionality with some useful convenience functions. +If these functions don't fit for your usecase, you can still get an `Observable` via the `getXapiClient()` function. It returns `undefined` if the launch parameters were not provided or were deficient. See the `XapiClient` examples above. ## Sending a State -After the `XapiCourse` was successfully initialized, you can send a state with the `putState` or `postState` method. It has only one mandatory argument, the state to be sent. All parameters needed for the LRS request are filled or defaulted in by the `XapiCourse`. +After the `XapiCourseService` was successfully initialized, you can send a state with the `putState` or `postState` functions. They have only one mandatory argument, the state to be sent. All parameters needed for the LRS request are filled or defaulted in by the `XapiCourseService`. default stateId: `progress` - default content-type: `application/json` -If you want to use different `stateId` or send a state with any other properties, you can override any of the defaults: +If you want to use different `stateId` or send a state with any other properties, you can override any of the defaults by providing the `StateParams` and `StateOptions` arguments: ```TypeScript this.courseService.putState( @@ -333,10 +335,10 @@ this.courseService.putState( ## Getting a State -The same way as above you can use the `getState` method. Without any arguments it will try to get the state by the default parameters, but you can override any of them: +You can use the `getState` function the same way as the above functions. Without any arguments it will try to get the state by the default parameters, but you can override any of them: ```TypeScript -this.xapiCourse.getState( +this.xapiCourseService.getState( { stateId: 'sample-state', activityId: 'http://example.com/activities/sample-activity', @@ -350,19 +352,19 @@ this.xapiCourse.getState( ## Sending a Statement -You can send a default statement using the `postStatement` method: +You can send a default statement using the `postStatement` function: ```TypeScript -this.xapiCourse.postStatement(); +this.xapiCourseService.postStatement(); ``` -The deafault verb is `experienced`, the actor, object and registration properties are picked up from the launch parameters. -You can provide a `Partial` argument to the `postStatement` method where you can override any of these parameters: +The default verb is `experienced`, the actor, object and registration properties are picked up from the launch parameters. +You can provide a `Partial` argument to the `postStatement` function where you can override any of these parameters: ```TypeScript this.courseService.postStatement( { - verb: experienced, + verb: attempted, object: { id: 'http://example.com/activities/sample-activity', definition: { @@ -388,8 +390,8 @@ this.courseService.postStatement( ) ``` -If the `XapiCourse` was initialized as a cmi5 course, then the properties from cmi5 `contextTemplate` are also added to the `Statement`. The mandatory parameters from the `contextTemplate` cannot be overridden. (these are the sessionId extension and the contextActivities arrays) -If you want full control over the `Statement`, you can configure it via a callback method. The incoming `defaultStatement` argument contains the prefilled Statement, but you can override any or all of the properties. (Do it only if you **really know** what are you doing) +If the `XapiCourseService` was initialized as a cmi5 course, then the properties from cmi5 `contextTemplate` are also added to the `Statement`. The mandatory parameters from the `contextTemplate` cannot be overridden. (these are the sessionId extension and the contextActivities arrays) +If you want full control over the `Statement`, you can configure it via a callback function. The incoming `defaultStatement` argument contains the prefilled Statement, but you can override any or all of the properties. (Do it only if you **really know** what are you doing) ```TypeScript this.courseService.postStatement( @@ -417,9 +419,9 @@ this.courseService.postStatement( ); ``` -## Convenience methods for sending Statements +## Convenience functions for sending Statements -You can use the following method for sending the most common tincan or cmi5 statements. All of them can be used with an extra statement-template or a callback method argument, like the `postStatement` method above: +You can use the following functions for sending the most common tincan or cmi5 statements. All of them can be used with an extra statement-template or a callback function argument, like the `postStatement` function above: ```TypeScript sendCompletedStatement(); @@ -429,9 +431,9 @@ sendProgressedStatement(progressValue); sendScoredStatement(scoreValue, scaledValue); ``` -In the latter methods the progress and score values are merged into the statement after the callback method was used. +In the latter functions the progress and score values are merged into the statement after the callback function was used. -eg. sending a score for a test which ha its own activity: +eg. sending a score for a test which has its own activity: ```TypeScript // For an IQ test, the scaled score is not applicable, so we send undefined diff --git a/projects/ngx-xapi/course/src/lib/xapi-course.ts b/projects/ngx-xapi/course/src/lib/xapi-course.ts index e35ee1f..8995c36 100644 --- a/projects/ngx-xapi/course/src/lib/xapi-course.ts +++ b/projects/ngx-xapi/course/src/lib/xapi-course.ts @@ -242,30 +242,32 @@ export class XapiCourseService { postState( state: T, params?: Partial, - options?: StateOptions + options?: Partial ) { return this.client.pipe( mergeMap((client) => client - ? client.postState( - state, - this.fillStateParams(params), - options ?? { contentType: 'application/json' } - ) + ? client.postState(state, this.fillStateParams(params), { + ...options, + contentType: options?.contentType ?? 'application/json', + }) : of(new HttpResponse({ status: 204 })) ) ); } - putState(state: T, params?: Partial, options?: StateOptions) { + putState( + state: T, + params?: Partial, + options?: Partial + ) { return this.client.pipe( mergeMap((client) => client - ? client.putState( - state, - this.fillStateParams(params), - options ?? { contentType: 'application/json' } - ) + ? client.putState(state, this.fillStateParams(params), { + ...options, + contentType: options?.contentType ?? 'application/json', + }) : of(new HttpResponse({ status: 204 })) ) ); From cc3562204523a6de6074124e3d110b80c6fb68ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Wed, 26 Jul 2023 13:01:54 +0100 Subject: [PATCH 20/22] sss --- projects/ngx-xapi/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/ngx-xapi/README.md b/projects/ngx-xapi/README.md index edb8241..5135c1d 100644 --- a/projects/ngx-xapi/README.md +++ b/projects/ngx-xapi/README.md @@ -299,7 +299,7 @@ If it's ignored then the course will still run, but it will silently ignore any If neither of the above initialization methods are suitable, you can create the `XapiCourseService` manually: ``` - const course = new XapiCourseService(activity, launch); + const courseService = new XapiCourseService(activity, launch); ``` The `XapiCourseService` uses the `XapiClient` internally, but extends its functionality with some useful convenience functions. From 18291fe67ca5bb1ac128b61bd81f4d5f2bc13fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Wed, 9 Aug 2023 15:07:44 +0100 Subject: [PATCH 21/22] sss --- package-lock.json | 147 ++++++++++++++++++++++++++-------------------- 1 file changed, 82 insertions(+), 65 deletions(-) diff --git a/package-lock.json b/package-lock.json index 47d3235..ebd1e05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,12 +48,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1601.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1601.6.tgz", - "integrity": "sha512-dY+/FNUNrOj+m4iG5/v8N0PfbDmjkjjoy/YkquRHS1yo7fGGDFNqji2552mbtjN6/LwyWDhOO7fxdqppadjnvA==", + "version": "0.1601.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1601.4.tgz", + "integrity": "sha512-OOSbNlDy+Q3jY0oFHaq8kkna9HYI1zaS8IHeCIDP6T/ZIAVad4+HqXAL4SKQrKJikkoBQv1Z/eaDBL5XPFK9Bw==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.1.6", + "@angular-devkit/core": "16.1.4", "rxjs": "7.8.1" }, "engines": { @@ -63,15 +63,15 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.1.6.tgz", - "integrity": "sha512-IEC1tApX8/Qa/RIVmbj0nYbOQ5WGcrkGNJ7D42q4DkIo74XKPzxDRruJE1RCjdZsj8lf4CCCZgSOPBsEI8Zbdw==", + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.1.4.tgz", + "integrity": "sha512-LiHM7R20fTHg/eM+Iabotj08edP5wVBQahRfVNLxERo8X6VJgSjVChnsh3AQJkRywlGuFe20AOQYpyLyN367Ug==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1601.6", - "@angular-devkit/build-webpack": "0.1601.6", - "@angular-devkit/core": "16.1.6", + "@angular-devkit/architect": "0.1601.4", + "@angular-devkit/build-webpack": "0.1601.4", + "@angular-devkit/core": "16.1.4", "@babel/core": "7.22.5", "@babel/generator": "7.22.7", "@babel/helper-annotate-as-pure": "7.22.5", @@ -83,7 +83,7 @@ "@babel/runtime": "7.22.5", "@babel/template": "7.22.5", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "16.1.6", + "@ngtools/webpack": "16.1.4", "@vitejs/plugin-basic-ssl": "1.0.1", "ansi-colors": "4.1.3", "autoprefixer": "10.4.14", @@ -191,12 +191,12 @@ "dev": true }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1601.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1601.6.tgz", - "integrity": "sha512-Uz/GjnhgAqSDPxrO4HP/tHNGPPZU3tEShtAVKyAypBl20bh2Aw1L5D+lCZi/Uq3Sh2JTPD9/M0ei2u9CMLhLDw==", + "version": "0.1601.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1601.4.tgz", + "integrity": "sha512-GC1y//ScAYbYQ68Wri2QgTEekC4hRxBC+xEkYL9OFiAMQ4mcN+eYvbkQBX8enJwDMXpkYfLR6VV8cChjAVYIgg==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1601.6", + "@angular-devkit/architect": "0.1601.4", "rxjs": "7.8.1" }, "engines": { @@ -210,9 +210,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.6.tgz", - "integrity": "sha512-3OjtrPWvsqVkMBwqPeE65ccCIw56FooNpVVAJ0XwhVQv5mA81pmbCzU7JsR6U449ZT7O4cQblzZMQvWvx74HCg==", + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.4.tgz", + "integrity": "sha512-WCAzNi9LxpFIi2WVPaJQd2kHPqCnCexWzUZN05ltJuBGCQL1O+LgRHGwnQ4WZoqmrF5tcWt2a3GFtJ3DgMc1hw==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -236,12 +236,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.6.tgz", - "integrity": "sha512-KA8P78gaS76HMHGBOM8JHJXWLOxCIShYVB2Un/Cu6z3jVODvXq+ILZUc1Y0RsAce/vsl2wf8qpoh5Lku9KJHUQ==", + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.4.tgz", + "integrity": "sha512-yjRgwHAfFaeuimgbQtjwSUyXzEHpMSdTRb2zg+TOp6skoGvHOG8xXFJ7DjBkSMeAQdFF0fkxhPS9YmlxqNc+7A==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.1.6", + "@angular-devkit/core": "16.1.4", "jsonc-parser": "3.2.0", "magic-string": "0.30.0", "ora": "5.4.1", @@ -254,15 +254,15 @@ } }, "node_modules/@angular/cli": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.1.6.tgz", - "integrity": "sha512-yXVgUKMXxlAHkhc6xk3ljR7TXpMLBykyu8do+ooSP08VKEQnWjTdVgrcOHd0n5w9YHXUQgBSmjDKxtQaBmvyZQ==", + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.1.4.tgz", + "integrity": "sha512-coSOLVLpOCOD5q9K9EAFFMrTES+HtdJiLy/iI9kdKNCKWUJpm8/svZ3JZOej3vPxYEp0AokXNOwORQnX21/qZQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1601.6", - "@angular-devkit/core": "16.1.6", - "@angular-devkit/schematics": "16.1.6", - "@schematics/angular": "16.1.6", + "@angular-devkit/architect": "0.1601.4", + "@angular-devkit/core": "16.1.4", + "@angular-devkit/schematics": "16.1.4", + "@schematics/angular": "16.1.4", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", @@ -288,9 +288,9 @@ } }, "node_modules/@angular/common": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.1.7.tgz", - "integrity": "sha512-7WwYwtJjuJtUkutB+aMCvtV5zxa43T4x+kqT+kS4KnUmLv5KdrGPxcS+/7YUuKEELWp1SG032UTwGPX0DXxH4g==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.1.5.tgz", + "integrity": "sha512-XQVIpICniWXXMoXsr6X7Q3pVcYBeQ0FZF06BNNolkkkVuReYpqr3TwWrZfuB9TUmxdF6R5WZ+M3NAdXodDDUNA==", "dependencies": { "tslib": "^2.3.0" }, @@ -298,14 +298,14 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.1.7", + "@angular/core": "16.1.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.1.7.tgz", - "integrity": "sha512-93nbMFPSpKNfUyuRvEQxPdYLU6g25oZ4Gp7ewzNLyDHIbTQv6FwsthHfgPigPJJUUyKak6Gr3koFsgk7Dl3LAA==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.1.5.tgz", + "integrity": "sha512-QNyisdr9lEN43v/e/fjS0H1vrJBMY8lIGpxVY1OOERFjA1clfMhaz5fiPE3vWFV5TOm3/ym9z2xuRXM6UoyWoA==", "peer": true, "dependencies": { "tslib": "^2.3.0" @@ -314,7 +314,7 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.1.7" + "@angular/core": "16.1.5" }, "peerDependenciesMeta": { "@angular/core": { @@ -323,9 +323,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.1.7.tgz", - "integrity": "sha512-6iuogfVrbCh6o4hWbNCClsLQdLtlXiaNc72LGz5LMXI0TOwKVlRXhbzhiQeLS0/nsYIdHFbgyr1aepI2wQA3mQ==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.1.5.tgz", + "integrity": "sha512-j20hmPyM+rLJDU1y0ta9Uf7+o2oGjvGWGpyANbpuTlAfA1+VN5G3xD53FnNcmO6LZuAw0wDw6NDAyy+G55o8xQ==", "dev": true, "dependencies": { "@babel/core": "7.22.5", @@ -346,14 +346,14 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/compiler": "16.1.7", + "@angular/compiler": "16.1.5", "typescript": ">=4.9.3 <5.2" } }, "node_modules/@angular/core": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.1.7.tgz", - "integrity": "sha512-Wl5BR9X1xnV7Z9v/MNVANhymuTKAuRv4etr4rRgaC5NbbJSuFM4y+mg4yVI4wmrYJo0gKRcV9+2mHaePr41fTg==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.1.5.tgz", + "integrity": "sha512-xmk+WeL3qtFb3BM2hsEq/kGHJinqaTNVJkK/m4TiGArY+hjJwfCOeuTss7nOkKXvhRkZxU9VP0tej1w3QV5Yzw==", "dependencies": { "tslib": "^2.3.0" }, @@ -366,9 +366,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.1.7.tgz", - "integrity": "sha512-AjdUUv5+v50cclHPsKVVdNRdCQZJMGNKmvxyLgeGj2hs61lGoJxBYcYqPre2PpM0SvezNJBreUvjwqM3ttOjng==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.1.5.tgz", + "integrity": "sha512-TLM29KPr0A0pQ0YEmSy0JUOkfBXfwfBFzXQSt9SOiUs0wgDVVLMdGOpR/tbvBx2QfrSU3qgOX8P1FXIPJch6TQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -376,9 +376,9 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/animations": "16.1.7", - "@angular/common": "16.1.7", - "@angular/core": "16.1.7" + "@angular/animations": "16.1.5", + "@angular/common": "16.1.5", + "@angular/core": "16.1.5" }, "peerDependenciesMeta": { "@angular/animations": { @@ -387,9 +387,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.1.7.tgz", - "integrity": "sha512-xoT4wDl7Kurg2N5gcLNmkvqYx14xnYwa2Zm1ZIOM7kYMRXiAg1+XBzaxFXog0fCCs/lqUKUwaNn32YpLKwMNaw==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.1.5.tgz", + "integrity": "sha512-ugdIXeN5IVj9o15ywH32hxNI0ZLyakpBGqMTHZSeEhU/uN6ajAJX7z6okdMbJ7dlTyBO8eFV1KDX3aAz+sK9bg==", "dependencies": { "tslib": "^2.3.0" }, @@ -397,10 +397,27 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.1.7", - "@angular/compiler": "16.1.7", - "@angular/core": "16.1.7", - "@angular/platform-browser": "16.1.7" + "@angular/common": "16.1.5", + "@angular/compiler": "16.1.5", + "@angular/core": "16.1.5", + "@angular/platform-browser": "16.1.5" + } + }, + "node_modules/@angular/router": { + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.1.5.tgz", + "integrity": "sha512-L1gyWA16U+XgcxWmemWjy08/OPCjch9sBEiHaikuW8i9Ys0nx9ic3wh8Fyu6cVKQE9aQZ7xLYT5CdPPwYxclTw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "16.1.5", + "@angular/core": "16.1.5", + "@angular/platform-browser": "16.1.5", + "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@assemblyscript/loader": { @@ -2433,9 +2450,9 @@ "dev": true }, "node_modules/@ngtools/webpack": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.1.6.tgz", - "integrity": "sha512-rDE1bV3+Ys/VyeD6l7JKtbs3+bTQAfWhi7meEuq5mkaJHOERu6Z40ce866faAIX2I1AVpsSv8rLlb7kB7t7kzw==", + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.1.4.tgz", + "integrity": "sha512-+8bfavDH8eWxjlJFYr6bkjcRHhy95j+f8oNn7/sGLNu4L96nuE2AZ011XIu2dJahCnNiBvwc1EpkKa92t9rkaA==", "dev": true, "engines": { "node": "^16.14.0 || >=18.10.0", @@ -2708,13 +2725,13 @@ } }, "node_modules/@schematics/angular": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.1.6.tgz", - "integrity": "sha512-BxghkeLfnMgV0D4DZDcbfPpox/Orw1ismSVGoQMIV/Daj2pqfSK+n97NAu0r0EsQyR5agPxOX9khVft+otODhg==", + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.1.4.tgz", + "integrity": "sha512-XfoeL+aBVIR/DzgVKGVhHW/TGQnqWvngyJVuCwXEVWzNfjxHYFkchXa78OItpAvTEr6/Y0Me9FQVAGVA4mMUyg==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.1.6", - "@angular-devkit/schematics": "16.1.6", + "@angular-devkit/core": "16.1.4", + "@angular-devkit/schematics": "16.1.4", "jsonc-parser": "3.2.0" }, "engines": { From 16bf3c539d5a80a4a8901df0564ac76ff2eeba48 Mon Sep 17 00:00:00 2001 From: Thomas Turrell-Croft Date: Wed, 9 Aug 2023 15:30:20 +0100 Subject: [PATCH 22/22] tip --- projects/ngx-xapi/package-lock.json | 85 +++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 projects/ngx-xapi/package-lock.json diff --git a/projects/ngx-xapi/package-lock.json b/projects/ngx-xapi/package-lock.json new file mode 100644 index 0000000..c44a0f4 --- /dev/null +++ b/projects/ngx-xapi/package-lock.json @@ -0,0 +1,85 @@ +{ + "name": "@berry-cloud/ngx-xapi", + "version": "0.3.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@berry-cloud/ngx-xapi", + "version": "0.3.3", + "license": " Apache-2.0", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^16.1.0", + "@angular/core": "^16.1.0", + "uuid": "^9.0.0" + } + }, + "node_modules/@angular/common": { + "version": "16.1.8", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.1.8.tgz", + "integrity": "sha512-Zm+Ysxdf74VwG3mbAqs2v1QFUR+h9RyJBXF5VFABEpgFw7NUOBKrayjJmKjgZ0TBAmL2+nXehJgcPph3zNp3sg==", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "16.1.8", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/core": { + "version": "16.1.8", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.1.8.tgz", + "integrity": "sha512-XtOpY9HA85hPGrPwe1rgE8NJ3bFWbuJFx4SUlzB66k9B5jo8bD2Dxl/0id55RFS5gmvCe/Qhh0zoGyMpkWjMHA==", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.13.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/zone.js": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.13.1.tgz", + "integrity": "sha512-+bIeDAFEBYuXRuU3qGQvzdPap+N1zjM4KkBAiiQuVVCrHrhjDuY6VkUhNa5+U27+9w0q3fbKiMCbpJ0XzMmSWA==", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + } + } + } +}