diff --git a/.gitignore b/.gitignore index e022cb1..0ba8101 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # Compiled output /dist +/dist-wc /tmp /out-tsc /bazel-out diff --git a/angular.json b/angular.json index bd4d8e7..2c143ef 100644 --- a/angular.json +++ b/angular.json @@ -27,7 +27,63 @@ "test": { "builder": "@angular-builders/jest:run", "options": { - "tsConfig": "./tsconfig.spec.json" + "configPath": "jest.config.js", + "tsConfig": "tsconfig.spec.json" + } + } + } + }, + "wc": { + "projectType": "application", + "root": "projects/wc", + "sourceRoot": "projects/wc/src", + "prefix": "wc", + "architect": { + "build": { + "builder": "ngx-build-plus:browser", + "options": { + "outputPath": "dist-wc/assets", + "singleBundle": true, + "outputHashing": "none", + "index": "", + "main": "projects/wc/src/main.ts", + "assets": ["projects/wc/src/assets"], + "scripts": [] + }, + "configurations": { + "production": { + "optimization": true, + "buildOptimizer": true, + "sourceMap": false, + "extractLicenses": false, + "vendorChunk": false, + "namedChunks": false, + "tsConfig": "projects/wc/tsconfig.app.prod.json" + }, + "development": { + "aot": true, + "optimization": false, + "buildOptimizer": false, + "sourceMap": true, + "extractLicenses": false, + "namedChunks": true, + "vendorChunk": true, + "tsConfig": "projects/wc/tsconfig.app.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@angular-builders/jest:run", + "options": { + "configPath": "jest.config.js", + "tsConfig": "tsconfig.spec.json" + } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": ["projects/wc/**/*.ts", "projects/wc/**/*.html"] } } } diff --git a/jest.config.js b/jest.config.js index a85dcb3..abd995b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,9 +2,13 @@ module.exports = { preset: 'jest-preset-angular', testRunner: 'jest-jasmine2', collectCoverage: true, - modulePathIgnorePatterns: ['/dist/'], + modulePathIgnorePatterns: ['/dist/', '/dist-wc/', '/.yalc/'], coveragePathIgnorePatterns: ['/node_modules/', '/integration-tests/'], moduleNameMapper: { '^@luigi-project/client-support-angular$': '/projects/lib/_mocks_/luigi-client-support-angular.ts', + '^@platform-mesh/portal-ui-lib$': '/projects/lib/public-api.ts', + '^@platform-mesh/portal-ui-lib/services$': '/projects/lib/services/public-api.ts', + '^@platform-mesh/portal-ui-lib/utils$': '/projects/lib/utils/public-api.ts', + '^@platform-mesh/portal-ui-lib/(.*)': '/projects/lib/$1', }, }; diff --git a/package-lock.json b/package-lock.json index d8eda5a..3b76934 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,17 +19,21 @@ "@angular/compiler-cli": "^20.2.1", "@angular/localize": "^20.2.1", "@briebug/jest-schematic": "^6.0.0", - "@openmfp/portal-ui-lib": "0.174.2", + "@openmfp/portal-ui-lib": "^0.176.0", "@types/jest": "^30.0.0", "@types/jmespath": "0.15.2", + "@types/jsonpath": "^0.2.4", "@ui5/webcomponents-ngx": "^0.5.0", + "cpx2": "^8.0.0", "jest": "^29.7.0", "jest-jasmine2": "29.7.0", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jmespath": "0.16.0", "mkdirp": "^3.0.1", + "move-cli": "^2.0.0", "ng-packagr": "^20.2.0", + "ngx-build-plus": "^20.0.0", "nodemon": "3.1.10", "rimraf": "6.0.1", "ts-jest": "29.3.2", @@ -299,13 +303,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2002.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2002.1.tgz", - "integrity": "sha512-8jotVFz+83avTdeRoLe7wn/F+nnbjywuVHqZ/shDGRHssOtR8fkSCjSsKwPZejU6wsgTxAKFylWRIxydZE8Hzw==", + "version": "0.2002.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2002.2.tgz", + "integrity": "sha512-amppp/UqKyj+B8hYFU16j4t6SVN+SS0AEnHivDjKy41NNJgXv+5Sm2Q2jaMHviCT3rclyT0wqwNAi0RDjyLx5Q==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.2.1", + "@angular-devkit/core": "20.2.2", "rxjs": "7.8.2" }, "engines": { @@ -315,17 +319,17 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-20.2.1.tgz", - "integrity": "sha512-sAa+fk1NNEoYJgrzPYx3fPi2BDyYouCGDFd+L72LoxEvYpBw84tfdVm03JSUDbSr7/vc8xZ9msQGmiLpUpK/hg==", + "version": "20.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-20.2.2.tgz", + "integrity": "sha512-atmy2RNViTqzNYGLR94NxSEISGtynseKFF+FPEnYTBc3W08UcJmaC5AAdJeuDJqqW495tFM7dSxUMGlSfWsN2w==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2002.1", - "@angular-devkit/build-webpack": "0.2002.1", - "@angular-devkit/core": "20.2.1", - "@angular/build": "20.2.1", + "@angular-devkit/architect": "0.2002.2", + "@angular-devkit/build-webpack": "0.2002.2", + "@angular-devkit/core": "20.2.2", + "@angular/build": "20.2.2", "@babel/core": "7.28.3", "@babel/generator": "7.28.3", "@babel/helper-annotate-as-pure": "7.27.3", @@ -336,7 +340,7 @@ "@babel/preset-env": "7.28.3", "@babel/runtime": "7.28.3", "@discoveryjs/json-ext": "0.6.3", - "@ngtools/webpack": "20.2.1", + "@ngtools/webpack": "20.2.2", "ansi-colors": "4.1.3", "autoprefixer": "10.4.21", "babel-loader": "10.0.0", @@ -391,7 +395,7 @@ "@angular/platform-browser": "^20.0.0", "@angular/platform-server": "^20.0.0", "@angular/service-worker": "^20.0.0", - "@angular/ssr": "^20.2.1", + "@angular/ssr": "^20.2.2", "@web/test-runner": "^0.20.0", "browser-sync": "^3.0.2", "jest": "^29.5.0", @@ -448,13 +452,13 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.2002.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.2002.1.tgz", - "integrity": "sha512-A8byX/gK6jA0/2JXcFBtZ3b5iTH2yzY3hiEAxP9Nt5HVQ/sIZOmmYNbLzOnrNRCH47mXBq4JtJ9082Xl5Lvsrg==", + "version": "0.2002.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.2002.2.tgz", + "integrity": "sha512-DbHq8AHmlRsr1jFmrJSlksPl/ViSVPqQdicz0dkdo0rSGkQqGO1Z0vFLf0/trlDP7GgHz46yucDtaFjPq1dZ9Q==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2002.1", + "@angular-devkit/architect": "0.2002.2", "rxjs": "7.8.2" }, "engines": { @@ -468,9 +472,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.2.1.tgz", - "integrity": "sha512-07xiRltPA1X+C0AQo/glI0in+bpwGW1cgOen2pp0MhXVlawW1M9cKZFb/35uvYUEWJUxLwBB3ZKJXBmpWWw0Rg==", + "version": "20.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.2.2.tgz", + "integrity": "sha512-SC+f5isSWJBpEgR+R7jP++2Z14WExNWLAdKpIickLWjuL8FlGkj+kaF3dWXhh0KcXo+r6kKb4pWUptSaqer5gA==", "dev": true, "license": "MIT", "dependencies": { @@ -496,13 +500,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.2.1.tgz", - "integrity": "sha512-hxQQhlOKLjj4+fJrvMFWnVA6vwewwtkEGneolY+aMb8dUAEE7sw1FLo02pPdIBIXLWIYIcGVRI0E5iCTcLq9zw==", + "version": "20.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.2.2.tgz", + "integrity": "sha512-rtL7slZjzdChQoiADKZv/Ra8D3C3tIw/WcVxd2stiLHdK/Oaf9ejx5m/X9o0QMEbNsy2Fy/RKodNqmz1CjzpCg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.2.1", + "@angular-devkit/core": "20.2.2", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "8.2.0", @@ -530,9 +534,9 @@ } }, "node_modules/@angular/animations": { - "version": "20.2.3", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.2.3.tgz", - "integrity": "sha512-cyON3oVfaotz8d8DHP3uheC/XDG2gJD8aiyuG/SEAZ2X1S/tAHdVetESbDZM830lLdi+kB/3GBrMbWCCpMWD7Q==", + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.2.4.tgz", + "integrity": "sha512-mXiTlXZgAF4uYonOt7l2w7uvLLTJEk6jqs3H291bYuoDRM8R166UjN7ygAeBmPiJ4TLMyKGkwMQy3b1Vvw4RQA==", "dev": true, "license": "MIT", "peer": true, @@ -543,19 +547,18 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "20.2.3", - "@angular/core": "20.2.3" + "@angular/core": "20.2.4" } }, "node_modules/@angular/build": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-20.2.1.tgz", - "integrity": "sha512-FLiNDUhqCkU7EyODwPl8EZMubWdQG62ynczeLcHGtHOA2/Wiv+CvCP58GbuznZSslEcyyyE7MsEy3ZvsjxZuIA==", + "version": "20.2.2", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-20.2.2.tgz", + "integrity": "sha512-rvlKMt3OmeenHOwejRpI4OLcyERQn6Hl4ODRWlYfNX70Ki1zu6eAD0pWULzcD+HSQd0a26Xzt3gcpEy2vOEAzg==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2002.1", + "@angular-devkit/architect": "0.2002.2", "@babel/core": "7.28.3", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", @@ -597,7 +600,7 @@ "@angular/platform-browser": "^20.0.0", "@angular/platform-server": "^20.0.0", "@angular/service-worker": "^20.0.0", - "@angular/ssr": "^20.2.1", + "@angular/ssr": "^20.2.2", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^20.0.0", @@ -647,9 +650,9 @@ } }, "node_modules/@angular/cdk": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.1.tgz", - "integrity": "sha512-yEPh5hr9LZW4ey/HxtaGdSBDIkNzziLo0Dr1RP8JcxhOQ2Bzv2PZ+g8jC6aPGD7NPV8FtDf0FhTEzQr+m+gBXQ==", + "version": "20.2.2", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.2.tgz", + "integrity": "sha512-jLvIMmFI8zoi6vAu1Aszua59GmhqBOtsVfkwLUGg5Hi86DI/inJr9BznNX2EKDtaulYMGZCmDgsltXQXeqP5Lg==", "dev": true, "license": "MIT", "peer": true, @@ -664,19 +667,19 @@ } }, "node_modules/@angular/cli": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.2.1.tgz", - "integrity": "sha512-uKuq4+7EcEer7ixe6cYAAe8/WOvDIbLd/F7ZCMCb5dCGkGRoQKgodo6sorwZUpGvyuXO+mCYarTXzrBrY2b/Cg==", + "version": "20.2.2", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.2.2.tgz", + "integrity": "sha512-0K8cmuHzRTpPzy/w0+S5o3s0JPV++9/s2JhK4aw/+OnQRpUbodoqjm1ur5k5DUBQfIHi7aM73ZIW3G43lv4F0g==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2002.1", - "@angular-devkit/core": "20.2.1", - "@angular-devkit/schematics": "20.2.1", + "@angular-devkit/architect": "0.2002.2", + "@angular-devkit/core": "20.2.2", + "@angular-devkit/schematics": "20.2.2", "@inquirer/prompts": "7.8.2", "@listr2/prompt-adapter-inquirer": "3.0.1", "@modelcontextprotocol/sdk": "1.17.3", - "@schematics/angular": "20.2.1", + "@schematics/angular": "20.2.2", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.35.0", "ini": "5.0.0", @@ -699,9 +702,9 @@ } }, "node_modules/@angular/common": { - "version": "20.2.3", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.2.3.tgz", - "integrity": "sha512-QLffWL8asy2oG7p3jvoNmx9s1V1WuJAm6JmQ1S8J3AN/BxumCJan49Nj8rctP8J4uwJDPQV48hqbXUdl1v7CDg==", + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.2.4.tgz", + "integrity": "sha512-mc6Sq1cYjaPJYThnvG6x0f/E27pWksqwaNJxT1RtwhAGc1i2jsc0su6b7e5NnXEgVbdPqu1MZHAEFdXZ5+/MwQ==", "dev": true, "license": "MIT", "peer": true, @@ -712,14 +715,14 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "20.2.3", + "@angular/core": "20.2.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "20.2.3", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.2.3.tgz", - "integrity": "sha512-vYGDluko8zAIWhQmKijhcGO0tzanwGONDRgbJ01mCqUsQV+XwmDgUUDZKrUY9uve0wxxM3Xvo4/BjEpGpeG75w==", + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.2.4.tgz", + "integrity": "sha512-LQzf+Azb/Ms+BavpCFIat+f1C0gUJpby2RW4yebF3JkBFKfJ7M8d49TQpF8rSnGxMRTf49mln7laz4nBYTLDGA==", "dev": true, "license": "MIT", "peer": true, @@ -731,9 +734,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "20.2.3", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.2.3.tgz", - "integrity": "sha512-adLyPXmKbH8VZJCyOraaha+RPTdAjEBRTqwZ5YkjkViTMMANFkuj1w3pDwQsG3LFknRJ99aym+9neGINeAaI7A==", + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.2.4.tgz", + "integrity": "sha512-II2hEpfbo73dL12D42DoIHYGiTYAiO9cpwh29BIo8VD054ei4cm0oK+jCyryDQH5T3+wyCWlj0OFjcZ/GmO7HQ==", "dev": true, "license": "MIT", "dependencies": { @@ -754,7 +757,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "20.2.3", + "@angular/compiler": "20.2.4", "typescript": ">=5.8 <6.0" }, "peerDependenciesMeta": { @@ -764,9 +767,9 @@ } }, "node_modules/@angular/core": { - "version": "20.2.3", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.2.3.tgz", - "integrity": "sha512-pFMfg11X8SNNZHcLa+wy4y+eAN3FApt+wPzaxkaXaJ64c+tyHcrPNLotoWgE0jmiw8Idn4gGjKAL/WC0uw5dQA==", + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.2.4.tgz", + "integrity": "sha512-8yvfvPDWX8M7o82GBl5P1nlvm1ywQ2XZi5HWj3llKpSJE2XjzhATgPrpKwiNVnpgjZWTOwM11fpoAaRKqQjxTA==", "dev": true, "license": "MIT", "peer": true, @@ -777,7 +780,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "20.2.3", + "@angular/compiler": "20.2.4", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0" }, @@ -791,9 +794,9 @@ } }, "node_modules/@angular/elements": { - "version": "20.2.3", - "resolved": "https://registry.npmjs.org/@angular/elements/-/elements-20.2.3.tgz", - "integrity": "sha512-G8Vr1msGczyWf8AT/xhrsLZBO+DJIHAZY89FWLljE38PS8n4y13/0jh7/Sc/lUkVx3AqTo9iZ+ws8vcgOK86hA==", + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/@angular/elements/-/elements-20.2.4.tgz", + "integrity": "sha512-5v906L5iiN5E80bmcYRnL23rFiSmJ7YzXjpsAp8/W5AAJnLgGOsH1vObWl/i4bPul0ETwF6vdF5bobQ/04jWWg==", "dev": true, "license": "MIT", "peer": true, @@ -804,14 +807,14 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "20.2.3", + "@angular/core": "20.2.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/forms": { - "version": "20.2.3", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.2.3.tgz", - "integrity": "sha512-efMn/Hnspg91SzRTm69WpyGq0dgbCtWqUOrR0iZXTR/oDlJw9F/y/nrST36tOBwRNT0QQ2iU5z43iJY1Rl1Bng==", + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.2.4.tgz", + "integrity": "sha512-wbgnW+GALVAmK6hgFegkwlHKw35onvh9Z5A236HCyUySEAOiaD/3CoDg5Hw4iHQAiSU6Fn2NwDiv+W0xki6WDw==", "dev": true, "license": "MIT", "peer": true, @@ -822,16 +825,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "20.2.3", - "@angular/core": "20.2.3", - "@angular/platform-browser": "20.2.3", + "@angular/common": "20.2.4", + "@angular/core": "20.2.4", + "@angular/platform-browser": "20.2.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/localize": { - "version": "20.2.3", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-20.2.3.tgz", - "integrity": "sha512-+r7VbxuaOwUuvC1xPfuNpJSbwv4+LOUouVZhBq5sp2qYrKkVw2QZaIbd6uPTE1NWbu7rGwSGVw4rTx4LvA3fYw==", + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-20.2.4.tgz", + "integrity": "sha512-8OimXwR/hzUHJdegLD4+Zhg1h3qaAVLwLLK3G6Ba4EU9W9HJCyqvxIXooXossLBp/toFKyjU/RxmH+dwy4ztCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -849,14 +852,14 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "20.2.3", - "@angular/compiler-cli": "20.2.3" + "@angular/compiler": "20.2.4", + "@angular/compiler-cli": "20.2.4" } }, "node_modules/@angular/platform-browser": { - "version": "20.2.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.2.3.tgz", - "integrity": "sha512-oNaRqcGUve+E/CwR9fJb8uern5rb7qNOis1bZRdPXq5rHKaWgDCxUPkoqxRi0EytorntuYsWYPUPW3ul4Ea9tw==", + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.2.4.tgz", + "integrity": "sha512-81vzW8xhnJU7AiYJKXLR2MuvawzhRDgwyNkPEep58wty5zNuIUCXdUERJSsXo7m/U2Dg1FUFfqLm4RC2UkqLzA==", "dev": true, "license": "MIT", "peer": true, @@ -867,9 +870,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/animations": "20.2.3", - "@angular/common": "20.2.3", - "@angular/core": "20.2.3" + "@angular/animations": "20.2.4", + "@angular/common": "20.2.4", + "@angular/core": "20.2.4" }, "peerDependenciesMeta": { "@angular/animations": { @@ -878,9 +881,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "20.2.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.2.3.tgz", - "integrity": "sha512-uxqLv2yNibd2vf3OObyH4arVfwu+o9FKgkUcnFUdowK31emiZe1nAY8uEe/92JIsMMAoIllI/GAVzWH8dp05mg==", + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.2.4.tgz", + "integrity": "sha512-ktunGTMWuWtnKUicOdXjF8Nc1RInf78YW7TqMV35rF32VXpHwRRKw2M7OKViPk18MlbDE2pc5HCX558BEUla0A==", "dev": true, "license": "MIT", "peer": true, @@ -891,16 +894,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "20.2.3", - "@angular/compiler": "20.2.3", - "@angular/core": "20.2.3", - "@angular/platform-browser": "20.2.3" + "@angular/common": "20.2.4", + "@angular/compiler": "20.2.4", + "@angular/core": "20.2.4", + "@angular/platform-browser": "20.2.4" } }, "node_modules/@angular/router": { - "version": "20.2.3", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.2.3.tgz", - "integrity": "sha512-r8yGJcxHPfeIHZOoyCxN2H4nMgBD/k4TVTFaq8MHf5ryy1iLzayIMPJTFaZe7xpwlJJuBYEjBrYfUN38fYKWgA==", + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.2.4.tgz", + "integrity": "sha512-KoduI1o+iBfCBGtXMvmy/qncDIwGxd2hNt2hDkkiYZTftmSg/XUJDxJqN84ckm2WLkdJpR9EirrwfHapJBIZOQ==", "dev": true, "license": "MIT", "peer": true, @@ -911,9 +914,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "20.2.3", - "@angular/core": "20.2.3", - "@angular/platform-browser": "20.2.3", + "@angular/common": "20.2.4", + "@angular/core": "20.2.4", + "@angular/platform-browser": "20.2.4", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -977,9 +980,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "dev": true, "license": "MIT", "engines": { @@ -1356,27 +1359,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", - "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -1823,9 +1826,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", - "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", + "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", "dev": true, "license": "MIT", "dependencies": { @@ -1873,9 +1876,9 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz", - "integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", "dev": true, "license": "MIT", "dependencies": { @@ -1884,7 +1887,7 @@ "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -2277,9 +2280,9 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", - "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", "dev": true, "license": "MIT", "dependencies": { @@ -2287,7 +2290,7 @@ "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -2414,9 +2417,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz", - "integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", "dev": true, "license": "MIT", "dependencies": { @@ -2777,18 +2780,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.3", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2", + "@babel/types": "^7.28.4", "debug": "^4.3.1" }, "engines": { @@ -2796,9 +2799,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3618,9 +3621,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "peer": true, @@ -3678,6 +3681,20 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-array/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, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/config-helpers": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", @@ -3746,6 +3763,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3754,6 +3782,20 @@ "license": "MIT", "peer": true }, + "node_modules/@eslint/eslintrc/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, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3769,9 +3811,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", + "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", "dev": true, "license": "MIT", "peer": true, @@ -3976,35 +4018,20 @@ } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -4981,9 +5008,9 @@ } }, "node_modules/@jest/expect-utils": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.1.tgz", - "integrity": "sha512-5YUHr27fpJ64dnvtu+tt11ewATynrHkGYD+uSFgRr8V2eFJis/vEXgToyLwccIwqBihVfz9jwio+Zr1ab1Zihw==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.2.tgz", + "integrity": "sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==", "dev": true, "license": "MIT", "dependencies": { @@ -5425,6 +5452,28 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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/@jest/reporters/node_modules/jest-message-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", @@ -5464,6 +5513,19 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/reporters/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@jest/reporters/node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -6474,9 +6536,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-20.2.1.tgz", - "integrity": "sha512-4DyxUF3ArURjrBXzoIdlMi2Md2Lw7qdieyI070Usf9OpiF5Ouk3hqlRwE1RHznfDBOA7sLVj3ube5xP5kcPV1w==", + "version": "20.2.2", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-20.2.2.tgz", + "integrity": "sha512-q2kNlKmC+slbdwiOhnY7M610ie41P5j0WFz+1k73L57tE5xUitgdjCF/f4YPGlj7vNfFyuoX98k9IyQtsbzh8w==", "dev": true, "license": "MIT", "engines": { @@ -6722,6 +6784,22 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/@npmcli/package-json/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/@npmcli/package-json/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -6745,6 +6823,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@npmcli/package-json/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@npmcli/promise-spawn": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.3.tgz", @@ -6839,9 +6934,9 @@ } }, "node_modules/@openmfp/portal-ui-lib": { - "version": "0.174.2", - "resolved": "https://npm.pkg.github.com/download/@openmfp/portal-ui-lib/0.174.2/00b5c6fc8d4c167cda3a2706485e9f89a21872fd", - "integrity": "sha512-xsgvakAkJ1d7QnBIWt0Tm2if55RS2i9OTxq+xp/mQy9GiNqTbXEeDpksqG0N5Yr1ENPq1kRCa+UHEPR8nDIKLg==", + "version": "0.176.0", + "resolved": "https://npm.pkg.github.com/download/@openmfp/portal-ui-lib/0.176.0/a4d04938ae5f83feb8562cec79a3901374a80b13", + "integrity": "sha512-HE9Nrc6g9NyjBtq7vVKm/e28d+E23T1cvRxO1m7iRPBjvRz2F6O2eYIXcCKuqWafVTrk807OtZwfal+jYjrxrA==", "dev": true, "dependencies": { "@luigi-project/plugin-auth-oauth2": "^2.21.3", @@ -7465,9 +7560,9 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", - "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7488,9 +7583,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz", - "integrity": "sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", + "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", "cpu": [ "arm" ], @@ -7502,9 +7597,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.49.0.tgz", - "integrity": "sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", + "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", "cpu": [ "arm64" ], @@ -7516,9 +7611,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz", - "integrity": "sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", + "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", "cpu": [ "arm64" ], @@ -7530,9 +7625,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.49.0.tgz", - "integrity": "sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", + "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", "cpu": [ "x64" ], @@ -7544,9 +7639,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.49.0.tgz", - "integrity": "sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", + "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", "cpu": [ "arm64" ], @@ -7558,9 +7653,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.49.0.tgz", - "integrity": "sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", + "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", "cpu": [ "x64" ], @@ -7572,9 +7667,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.49.0.tgz", - "integrity": "sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", + "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", "cpu": [ "arm" ], @@ -7586,9 +7681,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.49.0.tgz", - "integrity": "sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", + "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", "cpu": [ "arm" ], @@ -7600,9 +7695,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.49.0.tgz", - "integrity": "sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", + "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", "cpu": [ "arm64" ], @@ -7614,9 +7709,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.49.0.tgz", - "integrity": "sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", + "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", "cpu": [ "arm64" ], @@ -7628,9 +7723,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.49.0.tgz", - "integrity": "sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", + "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", "cpu": [ "loong64" ], @@ -7642,9 +7737,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.49.0.tgz", - "integrity": "sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", + "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", "cpu": [ "ppc64" ], @@ -7656,9 +7751,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.49.0.tgz", - "integrity": "sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", + "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", "cpu": [ "riscv64" ], @@ -7670,9 +7765,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.49.0.tgz", - "integrity": "sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", + "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", "cpu": [ "riscv64" ], @@ -7684,9 +7779,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.49.0.tgz", - "integrity": "sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", + "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", "cpu": [ "s390x" ], @@ -7698,9 +7793,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.49.0.tgz", - "integrity": "sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", + "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", "cpu": [ "x64" ], @@ -7712,9 +7807,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.49.0.tgz", - "integrity": "sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", + "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", "cpu": [ "x64" ], @@ -7725,10 +7820,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", + "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.49.0.tgz", - "integrity": "sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", + "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", "cpu": [ "arm64" ], @@ -7740,9 +7849,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.49.0.tgz", - "integrity": "sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", + "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", "cpu": [ "ia32" ], @@ -7754,9 +7863,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.49.0.tgz", - "integrity": "sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", + "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", "cpu": [ "x64" ], @@ -7768,9 +7877,9 @@ ] }, "node_modules/@rollup/wasm-node": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.49.0.tgz", - "integrity": "sha512-SOBhjoq4JEY3pOhLu8qdIKftaR58j5/Hbj8zqxb2avjIl0zSYjO9wnJb1fsRsstGqwfwgHOyQOH2Yhe2E7Rmhw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.50.1.tgz", + "integrity": "sha512-3oCUcKNdkemnqy6r12UdAtfYMWywGxVHSCQvtDYeEtnOcOQC/SihSXkO6+rByH2ZhbgfeTbqLiw1NDGfJDptyg==", "dev": true, "license": "MIT", "dependencies": { @@ -7807,14 +7916,14 @@ } }, "node_modules/@schematics/angular": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.2.1.tgz", - "integrity": "sha512-7Vx11KWooiqxP206JEVgz3cp0rRv31PYnocNoPM6UqLhGtlvL9GdgaZHzDhGFEm0hv6DUFrbTGIzB89gXc54Xg==", + "version": "20.2.2", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.2.2.tgz", + "integrity": "sha512-VzJsEIiBmHzJAOVaKHn1CwTuOqvI1GwZuneUk/tmyYKkKdWEgxnoNBvz1ql6eHstkLz3S9yt6aUuAgjQC+J2Xw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.2.1", - "@angular-devkit/schematics": "20.2.1", + "@angular-devkit/core": "20.2.2", + "@angular-devkit/schematics": "20.2.2", "jsonc-parser": "3.3.1" }, "engines": { @@ -8649,6 +8758,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonpath": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.4.tgz", + "integrity": "sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -8656,10 +8772,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", "dev": true, "license": "MIT", "dependencies": { @@ -8676,10 +8799,17 @@ "@types/node": "*" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/openui5": { - "version": "1.138.0", - "resolved": "https://registry.npmjs.org/@types/openui5/-/openui5-1.138.0.tgz", - "integrity": "sha512-gxEgcNkYgFSDvHQs1Rk8lAF5etHeUxLFFRyMn0vVgw7xoIVtbMU+Y0t4E2SP4lLc3b+tIcma3Vq5s5CCmwg+nQ==", + "version": "1.140.0", + "resolved": "https://registry.npmjs.org/@types/openui5/-/openui5-1.140.0.tgz", + "integrity": "sha512-z5S2dVRZi4CFnNj4WGAgEcUbg3C3p1HyJvE57/WZBoMLxERS26KBqAF32yWBTSQCmI+E5T4Kk5vbfFfzppBvkQ==", "dev": true, "license": "MIT", "peer": true, @@ -8818,9 +8948,9 @@ "license": "MIT" }, "node_modules/@ui5/theming-ngx": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@ui5/theming-ngx/-/theming-ngx-0.5.5.tgz", - "integrity": "sha512-WUMGXy7ce754PvuZlRaC0Y1oUWnKfP3fFaLq2nDTzlcPrgHETQLKz4L6o0v7vmFcnuTTDzxPdHYEGuBeaIp0cg==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@ui5/theming-ngx/-/theming-ngx-0.5.6.tgz", + "integrity": "sha512-npfbnFCPkXnwzMja0ujSoG16UPUVzP7CwxLyWGGSpRfD4NEkPERZZltq/RLOSTbIkp81atuWHtV1hT5d5fQoRQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -8832,39 +8962,39 @@ } }, "node_modules/@ui5/webcomponents": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents/-/webcomponents-2.13.3.tgz", - "integrity": "sha512-j2l0UTXj2KG94+FiN4J1PJ/ANtMczBwLw9vbABCVaBZ4VnrfFzmoyzSuGs17FLoITvma5H1jONrDLr6WdGBepw==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents/-/webcomponents-2.14.0.tgz", + "integrity": "sha512-ZOJSXPChGMe9R/3ZqzCx1qhyHEazpWofBI+StJe316YcnnynJSJ0a0zuoTKrNac+6DoxwRq3SD3kK1W5TbbNZw==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@ui5/webcomponents-base": "2.13.3", - "@ui5/webcomponents-icons": "2.13.3", - "@ui5/webcomponents-icons-business-suite": "2.13.3", - "@ui5/webcomponents-icons-tnt": "2.13.3", - "@ui5/webcomponents-localization": "2.13.3", - "@ui5/webcomponents-theming": "2.13.3" + "@ui5/webcomponents-base": "2.14.0", + "@ui5/webcomponents-icons": "2.14.0", + "@ui5/webcomponents-icons-business-suite": "2.14.0", + "@ui5/webcomponents-icons-tnt": "2.14.0", + "@ui5/webcomponents-localization": "2.14.0", + "@ui5/webcomponents-theming": "2.14.0" } }, "node_modules/@ui5/webcomponents-ai": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-ai/-/webcomponents-ai-2.13.3.tgz", - "integrity": "sha512-AJ0CvS1WpAyzT7qyBz29mfds6Ugg4NHe/9cME5GbC2ckHZi5lGB3yZYGIunzSh7racafc+QwZnJ20WxaTkRqVA==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-ai/-/webcomponents-ai-2.14.0.tgz", + "integrity": "sha512-SHx1UtK/vgOIQaOBnKhL9LzYxila5gXVc8pm2BiwFlZ4NY1vclgE3+NvukTG/A+Z6YA521ao7xXLlXK0411sZw==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@ui5/webcomponents": "2.13.3", - "@ui5/webcomponents-base": "2.13.3", - "@ui5/webcomponents-icons": "2.13.3", - "@ui5/webcomponents-theming": "2.13.3" + "@ui5/webcomponents": "2.14.0", + "@ui5/webcomponents-base": "2.14.0", + "@ui5/webcomponents-icons": "2.14.0", + "@ui5/webcomponents-theming": "2.14.0" } }, "node_modules/@ui5/webcomponents-base": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-base/-/webcomponents-base-2.13.3.tgz", - "integrity": "sha512-W0gK5DD1aTyLQiYYHRlkNLbdyxmkruR9hIVvYjV2vm16JfcLvaXESq/u49aATHAyAxDqdwI3ffxnRRNMgMpYxA==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-base/-/webcomponents-base-2.14.0.tgz", + "integrity": "sha512-fu5+YjNFDcie2IiluYnp0nlkyluaN3JA997+zcwgXZFUU+2Vqm6eOpeTuXJBepVNcgMKo2uz0cdC1+rsL3gciQ==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -8874,101 +9004,101 @@ } }, "node_modules/@ui5/webcomponents-fiori": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-fiori/-/webcomponents-fiori-2.13.3.tgz", - "integrity": "sha512-1bBQICyhGyM4GRgNqfbkJft5twUkbMvBzIhL5SgweyFFy3imWjvf3Ic+TfZRqA3rpg6cVsm6nUstTRk5hTzZ2Q==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-fiori/-/webcomponents-fiori-2.14.0.tgz", + "integrity": "sha512-LXwmPmcNlxGN0WZKz5shfVltnb4dPoYYFAxxjFj+3UG2WJ+NB09dME8MMChy5Dq9jdqmXgF3wWRvXLzzVSgdIg==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@ui5/webcomponents": "2.13.3", - "@ui5/webcomponents-base": "2.13.3", - "@ui5/webcomponents-icons": "2.13.3", - "@ui5/webcomponents-theming": "2.13.3", + "@ui5/webcomponents": "2.14.0", + "@ui5/webcomponents-base": "2.14.0", + "@ui5/webcomponents-icons": "2.14.0", + "@ui5/webcomponents-theming": "2.14.0", "@zxing/library": "^0.21.3" } }, "node_modules/@ui5/webcomponents-icons": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-icons/-/webcomponents-icons-2.13.3.tgz", - "integrity": "sha512-dJsCWKrwct0WlQ+PLvFpQS7gPz3UDCwVvF7aDgbaOT7Hz1uTzTaCYgMtI9DxbMpcoIZjSPvmk1rJyU1KaAwQvA==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-icons/-/webcomponents-icons-2.14.0.tgz", + "integrity": "sha512-pGFhJjGxkLYpFo52nRWu/DC24Y6yg9XJ06rBw999TW6WTE0Ay1dcBnhp/Nfi8KEgDdY3oBlxQlWXcLxjrf5t1Q==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@ui5/webcomponents-base": "2.13.3" + "@ui5/webcomponents-base": "2.14.0" } }, "node_modules/@ui5/webcomponents-icons-business-suite": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-icons-business-suite/-/webcomponents-icons-business-suite-2.13.3.tgz", - "integrity": "sha512-8Jbr6D5y5ION8HwAW5TXHQpj4m00Atq9jJqcvKh3iMN+SO2tnRMHhHTt18CDe6tm7pWjcVyX853FP2Z2V7ipKA==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-icons-business-suite/-/webcomponents-icons-business-suite-2.14.0.tgz", + "integrity": "sha512-Kl9R4c3B5LELZhejZ/ZaPbV372VjVKNCt4q8kSBih0i3Ekp6k6oM1dVcTIJd3ttJ5r0iDFBR1wgtxn5RNDrygg==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@ui5/webcomponents-base": "2.13.3" + "@ui5/webcomponents-base": "2.14.0" } }, "node_modules/@ui5/webcomponents-icons-tnt": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-icons-tnt/-/webcomponents-icons-tnt-2.13.3.tgz", - "integrity": "sha512-dinUIrkztvmmCFehGoqwnk8Fom5ru9AlX/E7dRg3f4aPbFPdkGlYdYOyvJdb66k2w/HcxIATTzRw6OmGakph1A==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-icons-tnt/-/webcomponents-icons-tnt-2.14.0.tgz", + "integrity": "sha512-8nslq04M9pS7ZRI9hVO9VhJWuX/hlIMXKevH0X3Cgw8suzy6yib6n552XvltSM11XMpAmReVjUeTa1n5OgbblQ==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@ui5/webcomponents-base": "2.13.3" + "@ui5/webcomponents-base": "2.14.0" } }, "node_modules/@ui5/webcomponents-localization": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-localization/-/webcomponents-localization-2.13.3.tgz", - "integrity": "sha512-a7K41JIo4FPqPUjpZ1cBt3JQKgDEsoT8go+8dN3FcRLKE+9z+Troy2xNE8/Dn4LkMQ/zVpWI5psQpfocvGhbkA==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-localization/-/webcomponents-localization-2.14.0.tgz", + "integrity": "sha512-XyYvsJF13p5wE+VTkVucJKe5vvTFfbdSOQi4CBzlhcrtCHZHq0CY3nq8eLzSd3buDHN6XOvWTny1VRmcz5I3rA==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { "@types/openui5": "^1.113.0", - "@ui5/webcomponents-base": "2.13.3" + "@ui5/webcomponents-base": "2.14.0" } }, "node_modules/@ui5/webcomponents-ngx": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-ngx/-/webcomponents-ngx-0.5.5.tgz", - "integrity": "sha512-z6AlxGzSMwF3KmQpWQBB/nVLf3Pq00Zeb2W3QnCEzM+Ulf6N0SjUIKjbfvrvOuIa4y3KpjZwlcttH4BofOTDSw==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-ngx/-/webcomponents-ngx-0.5.6.tgz", + "integrity": "sha512-fUZOAG1DuRk1+kRTPq3me4e4SaA0zwPOcmFn6YSywzXjmDgJVLdM1Cn9lWZafCLDmkSQFGohXRLn8XlWMYmc/w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@ui5/theming-ngx": "^0.5.5", + "@ui5/theming-ngx": "^0.5.6", "tslib": "^2.4.1" }, "peerDependencies": { "@angular/common": "^20.0.0", "@angular/core": "^20.0.0", "@angular/forms": "^20.0.0", - "@ui5/webcomponents": "2.13.3", - "@ui5/webcomponents-ai": "2.13.3", - "@ui5/webcomponents-base": "2.13.3", - "@ui5/webcomponents-fiori": "2.13.3", - "@ui5/webcomponents-icons": "2.13.3", - "@ui5/webcomponents-icons-business-suite": "2.13.3", - "@ui5/webcomponents-icons-tnt": "2.13.3", - "@ui5/webcomponents-theming": "2.13.3", + "@ui5/webcomponents": "2.14.0", + "@ui5/webcomponents-ai": "2.14.0", + "@ui5/webcomponents-base": "2.14.0", + "@ui5/webcomponents-fiori": "2.14.0", + "@ui5/webcomponents-icons": "2.14.0", + "@ui5/webcomponents-icons-business-suite": "2.14.0", + "@ui5/webcomponents-icons-tnt": "2.14.0", + "@ui5/webcomponents-theming": "2.14.0", "fast-deep-equal": "^3.1.3", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@ui5/webcomponents-theming": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-theming/-/webcomponents-theming-2.13.3.tgz", - "integrity": "sha512-d3SElREhXTTaVSxrVzvrqAI4aFZj8ajOHLgXuWkoE9dGdPAYpFbbK9Gc6uSa0ZcDFiY70xw64YxSnMpeId9MwA==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-theming/-/webcomponents-theming-2.14.0.tgz", + "integrity": "sha512-L+CHet/jA8yYlPjr5lBZDZGzZeuytHJFC+yt4n6POdVRpXSpctcJIoFXUX3PJrH5s/Uc6dYKvQKMf1Toxw3HPw==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { "@sap-theming/theming-base-content": "11.29.3", - "@ui5/webcomponents-base": "2.13.3" + "@ui5/webcomponents-base": "2.14.0" } }, "node_modules/@ui5/webcomponents-theming/node_modules/@sap-theming/theming-base-content": { @@ -9605,6 +9735,16 @@ "dev": true, "license": "MIT" }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -10126,6 +10266,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/cacache/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/cacache/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -10149,6 +10305,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/cacache/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/cacache/node_modules/tar": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", @@ -10228,10 +10401,28 @@ "node": ">=6" } }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { - "version": "1.0.30001737", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz", - "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==", + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", "dev": true, "funding": [ { @@ -10895,6 +11086,35 @@ } } }, + "node_modules/cpx2": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/cpx2/-/cpx2-8.0.0.tgz", + "integrity": "sha512-RxD9jrSVNSOmfcbiPlr3XnKbUKH9K1w2HCv0skczUKhsZTueiDBecxuaSAKQkYSLQaGVA4ZQJZlTj5hVNNEvKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debounce": "^2.0.0", + "debug": "^4.1.1", + "duplexer": "^0.1.1", + "fs-extra": "^11.1.0", + "glob": "^11.0.0", + "glob2base": "0.0.12", + "ignore": "^6.0.2", + "minimatch": "^10.0.1", + "p-map": "^7.0.0", + "resolve": "^1.12.0", + "safe-buffer": "^5.2.0", + "shell-quote": "^1.8.0", + "subarg": "^1.0.0" + }, + "bin": { + "cpx": "bin/index.js" + }, + "engines": { + "node": "^20.0.0 || >=22.0.0", + "npm": ">=10" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -11091,6 +11311,19 @@ "node": ">=12" } }, + "node_modules/debounce": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", + "integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -11109,6 +11342,43 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -11117,9 +11387,9 @@ "license": "MIT" }, "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -11395,6 +11665,13 @@ "node": ">= 0.4" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -11426,9 +11703,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.211", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz", - "integrity": "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==", + "version": "1.5.214", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.214.tgz", + "integrity": "sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q==", "dev": true, "license": "ISC" }, @@ -11740,20 +12017,20 @@ } }, "node_modules/eslint": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", - "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", + "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.34.0", + "@eslint/js": "9.35.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -11851,7 +12128,18 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/json-schema-traverse": { + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", @@ -11859,6 +12147,20 @@ "license": "MIT", "peer": true }, + "node_modules/eslint/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, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -11986,13 +12288,13 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.5.tgz", - "integrity": "sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", "dev": true, "license": "MIT", "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, "node_modules/execa": { @@ -12036,15 +12338,15 @@ } }, "node_modules/expect": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.1.tgz", - "integrity": "sha512-OKe7cdic4qbfWd/CcgwJvvCrNX2KWfuMZee9AfJHL1gTYmvqjBjZG1a2NwfhspBzxzlXwsN75WWpKTYfsJpBxg==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.2.tgz", + "integrity": "sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.1.1", + "@jest/expect-utils": "30.1.2", "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.1.1", + "jest-matcher-utils": "30.1.2", "jest-message-util": "30.1.0", "jest-mock": "30.0.5", "jest-util": "30.0.5" @@ -12408,6 +12710,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-index": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz", + "integrity": "sha512-uJ5vWrfBKMcE6y2Z8834dwEZj9mNGxYa3t3I53OwFeuZ8D9oc2E5zcsrkuhX6h4iYrjhiv0T3szQmxlAV9uxDg==", + "dev": true, + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -12594,6 +12903,21 @@ "node": ">= 0.8" } }, + "node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs-minipass": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", @@ -12681,9 +13005,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.1.tgz", + "integrity": "sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==", "dev": true, "license": "MIT", "engines": { @@ -12756,22 +13080,24 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "dev": true, "license": "ISC", "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.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -12814,6 +13140,18 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/glob2base": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", + "integrity": "sha512-ZyqlgowMbfj2NPjxaZZ/EtsXlOch28FRXgMd64vqZWk1bT9+wvSRLYD1om9M7QfQru51zJPAT17qXm4/zd+9QA==", + "dev": true, + "dependencies": { + "find-index": "^0.1.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -12908,6 +13246,16 @@ "dev": true, "license": "MIT" }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -12972,27 +13320,11 @@ } }, "node_modules/hosted-git-info": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.0.tgz", - "integrity": "sha512-gEf705MZLrDPkbbhi8PnoO4ZwYgKoNL+ISZ3AjZMht2r3N5tuTwncyDi6Fv2/qDnMmZxgs0yI8WDOyR8q3G+SQ==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^11.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } + "license": "ISC" }, "node_modules/hpack.js": { "version": "2.1.6", @@ -13263,12 +13595,11 @@ } }, "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", + "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 4" } @@ -13293,22 +13624,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/ignore-walk/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "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", @@ -13377,6 +13692,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -14218,16 +14543,13 @@ } }, "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, "node_modules/is-plain-object": { @@ -14409,19 +14731,19 @@ } }, "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, + "engines": { + "node": "20 || >=22" + }, "funding": { "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/jake": { @@ -14806,6 +15128,16 @@ "node": ">=12" } }, + "node_modules/jest-cli/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/jest-config": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", @@ -14865,6 +15197,28 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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/jest-config/node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -14883,6 +15237,19 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-config/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/jest-config/node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -14932,9 +15299,9 @@ } }, "node_modules/jest-diff": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.1.tgz", - "integrity": "sha512-LUU2Gx8EhYxpdzTR6BmjL1ifgOAQJQELTHOiPv9KITaKjZvJ9Jmgigx01tuZ49id37LorpGc9dPBPlXTboXScw==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", + "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15498,15 +15865,15 @@ "license": "MIT" }, "node_modules/jest-matcher-utils": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.1.tgz", - "integrity": "sha512-SuH2QVemK48BNTqReti6FtjsMPFsSOD/ZzRxU1TttR7RiRsRSe78d03bb4Cx6D4bQC/80Q8U4VnaaAH9FlbZ9w==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.2.tgz", + "integrity": "sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.1.1", + "jest-diff": "30.1.2", "pretty-format": "30.0.5" }, "engines": { @@ -15567,9 +15934,9 @@ } }, "node_modules/jest-message-util/node_modules/@sinclair/typebox": { - "version": "0.34.40", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.40.tgz", - "integrity": "sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "dev": true, "license": "MIT" }, @@ -15635,9 +16002,9 @@ } }, "node_modules/jest-mock/node_modules/@sinclair/typebox": { - "version": "0.34.40", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.40.tgz", - "integrity": "sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "dev": true, "license": "MIT" }, @@ -16023,6 +16390,28 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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/jest-runtime/node_modules/jest-message-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", @@ -16077,6 +16466,19 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-runtime/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/jest-runtime/node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -16344,9 +16746,9 @@ } }, "node_modules/jest-util/node_modules/@sinclair/typebox": { - "version": "0.34.40", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.40.tgz", - "integrity": "sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "dev": true, "license": "MIT" }, @@ -16751,6 +17153,19 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -17342,13 +17757,13 @@ "license": "MIT" }, "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", - "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "dev": true, "license": "MIT", "dependencies": { - "get-east-asian-width": "^1.0.0" + "get-east-asian-width": "^1.3.1" }, "engines": { "node": ">=18" @@ -17549,6 +17964,19 @@ "tmpl": "1.0.5" } }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -17591,29 +18019,68 @@ "url": "https://github.com/sponsors/streamich" } }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "node_modules/meow": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz", + "integrity": "sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==", "dev": true, "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "^4.0.2", + "normalize-package-data": "^2.5.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.13.1", + "yargs-parser": "^18.1.3" + }, "engines": { - "node": ">=18" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "node_modules/meow/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", @@ -17717,6 +18184,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/mini-css-extract-plugin": { "version": "2.9.4", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", @@ -17746,16 +18223,19 @@ "license": "ISC" }, "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": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": "*" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -17768,6 +18248,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -17937,6 +18432,24 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/move-cli": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/move-cli/-/move-cli-2.0.0.tgz", + "integrity": "sha512-/YUsTv5Gwemt9Iv2YkyVJvqphssA97I5fc2fr1Ak+Buh4pSDIPCTunx+wespnsEK3m31xVYwj8btzmdfUM90Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "meow": "^6.0.0", + "mv": "^2.1.1" + }, + "bin": { + "move": "cli.js", + "move-cli": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -18012,6 +18525,79 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/mv/node_modules/glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mv/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mv/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mv/node_modules/rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^6.0.1" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -18038,6 +18624,16 @@ "dev": true, "license": "MIT" }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "dev": true, + "license": "MIT", + "bin": { + "ncp": "bin/ncp" + } + }, "node_modules/needle": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", @@ -18123,6 +18719,21 @@ } } }, + "node_modules/ngx-build-plus": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/ngx-build-plus/-/ngx-build-plus-20.0.0.tgz", + "integrity": "sha512-cm1ZMTACAN3DEqBt/alS84zwVGgL5HAl5Dk/wh7CPyGUBQnLaxiAhjFZ6iykxgSO3e9ebIZmDBvTC480piC1eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "webpack-merge": "^6.0.0" + }, + "peerDependencies": { + "@angular-devkit/build-angular": ">=20.0.0", + "@schematics/angular": ">=20.0.0", + "rxjs": ">= 6.0.0" + } + }, "node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", @@ -18254,9 +18865,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz", + "integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==", "dev": true, "license": "MIT" }, @@ -18337,6 +18948,19 @@ "node": ">=4" } }, + "node_modules/nodemon/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/nodemon/node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -18392,6 +19016,29 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -18464,6 +19111,29 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/npm-package-arg/node_modules/hosted-git-info": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.0.tgz", + "integrity": "sha512-gEf705MZLrDPkbbhi8PnoO4ZwYgKoNL+ISZ3AjZMht2r3N5tuTwncyDi6Fv2/qDnMmZxgs0yI8WDOyR8q3G+SQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg/node_modules/lru-cache": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", + "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/npm-packlist": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.1.tgz", @@ -18612,9 +19282,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.21", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", - "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", "dev": true, "license": "MIT" }, @@ -19229,37 +19899,41 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", + "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", "dev": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": "20 || >=22" + } }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=16" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/picocolors": { @@ -19587,9 +20261,9 @@ } }, "node_modules/pretty-format/node_modules/@sinclair/typebox": { - "version": "0.34.40", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.40.tgz", - "integrity": "sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "dev": true, "license": "MIT" }, @@ -19784,6 +20458,16 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -19805,19 +20489,36 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.6.3", + "iconv-lite": "0.7.0", "unpipe": "1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/react-is": { @@ -19828,6 +20529,116 @@ "license": "MIT", "peer": true }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -19857,6 +20668,20 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -20187,89 +21012,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rolldown": { "version": "1.0.0-beta.32", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.32.tgz", @@ -20303,9 +21045,9 @@ } }, "node_modules/rollup": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.49.0.tgz", - "integrity": "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", + "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", "dev": true, "license": "MIT", "dependencies": { @@ -20319,26 +21061,27 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.49.0", - "@rollup/rollup-android-arm64": "4.49.0", - "@rollup/rollup-darwin-arm64": "4.49.0", - "@rollup/rollup-darwin-x64": "4.49.0", - "@rollup/rollup-freebsd-arm64": "4.49.0", - "@rollup/rollup-freebsd-x64": "4.49.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.49.0", - "@rollup/rollup-linux-arm-musleabihf": "4.49.0", - "@rollup/rollup-linux-arm64-gnu": "4.49.0", - "@rollup/rollup-linux-arm64-musl": "4.49.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.49.0", - "@rollup/rollup-linux-ppc64-gnu": "4.49.0", - "@rollup/rollup-linux-riscv64-gnu": "4.49.0", - "@rollup/rollup-linux-riscv64-musl": "4.49.0", - "@rollup/rollup-linux-s390x-gnu": "4.49.0", - "@rollup/rollup-linux-x64-gnu": "4.49.0", - "@rollup/rollup-linux-x64-musl": "4.49.0", - "@rollup/rollup-win32-arm64-msvc": "4.49.0", - "@rollup/rollup-win32-ia32-msvc": "4.49.0", - "@rollup/rollup-win32-x64-msvc": "4.49.0", + "@rollup/rollup-android-arm-eabi": "4.50.1", + "@rollup/rollup-android-arm64": "4.50.1", + "@rollup/rollup-darwin-arm64": "4.50.1", + "@rollup/rollup-darwin-x64": "4.50.1", + "@rollup/rollup-freebsd-arm64": "4.50.1", + "@rollup/rollup-freebsd-x64": "4.50.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", + "@rollup/rollup-linux-arm-musleabihf": "4.50.1", + "@rollup/rollup-linux-arm64-gnu": "4.50.1", + "@rollup/rollup-linux-arm64-musl": "4.50.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", + "@rollup/rollup-linux-ppc64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-musl": "4.50.1", + "@rollup/rollup-linux-s390x-gnu": "4.50.1", + "@rollup/rollup-linux-x64-gnu": "4.50.1", + "@rollup/rollup-linux-x64-musl": "4.50.1", + "@rollup/rollup-openharmony-arm64": "4.50.1", + "@rollup/rollup-win32-arm64-msvc": "4.50.1", + "@rollup/rollup-win32-ia32-msvc": "4.50.1", + "@rollup/rollup-win32-x64-msvc": "4.50.1", "fsevents": "~2.3.2" } }, @@ -21616,6 +22359,19 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", @@ -21626,6 +22382,16 @@ "node": ">=8" } }, + "node_modules/subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.1.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -21900,6 +22666,41 @@ "node": ">=8" } }, + "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==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/thingies": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", @@ -22017,6 +22818,16 @@ "node": ">=6" } }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/tr46": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", @@ -22031,9 +22842,9 @@ } }, "node_modules/tree-dump": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.3.tgz", - "integrity": "sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -22057,6 +22868,16 @@ "tree-kill": "cli.js" } }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ts-custom-error": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz", @@ -22191,6 +23012,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ts-jest/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -22447,13 +23278,13 @@ } }, "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 4.0.0" + "node": ">= 10.0.0" } }, "node_modules/unpipe": { @@ -23163,6 +23994,19 @@ "node": ">= 10" } }, + "node_modules/webpack-dev-server/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/webpack-dev-server/node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -23759,13 +24603,17 @@ } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, "engines": { - "node": ">=12" + "node": ">=6" } }, "node_modules/yargs/node_modules/ansi-regex": { diff --git a/package.json b/package.json index cf78227..c1f7d1f 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,18 @@ "version": "0.1.5", "scripts": { "ng": "ng", - "start": "ng serve", - "build": "ng build", - "build:watch": "mkdirp dist && nodemon --ignore dist --ext js,yml,yaml,ts,html,css,scss,json,md --exec \"rimraf dist && npm run build && cd dist && yalc publish --push --sig\"", - "test": "ng test" + "build": "npm run build:lib && npm run build:wc", + "build:dev": "npm run build:lib && npm run build:wc:dev", + "build:lib": "ng build lib", + "build:wc": "ng build wc && npm run build:wc:rename && npm run build:wc:copy", + "build:wc:rename": "move-cli dist-wc/assets/main.js dist-wc/assets/platform-mesh-portal-ui-wc.js", + "build:wc:copy": "cpx \"dist-wc/**/*\" dist", + "build:wc:dev": "ng build wc --configuration development && npm run build:wc:rename && npm run build:wc:copy", + "build:watch": "mkdirp dist && nodemon --ignore dist --ignore dist-wc --ext js,yml,yaml,ts,html,css,scss,json,md --exec \"rimraf dist && npm run build:dev && cd dist && yalc publish --push --sig\"", + "watch": "npm run build --watch --configuration development", + "test": "ng test", + "test:lib": "ng test lib", + "test:wc": "ng test wc" }, "prettier": "@openmfp/config-prettier", "dependencies": { @@ -21,17 +29,21 @@ "@angular/compiler-cli": "^20.2.1", "@angular/localize": "^20.2.1", "@briebug/jest-schematic": "^6.0.0", - "@openmfp/portal-ui-lib": "0.174.2", + "@openmfp/portal-ui-lib": "^0.176.0", "@types/jest": "^30.0.0", "@types/jmespath": "0.15.2", + "@types/jsonpath": "^0.2.4", "@ui5/webcomponents-ngx": "^0.5.0", + "cpx2": "^8.0.0", "jest": "^29.7.0", "jest-jasmine2": "29.7.0", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jmespath": "0.16.0", + "move-cli": "^2.0.0", "mkdirp": "^3.0.1", "ng-packagr": "^20.2.0", + "ngx-build-plus": "^20.0.0", "nodemon": "3.1.10", "rimraf": "6.0.1", "ts-jest": "29.3.2", diff --git a/projects/lib/_mocks_/luigi-client-support-angular.ts b/projects/lib/_mocks_/luigi-client-support-angular.ts deleted file mode 100644 index 87f8721..0000000 --- a/projects/lib/_mocks_/luigi-client-support-angular.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Observable, of } from 'rxjs'; -export class LuigiContextService { - contextObservable(): Observable<{ context: any }> { - return of({ context: null }); - } -} diff --git a/projects/lib/jest.config.js b/projects/lib/jest.config.js index f1d91f0..3daf81d 100644 --- a/projects/lib/jest.config.js +++ b/projects/lib/jest.config.js @@ -2,8 +2,9 @@ const path = require('path'); module.exports = { displayName: 'lib', + roots: [__dirname], + testMatch: ['**/*.spec.ts'], coverageDirectory: path.resolve(__dirname, '../../coverage/lib'), - setupFilesAfterEnv: [`${__dirname}/jest.setup.ts`], coverageThreshold: { global: { branches: 67, @@ -12,4 +13,10 @@ module.exports = { statements: -16, }, }, + moduleNameMapper: { + '^@platform-mesh/portal-ui-lib$': '/projects/lib/public-api.ts', + '^@platform-mesh/portal-ui-lib/services$': '/projects/lib/services/public-api.ts', + '^@platform-mesh/portal-ui-lib/utils$': '/projects/lib/utils/public-api.ts', + '^@platform-mesh/portal-ui-lib/(.*)': '/projects/lib/$1', + }, }; diff --git a/projects/lib/jest.setup.ts b/projects/lib/jest.setup.ts deleted file mode 100644 index 39cf8da..0000000 --- a/projects/lib/jest.setup.ts +++ /dev/null @@ -1 +0,0 @@ -jest.requireMock('./_mocks_/ui5‑mock'); diff --git a/projects/lib/organization/provide-organization-feature.ts b/projects/lib/organization/provide-organization-feature.ts index 91777bd..1899e13 100644 --- a/projects/lib/organization/provide-organization-feature.ts +++ b/projects/lib/organization/provide-organization-feature.ts @@ -1,20 +1,9 @@ import { makeEnvironmentProviders } from '@angular/core'; -import { provideRouter } from '@angular/router'; -import { - LuigiContextService, - LuigiContextServiceImpl, -} from '@luigi-project/client-support-angular'; import { organizationInitializer } from './initializers/organization-initializer'; -import { routes } from './routes'; export const provideOrganizationFeature = () => { return makeEnvironmentProviders([ - provideRouter(routes), organizationInitializer(), - { - provide: LuigiContextService, - useClass: LuigiContextServiceImpl, - } ]); }; diff --git a/projects/lib/organization/routes.ts b/projects/lib/organization/routes.ts deleted file mode 100644 index e0b72b2..0000000 --- a/projects/lib/organization/routes.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Route } from '@angular/router'; -import { OrganizationManagementComponent } from './components/organization-management/organization-management.component'; - -export const routes: Route[] = [ - { - path: 'organization-management', - component: OrganizationManagementComponent, - }, -]; diff --git a/projects/lib/package.json b/projects/lib/package.json index 507d011..cb4329f 100644 --- a/projects/lib/package.json +++ b/projects/lib/package.json @@ -15,8 +15,12 @@ "@angular/platform-browser-dynamic": "^19.0.0 || ^20.0.0", "@angular/router": "^19.0.0 || ^20.0.0", "@ui5/webcomponents-ngx": "^0.4.8 || ^0.5.0", - "@luigi-project/client-support-angular": "^20.0.1", + "apollo-angular": "^10.0.0 || ^11.0.0", + "gql-query-builder": "^3.8.0", + "graphql": "^17.0.0 || ^16.10.0", + "graphql-sse": "2.5.4", "rxjs": "~7.8.0", - "zone.js": "~0.15.1" + "zone.js": "~0.15.1", + "jsonpath": "^1.1.1" } } diff --git a/projects/lib/portal-options/services/crd-gateway-kcp-patch-resolver.service.spec.ts b/projects/lib/portal-options/services/crd-gateway-kcp-patch-resolver.service.spec.ts index 1dcdae6..c38f328 100644 --- a/projects/lib/portal-options/services/crd-gateway-kcp-patch-resolver.service.spec.ts +++ b/projects/lib/portal-options/services/crd-gateway-kcp-patch-resolver.service.spec.ts @@ -1,12 +1,11 @@ -import { kcpRootOrgsPath } from '../models/constants'; -import { PortalLuigiNode } from '../models/luigi-node'; -import { CrdGatewayKcpPatchResolver } from './crd-gateway-kcp-patch-resolver.service'; import { TestBed } from '@angular/core/testing'; import { - EnvConfigService, - GatewayService, - LuigiCoreService, + EnvConfigService } from '@openmfp/portal-ui-lib'; +import { GatewayService } from '@platform-mesh/portal-ui-lib/services/resource'; +import { kcpRootOrgsPath } from '../models/constants'; +import { PortalLuigiNode } from '../models/luigi-node'; +import { CrdGatewayKcpPatchResolver } from './crd-gateway-kcp-patch-resolver.service'; describe('CrdGatewayKcpPatchResolver', () => { let resolver: CrdGatewayKcpPatchResolver; @@ -86,4 +85,79 @@ describe('CrdGatewayKcpPatchResolver', () => { gatewayServiceMock.updateCrdGatewayUrlWithEntityPath, ).toHaveBeenCalledWith(`${kcpRootOrgsPath}:org1`); }); + + describe('resolveCrdGatewayKcpPathForNextAccountEntity', () => { + it('should return early if kind is not Account', async () => { + const nextNode: PortalLuigiNode = { context: {}, parent: undefined } as any; + + await resolver.resolveCrdGatewayKcpPathForNextAccountEntity( + 'leafAcc', + 'Project', + nextNode, + ); + + expect(gatewayServiceMock.updateCrdGatewayUrlWithEntityPath).not.toHaveBeenCalled(); + expect(envConfigServiceMock.getEnvConfig).not.toHaveBeenCalled(); + }); + + it('should return early if entityId is empty', async () => { + const nextNode: PortalLuigiNode = { context: {}, parent: undefined } as any; + + await resolver.resolveCrdGatewayKcpPathForNextAccountEntity( + '', + 'Account', + nextNode, + ); + + expect(gatewayServiceMock.updateCrdGatewayUrlWithEntityPath).not.toHaveBeenCalled(); + expect(envConfigServiceMock.getEnvConfig).not.toHaveBeenCalled(); + }); + + it('should aggregate parent Account entities and append entityId', async () => { + const nextNode: PortalLuigiNode = { + context: {}, + parent: { + context: { entity: { metadata: { name: 'acc2' }, __typename: 'Account' } }, + parent: { + context: { entity: { metadata: { name: 'team1' }, __typename: 'Team' } }, + parent: { + context: { entity: { metadata: { name: 'acc1' }, __typename: 'Account' } }, + parent: undefined, + }, + }, + }, + } as any; + + await resolver.resolveCrdGatewayKcpPathForNextAccountEntity( + 'leafAcc', + 'Account', + nextNode, + ); + + expect(envConfigServiceMock.getEnvConfig).toHaveBeenCalled(); + expect( + gatewayServiceMock.updateCrdGatewayUrlWithEntityPath, + ).toHaveBeenCalledWith(`${kcpRootOrgsPath}:org1:acc1:acc2:leafAcc`); + }); + + it('should use kcpPath from node context if provided (override)', async () => { + const nextNode: PortalLuigiNode = { + context: { kcpPath: 'overridePath' }, + parent: { + context: { entity: { metadata: { name: 'accParent' }, __typename: 'Account' } }, + parent: undefined, + }, + } as any; + + await resolver.resolveCrdGatewayKcpPathForNextAccountEntity( + 'leafAcc', + 'Account', + nextNode, + ); + + expect( + gatewayServiceMock.updateCrdGatewayUrlWithEntityPath, + ).toHaveBeenCalledWith('overridePath'); + }); + }); }); diff --git a/projects/lib/portal-options/services/crd-gateway-kcp-patch-resolver.service.ts b/projects/lib/portal-options/services/crd-gateway-kcp-patch-resolver.service.ts index f52c4b5..5bc69c4 100644 --- a/projects/lib/portal-options/services/crd-gateway-kcp-patch-resolver.service.ts +++ b/projects/lib/portal-options/services/crd-gateway-kcp-patch-resolver.service.ts @@ -1,5 +1,6 @@ import { Injectable, inject } from '@angular/core'; -import { EnvConfigService, GatewayService } from '@openmfp/portal-ui-lib'; +import { EnvConfigService } from '@openmfp/portal-ui-lib'; +import { GatewayService } from '@platform-mesh/portal-ui-lib/services'; import { kcpRootOrgsPath } from '../models/constants'; import { PortalLuigiNode } from '../models/luigi-node'; diff --git a/projects/lib/portal-options/services/header-bar-renderers/namespace-selection-renderer.service.spec.ts b/projects/lib/portal-options/services/header-bar-renderers/namespace-selection-renderer.service.spec.ts index 4770de1..a537850 100644 --- a/projects/lib/portal-options/services/header-bar-renderers/namespace-selection-renderer.service.spec.ts +++ b/projects/lib/portal-options/services/header-bar-renderers/namespace-selection-renderer.service.spec.ts @@ -1,6 +1,7 @@ import { TestBed } from '@angular/core/testing'; +import { AuthService, LuigiCoreService } from '@openmfp/portal-ui-lib'; +import { ResourceService } from '@platform-mesh/portal-ui-lib/services'; import { of } from 'rxjs'; -import { AuthService, LuigiCoreService, ResourceService } from '@openmfp/portal-ui-lib'; import { NamespaceSelectionRendererService } from './namespace-selection-renderer.service'; jest.mock('@ui5/webcomponents/dist/ComboBox.js', () => ({}), { virtual: true }); diff --git a/projects/lib/portal-options/services/header-bar-renderers/namespace-selection-renderer.service.ts b/projects/lib/portal-options/services/header-bar-renderers/namespace-selection-renderer.service.ts index 8182fb8..26d3d17 100644 --- a/projects/lib/portal-options/services/header-bar-renderers/namespace-selection-renderer.service.ts +++ b/projects/lib/portal-options/services/header-bar-renderers/namespace-selection-renderer.service.ts @@ -7,10 +7,9 @@ import { LuigiNode, PortalConfig, Resource, - ResourceNodeContext, - ResourceService, - generateGraphQLFields, } from '@openmfp/portal-ui-lib'; +import { ResourceNodeContext, ResourceService } from '@platform-mesh/portal-ui-lib/services'; +import { generateGraphQLFields } from '@platform-mesh/portal-ui-lib/utils'; import '@ui5/webcomponents/dist/ComboBox.js'; import { Observable, of } from 'rxjs'; import { shareReplay } from 'rxjs/operators'; diff --git a/projects/lib/portal-options/services/luigi-extended-global-context-config.service.spec.ts b/projects/lib/portal-options/services/luigi-extended-global-context-config.service.spec.ts index dec9823..2ee2206 100644 --- a/projects/lib/portal-options/services/luigi-extended-global-context-config.service.spec.ts +++ b/projects/lib/portal-options/services/luigi-extended-global-context-config.service.spec.ts @@ -3,8 +3,8 @@ import { AuthService, ConfigService, EnvConfigService, - ResourceService, } from '@openmfp/portal-ui-lib'; +import { ResourceService } from '@platform-mesh/portal-ui-lib/services'; import { of, throwError } from 'rxjs'; import { LuigiExtendedGlobalContextConfigServiceImpl } from './luigi-extended-global-context-config.service'; diff --git a/projects/lib/portal-options/services/luigi-extended-global-context-config.service.ts b/projects/lib/portal-options/services/luigi-extended-global-context-config.service.ts index dd02679..aeeb3e7 100644 --- a/projects/lib/portal-options/services/luigi-extended-global-context-config.service.ts +++ b/projects/lib/portal-options/services/luigi-extended-global-context-config.service.ts @@ -4,8 +4,8 @@ import { ConfigService, EnvConfigService, LuigiExtendedGlobalContextConfigService, - ResourceService, } from '@openmfp/portal-ui-lib'; +import { ResourceService } from '@platform-mesh/portal-ui-lib/services'; import { firstValueFrom } from 'rxjs'; @Injectable({ providedIn: 'root' }) diff --git a/projects/lib/portal-options/services/node-change-hook-config.service.spec.ts b/projects/lib/portal-options/services/node-change-hook-config.service.spec.ts index 6e4edb8..770282b 100644 --- a/projects/lib/portal-options/services/node-change-hook-config.service.spec.ts +++ b/projects/lib/portal-options/services/node-change-hook-config.service.spec.ts @@ -1,7 +1,7 @@ +import { TestBed } from '@angular/core/testing'; +import { LuigiCoreService } from '@openmfp/portal-ui-lib'; import { CrdGatewayKcpPatchResolver } from './crd-gateway-kcp-patch-resolver.service'; import { NodeChangeHookConfigServiceImpl } from './node-change-hook-config.service'; -import { TestBed } from '@angular/core/testing'; -import { GatewayService, LuigiCoreService } from '@openmfp/portal-ui-lib'; describe('NodeChangeHookConfigServiceImpl', () => { let service: NodeChangeHookConfigServiceImpl; diff --git a/projects/lib/portal-options/services/node-context-processing.service.spec.ts b/projects/lib/portal-options/services/node-context-processing.service.spec.ts index ba6ac35..328524e 100644 --- a/projects/lib/portal-options/services/node-context-processing.service.spec.ts +++ b/projects/lib/portal-options/services/node-context-processing.service.spec.ts @@ -1,10 +1,11 @@ +import { TestBed } from '@angular/core/testing'; +import { Resource } from '@openmfp/portal-ui-lib'; +import { ResourceService } from '@platform-mesh/portal-ui-lib/services'; +import { of, throwError } from 'rxjs'; import { PortalNodeContext } from '../models/luigi-context'; import { PortalLuigiNode } from '../models/luigi-node'; import { CrdGatewayKcpPatchResolver } from './crd-gateway-kcp-patch-resolver.service'; import { NodeContextProcessingServiceImpl } from './node-context-processing.service'; -import { TestBed } from '@angular/core/testing'; -import { Resource, ResourceService } from '@openmfp/portal-ui-lib'; -import { of, throwError } from 'rxjs'; describe('NodeContextProcessingServiceImpl', () => { let service: NodeContextProcessingServiceImpl; diff --git a/projects/lib/portal-options/services/node-context-processing.service.ts b/projects/lib/portal-options/services/node-context-processing.service.ts index 213e167..58b11a7 100644 --- a/projects/lib/portal-options/services/node-context-processing.service.ts +++ b/projects/lib/portal-options/services/node-context-processing.service.ts @@ -1,13 +1,13 @@ -import { PortalNodeContext } from '../models/luigi-context'; -import { PortalLuigiNode } from '../models/luigi-node'; -import { CrdGatewayKcpPatchResolver } from './crd-gateway-kcp-patch-resolver.service'; import { Injectable, inject } from '@angular/core'; import { NodeContextProcessingService, - ResourceService, - replaceDotsAndHyphensWithUnderscores, } from '@openmfp/portal-ui-lib'; +import { ResourceService } from '@platform-mesh/portal-ui-lib/services'; +import { replaceDotsAndHyphensWithUnderscores } from '@platform-mesh/portal-ui-lib/utils'; import { firstValueFrom } from 'rxjs'; +import { PortalNodeContext } from '../models/luigi-context'; +import { PortalLuigiNode } from '../models/luigi-node'; +import { CrdGatewayKcpPatchResolver } from './crd-gateway-kcp-patch-resolver.service'; @Injectable({ providedIn: 'root', diff --git a/projects/lib/services/ng-package.json b/projects/lib/services/ng-package.json new file mode 100644 index 0000000..37e141d --- /dev/null +++ b/projects/lib/services/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public-api.ts" + } +} diff --git a/projects/lib/services/public-api.ts b/projects/lib/services/public-api.ts new file mode 100644 index 0000000..c1cb5de --- /dev/null +++ b/projects/lib/services/public-api.ts @@ -0,0 +1 @@ +export * from './resource'; diff --git a/projects/lib/services/resource/apollo-factory.spec.ts b/projects/lib/services/resource/apollo-factory.spec.ts new file mode 100644 index 0000000..43096bc --- /dev/null +++ b/projects/lib/services/resource/apollo-factory.spec.ts @@ -0,0 +1,177 @@ +import { NgZone } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { ApolloLink, InMemoryCache, execute } from '@apollo/client/core'; +import { parse } from 'graphql'; +import { LuigiCoreService } from '@openmfp/portal-ui-lib'; +import { Apollo } from 'apollo-angular'; +import { HttpLink } from 'apollo-angular/http'; +import { createClient } from 'graphql-sse'; +import { mock } from 'jest-mock-extended'; +import { ApolloFactory } from './apollo-factory'; +import { GatewayService } from './gateway.service'; +import { ResourceNodeContext } from './resource-node-context'; + +// Mock graphql-sse client to capture provided options +jest.mock('graphql-sse', () => ({ + createClient: jest.fn(), +})); + +global.fetch = (...args) => + // @ts-ignore + import('node-fetch').then(({ default: fetch }) => fetch(...args)); + +describe('ApolloFactory', () => { + let factory: ApolloFactory; + let luigiCoreServiceMock: any; + let httpLinkMock: any; + let gatewayServiceMock: jest.Mocked; + let ngZone: NgZone; + + beforeEach(() => { + httpLinkMock = { + create: jest.fn().mockReturnValue({ request: [] }), + }; + luigiCoreServiceMock = { + getWcExtendedContext: jest.fn().mockReturnValue({ + portalContext: { crdGatewayApiUrl: 'http://example.com/graphql' }, + accountId: '123', + }), + getGlobalContext: jest.fn().mockReturnValue({ token: 'fake-token' }), + }; + gatewayServiceMock = mock(); + TestBed.configureTestingModule({ + providers: [ + ApolloFactory, + { provide: HttpLink, useValue: httpLinkMock }, + { + provide: NgZone, + useValue: new NgZone({ enableLongStackTrace: false }), + }, + { provide: LuigiCoreService, useValue: luigiCoreServiceMock }, + { provide: GatewayService, useValue: gatewayServiceMock }, + ], + }); + factory = TestBed.inject(ApolloFactory); + ngZone = TestBed.inject(NgZone); + }); + + it('should create an Apollo instance', () => { + expect(factory.apollo({} as ResourceNodeContext)).toBeInstanceOf(Apollo); + }); + + it('should create Apollo options with InMemoryCache', () => { + const options = (factory as any).createApolloOptions(); + expect(options.cache).toBeInstanceOf(InMemoryCache); + }); + + it('should create HttpLink with default options', () => { + (factory as any).createApolloOptions({ token: 't' } as unknown as ResourceNodeContext); + expect(httpLinkMock.create).toHaveBeenCalledWith({}); + }); + + it('should configure SSE client with dynamic url and auth header', () => { + // reset call count since previous tests may initialize SSE link too + (createClient as jest.Mock).mockClear(); + const subscribeMock = jest.fn().mockReturnValue(() => void 0); + (createClient as jest.Mock).mockReturnValue({ subscribe: subscribeMock }); + + const nodeContext: ResourceNodeContext = { + token: 'fake-token', + } as unknown as ResourceNodeContext; + + gatewayServiceMock.getGatewayUrl.mockReturnValue('http://example.com/graphql'); + + (factory as any).createApolloOptions(nodeContext, false); + + expect(createClient).toHaveBeenCalledTimes(1); + const clientOptions = (createClient as jest.Mock).mock.calls[0][0]; + + expect(typeof clientOptions.url).toBe('function'); + expect(typeof clientOptions.headers).toBe('function'); + + // url() should call GatewayService.getGatewayUrl lazily + expect(gatewayServiceMock.getGatewayUrl).not.toHaveBeenCalled(); + const resolvedUrl = clientOptions.url(); + expect(gatewayServiceMock.getGatewayUrl).toHaveBeenCalledWith(nodeContext, false); + expect(resolvedUrl).toBe('http://example.com/graphql'); + + // headers() should include bearer token + const headers = clientOptions.headers(); + expect(headers).toEqual({ Authorization: 'Bearer fake-token' }); + }); + + it('should pass readFromParentKcpPath flag to SSE url resolver', () => { + (createClient as jest.Mock).mockClear(); + const subscribeMock = jest.fn().mockReturnValue(() => void 0); + (createClient as jest.Mock).mockReturnValue({ subscribe: subscribeMock }); + + const nodeContext: ResourceNodeContext = { token: 't' } as unknown as ResourceNodeContext; + gatewayServiceMock.getGatewayUrl.mockReturnValue('http://gw/graphql'); + + (factory as any).createApolloOptions(nodeContext, true); + + const clientOptions = (createClient as jest.Mock).mock.calls.at(-1)[0]; + clientOptions.url(); + expect(gatewayServiceMock.getGatewayUrl).toHaveBeenCalledWith(nodeContext, true); + }); + + it('should pass readFromParentKcpPath from apollo() to options builder', () => { + const nodeContext = { token: 'x' } as unknown as ResourceNodeContext; + const spy = jest.spyOn(factory as any, 'createApolloOptions'); + factory.apollo(nodeContext, true); + expect(spy).toHaveBeenCalledWith(nodeContext, true); + }); + + it('should create a new Apollo instance per call', () => { + const ctx = { token: 'a' } as unknown as ResourceNodeContext; + const a1 = factory.apollo(ctx); + const a2 = factory.apollo(ctx); + expect(a1).not.toBe(a2); + }); + + it('should compose a valid ApolloLink chain', () => { + const options = (factory as any).createApolloOptions({ token: 't' } as unknown as ResourceNodeContext); + expect(options.link).toBeInstanceOf(ApolloLink); + expect(typeof (options.link as ApolloLink).request).toBe('function'); + }); + + it('should not eagerly resolve gateway URL during options creation', () => { + gatewayServiceMock.getGatewayUrl.mockClear(); + (factory as any).createApolloOptions({ token: 't' } as unknown as ResourceNodeContext); + expect(gatewayServiceMock.getGatewayUrl).not.toHaveBeenCalled(); + }); + + it('routes query operations without errors', () => { + const httpReturnLink = new ApolloLink(() => ({ subscribe: jest.fn() } as any)); + httpLinkMock.create.mockReturnValue(httpReturnLink as any); + + const nodeContext = { + token: 't', + portalContext: { crdGatewayApiUrl: 'http://x/:kcp/graphql' }, + } as unknown as ResourceNodeContext; + + const options = (factory as any).createApolloOptions(nodeContext, false); + const queryDoc = parse('query Q { x }'); + const obs = execute(options.link, { query: queryDoc } as any) as any; + expect(obs).toBeTruthy(); + expect(typeof obs.subscribe).toBe('function'); + expect(() => obs.subscribe({})).not.toThrow(); + }); + + it('routes subscription operations without errors', () => { + (createClient as jest.Mock).mockClear(); + (createClient as jest.Mock).mockReturnValue({ subscribe: jest.fn().mockReturnValue(() => void 0) }); + + const nodeContext = { + token: 't', + portalContext: { crdGatewayApiUrl: 'http://x/:kcp/graphql' }, + } as unknown as ResourceNodeContext; + + const options = (factory as any).createApolloOptions(nodeContext, false); + const subDoc = parse('subscription S { x }'); + const obs = execute(options.link, { query: subDoc } as any) as any; + expect(obs).toBeTruthy(); + expect(typeof obs.subscribe).toBe('function'); + expect(() => obs.subscribe({})).not.toThrow(); + }); +}); diff --git a/projects/lib/services/resource/apollo-factory.ts b/projects/lib/services/resource/apollo-factory.ts new file mode 100644 index 0000000..154631e --- /dev/null +++ b/projects/lib/services/resource/apollo-factory.ts @@ -0,0 +1,100 @@ +import { GatewayService } from './gateway.service'; +import { ResourceNodeContext } from './resource-node-context'; +import { Injectable, NgZone, inject } from '@angular/core'; +import { + type ApolloClientOptions, + ApolloLink, + Observable as ApolloObservable, + FetchResult, + InMemoryCache, + Operation, + split, +} from '@apollo/client/core'; +import { setContext } from '@apollo/client/link/context'; +import { getMainDefinition } from '@apollo/client/utilities'; +import { Apollo } from 'apollo-angular'; +import { HttpLink } from 'apollo-angular/http'; +import { print } from 'graphql'; +import { Client, ClientOptions, createClient } from 'graphql-sse'; + +class SSELink extends ApolloLink { + private client: Client; + + constructor(options: ClientOptions) { + super(); + this.client = createClient(options); + } + + public override request(operation: Operation): ApolloObservable { + return new ApolloObservable((sink) => { + return this.client.subscribe( + { ...operation, query: print(operation.query) }, + { + next: sink.next.bind(sink), + complete: sink.complete.bind(sink), + error: sink.error.bind(sink), + }, + ); + }); + } +} + +@Injectable({ + providedIn: 'root', +}) +export class ApolloFactory { + private httpLink = inject(HttpLink); + private ngZone = inject(NgZone); + private gatewayService = inject(GatewayService); + + public readonly apollo = ( + nodeContext: ResourceNodeContext, + readFromParentKcpPath = false, + ): Apollo => + new Apollo( + this.ngZone, + this.createApolloOptions(nodeContext, readFromParentKcpPath), + ); + + private createApolloOptions( + nodeContext: ResourceNodeContext, + readFromParentKcpPath = false, + ): ApolloClientOptions { + const contextLink = setContext(() => { + return { + uri: () => + this.gatewayService.getGatewayUrl(nodeContext, readFromParentKcpPath), + headers: { + Authorization: `Bearer ${nodeContext.token}`, + Accept: 'charset=utf-8', + }, + }; + }); + + const splitClient = split( + ({ query }) => { + const definition = getMainDefinition(query); + return ( + definition.kind === 'OperationDefinition' && + definition.operation === 'subscription' + ); + }, + new SSELink({ + url: () => + this.gatewayService.getGatewayUrl(nodeContext, readFromParentKcpPath), + headers: () => ({ + Authorization: `Bearer ${nodeContext.token}`, + }), + }), + this.httpLink.create({}), + ); + + const link = ApolloLink.from([contextLink, splitClient]); + const cache = new InMemoryCache(); + + return { + link, + cache, + }; + } +} diff --git a/projects/lib/services/resource/gateway.service.spec.ts b/projects/lib/services/resource/gateway.service.spec.ts new file mode 100644 index 0000000..2f27ee9 --- /dev/null +++ b/projects/lib/services/resource/gateway.service.spec.ts @@ -0,0 +1,104 @@ +import { TestBed } from '@angular/core/testing'; +import { LuigiCoreService } from '@openmfp/portal-ui-lib'; +import { GatewayService } from './gateway.service'; + +describe('GatewayService', () => { + let service: GatewayService; + let mockLuigiCoreService: any; + + beforeEach(() => { + mockLuigiCoreService = { + getGlobalContext: jest.fn().mockReturnValue({ + portalContext: { + crdGatewayApiUrl: 'https://example.com/:org1:acc1/graphql', + }, + }), + }; + + TestBed.configureTestingModule({ + providers: [ + GatewayService, + { provide: LuigiCoreService, useValue: mockLuigiCoreService }, + ], + }); + + service = TestBed.inject(GatewayService); + }); + + describe('getGatewayUrl', () => { + it('should replace current kcp path with new one', () => { + const nodeContext = { + portalContext: { + crdGatewayApiUrl: 'https://example.com/:org1:acc1/graphql', + }, + token: 'token', + accountId: 'entityId', + kcpPath: ':org1:acc2', + }; + const result = service.getGatewayUrl(nodeContext); + expect(result).toBe('https://example.com/:org1:acc2/graphql'); + }); + + it('should slice current kcp path when readFromParentKcpPath is true', () => { + const nodeContext = { + portalContext: { + crdGatewayApiUrl: 'https://example.com/:org1:acc1/graphql', + }, + token: 'token', + accountId: 'acc1', + }; + const result = service.getGatewayUrl(nodeContext as any, true); + expect(result).toBe('https://example.com/:org1/graphql'); + }); + }); + + describe('updateCrdGatewayUrlWithEntityPath', () => { + it('should update crdGatewayApiUrl with new kcp path', () => { + const globalContext = mockLuigiCoreService.getGlobalContext(); + service.updateCrdGatewayUrlWithEntityPath(':org1:acc3'); + expect(globalContext.portalContext.crdGatewayApiUrl).toBe( + 'https://example.com/:org1:acc3/graphql', + ); + }); + }); + + describe('resolveKcpPath', () => { + it('should return kcpPath from context if present', () => { + const nodeContext = { + portalContext: { + crdGatewayApiUrl: 'https://example.com/:org1:acc1/graphql', + }, + token: 'token', + accountId: 'entityId', + kcpPath: ':org1:acc2', + }; + const result = service.resolveKcpPath(nodeContext); + expect(result).toBe(':org1:acc2'); + }); + + it('should slice path by accountId if readFromParentKcpPath is true', () => { + const nodeContext = { + portalContext: { + crdGatewayApiUrl: 'https://example.com/:org1:acc1/graphql', + }, + token: 'token', + accountId: 'entityId', + kcpPath: ':org1', + }; + const result = service.resolveKcpPath(nodeContext, true); + expect(result).toBe(':org1'); + }); + + it('should return current kcp path if no override provided', () => { + const nodeContext = { + portalContext: { + crdGatewayApiUrl: 'https://example.com/:org1:acc1/graphql', + }, + token: 'token', + accountId: 'entityId', + }; + const result = service.resolveKcpPath(nodeContext); + expect(result).toBe(':org1:acc1'); + }); + }); +}); diff --git a/projects/lib/services/resource/gateway.service.ts b/projects/lib/services/resource/gateway.service.ts new file mode 100644 index 0000000..d067717 --- /dev/null +++ b/projects/lib/services/resource/gateway.service.ts @@ -0,0 +1,49 @@ +import { Injectable, inject } from '@angular/core'; +import { LuigiCoreService } from '@openmfp/portal-ui-lib'; +import { ResourceNodeContext } from './resource-node-context'; + +@Injectable({ providedIn: 'root' }) +export class GatewayService { + private luigiCoreService = inject(LuigiCoreService); + + public getGatewayUrl( + nodeContext: ResourceNodeContext, + readFromParentKcpPath = false, + ) { + const gatewayUrl = nodeContext.portalContext.crdGatewayApiUrl; + const kcpPathRegexp = /\/([^\/]+)\/graphql$/; + const currentKcpPath = gatewayUrl?.match(kcpPathRegexp)[1]; + return gatewayUrl?.replace( + currentKcpPath, + this.resolveKcpPath(nodeContext, readFromParentKcpPath), + ); + } + + public updateCrdGatewayUrlWithEntityPath(kcpPath: string) { + const gatewayUrl = + this.luigiCoreService.getGlobalContext().portalContext.crdGatewayApiUrl; + const kcpPathRegexp = /(.*\/)[^/]+(?=\/graphql$)/; + this.luigiCoreService.getGlobalContext().portalContext.crdGatewayApiUrl = + gatewayUrl.replace(kcpPathRegexp, `$1${kcpPath}`); + } + + public resolveKcpPath( + nodeContext: ResourceNodeContext, + readFromParentKcpPath = false, + ) { + const gatewayUrl = nodeContext.portalContext.crdGatewayApiUrl; + const currentKcpPath = gatewayUrl?.match(/\/([^\/]+)\/graphql$/)[1]; + + let kcpPath = currentKcpPath; + if (nodeContext.kcpPath) { + kcpPath = nodeContext.kcpPath; + } else if (readFromParentKcpPath) { + const lastIndex = currentKcpPath.lastIndexOf(':'); + if (lastIndex !== -1) { + kcpPath = currentKcpPath.slice(0, lastIndex); + } + } + + return kcpPath; + } +} diff --git a/projects/lib/services/resource/index.ts b/projects/lib/services/resource/index.ts new file mode 100644 index 0000000..d59ff52 --- /dev/null +++ b/projects/lib/services/resource/index.ts @@ -0,0 +1,4 @@ +export * from './apollo-factory'; +export * from './gateway.service'; +export * from './resource-node-context'; +export * from './resource.service'; diff --git a/projects/lib/services/resource/resource-node-context.ts b/projects/lib/services/resource/resource-node-context.ts new file mode 100644 index 0000000..7a13715 --- /dev/null +++ b/projects/lib/services/resource/resource-node-context.ts @@ -0,0 +1,16 @@ +import { NodeContext } from '@openmfp/portal-ui-lib'; + +export interface ResourceNodeContext extends Partial { + accountId?: string; + kcpCA?: string; + entity?: { + metadata: { + name: string; + namespace?: string; + }; + }; + namespaceId?: string; + portalContext: { + crdGatewayApiUrl: string; + }; +} diff --git a/projects/lib/services/resource/resource.service.spec.ts b/projects/lib/services/resource/resource.service.spec.ts new file mode 100644 index 0000000..8eec412 --- /dev/null +++ b/projects/lib/services/resource/resource.service.spec.ts @@ -0,0 +1,610 @@ +import { TestBed } from '@angular/core/testing'; +import { LuigiCoreService } from '@openmfp/portal-ui-lib'; +import { mock } from 'jest-mock-extended'; +import { of, throwError } from 'rxjs'; +import { ApolloFactory } from './apollo-factory'; +import { ResourceService } from './resource.service'; + +describe('ResourceService', () => { + let service: ResourceService; + let mockApollo: any; + let mockApolloFactory: any; + let mockLuigiCoreService: jest.Mocked; + + const resourceDefinition: any = { + group: 'core.k8s.io', + kind: 'TestKind', + scope: 'Namespaced', + namespace: 'default', + }; + + const namespacedNodeContext: any = { + cluster: 'test', + namespaceId: 'test-namespace', + resourceDefinition: { + group: 'core.k8s.io', + kind: 'TestKind', + scope: 'Namespaced', + namespace: 'default', + }, + }; + + const clusterScopeNodeContext: any = { + namespaceId: 'test-namespace', + resourceDefinition: { + group: 'core.k8s.io', + kind: 'TestKind', + scope: 'Cluster', + namespace: 'default', + }, + }; + + const resource: any = { metadata: { name: 'test-name' } }; + + beforeEach(() => { + mockLuigiCoreService = mock(); + mockApollo = { + query: jest.fn(), + subscribe: jest.fn(), + mutate: jest.fn(), + }; + + mockApolloFactory = { + apollo: jest.fn().mockReturnValue(mockApollo), + }; + + TestBed.configureTestingModule({ + providers: [ + ResourceService, + { provide: ApolloFactory, useValue: mockApolloFactory }, + { provide: LuigiCoreService, useValue: mockLuigiCoreService }, + ], + }); + + service = TestBed.inject(ResourceService); + }); + + describe('read', () => { + it('should catch gql parsing error and return null observable', (done) => { + const invalidQuery = + `query { core_k8s_io { TestKind(name: "test-name") {` as unknown as any; + + const mockShowAlert = jest.fn(); + service['luigiCoreService'].showAlert = mockShowAlert; + + service + .read( + 'test-name', + 'core_k8s_io', + 'TestKind', + invalidQuery, + namespacedNodeContext, + ) + .subscribe((res) => { + expect(res).toBeNull(); + expect(mockLuigiCoreService.showAlert).toHaveBeenCalledWith({ + text: expect.any(String), + type: 'error', + }); + done(); + }); + }); + + it('should read resource using fields', (done) => { + mockApollo.query.mockReturnValue( + of({ data: { core_k8s_io: { TestKind: { name: 'test' } } } }), + ); + + service + .read( + 'test-name', + 'core_k8s_io', + 'TestKind', + ['name'], + namespacedNodeContext, + ) + .subscribe((res) => { + expect(res).toEqual({ name: 'test' }); + expect(mockApollo.query).toHaveBeenCalledWith({ + query: expect.anything(), + variables: { + name: 'test-name', + namespace: 'test-namespace', + }, + }); + done(); + }); + }); + + it('should read resource using fields with namespaced scope', (done) => { + mockApollo.query.mockReturnValue( + of({ data: { core_k8s_io: { TestKind: { name: 'test' } } } }), + ); + + service + .read( + 'test-name', + 'core_k8s_io', + 'TestKind', + ['name'], + namespacedNodeContext, + ) + .subscribe((res) => { + expect(res).toEqual({ name: 'test' }); + expect(mockApollo.query).toHaveBeenCalledWith({ + query: expect.anything(), + variables: { + name: 'test-name', + namespace: namespacedNodeContext.namespaceId, + }, + }); + done(); + }); + }); + + it('should read resource using fields with cluster scope', (done) => { + mockApollo.query.mockReturnValue( + of({ data: { core_k8s_io: { TestKind: { name: 'test' } } } }), + ); + + service + .read( + 'test-name', + 'core_k8s_io', + 'TestKind', + ['name'], + clusterScopeNodeContext, + ) + .subscribe((res) => { + expect(res).toEqual({ name: 'test' }); + expect(mockApollo.query).toHaveBeenCalledWith({ + query: expect.anything(), + variables: { + name: 'test-name', + }, + }); + done(); + }); + }); + + it('should read resource using raw query, namespaced scope', (done) => { + const rawQuery = `query { core_k8s_io { TestKind(name: "test-name") { name } } }`; + mockApollo.query.mockReturnValue( + of({ data: { core_k8s_io: { TestKind: { name: 'test' } } } }), + ); + + service + .read( + 'test-name', + 'core_k8s_io', + 'TestKind', + rawQuery, + namespacedNodeContext, + ) + .subscribe((res) => { + expect(res).toEqual({ name: 'test' }); + expect(mockApollo.query).toHaveBeenCalledWith({ + query: expect.anything(), + variables: { + name: 'test-name', + namespace: 'test-namespace', + }, + }); + done(); + }); + }); + + it('should read resource using raw query, cluster scope', (done) => { + const rawQuery = `query { core_k8s_io { TestKind(name: "test") { name } } }`; + mockApollo.query.mockReturnValue( + of({ data: { core_k8s_io: { TestKind: { name: 'test' } } } }), + ); + + service + .read( + 'test', + 'core_k8s_io', + 'TestKind', + rawQuery, + clusterScopeNodeContext, + ) + .subscribe((res) => { + expect(res).toEqual({ name: 'test' }); + expect(mockApollo.query).toHaveBeenCalledWith({ + query: expect.anything(), + variables: { + name: 'test', + }, + }); + done(); + }); + }); + + it('should read resource using raw query with namespace', (done) => { + const rawQuery = `query { core_k8s_io { TestKind(name: "test-name", namespace: "test-namespace") { name } } }`; + mockApollo.query.mockReturnValue( + of({ data: { core_k8s_io: { TestKind: { name: 'test' } } } }), + ); + + service + .read( + 'test-name', + 'core_k8s_io', + 'TestKind', + rawQuery, + namespacedNodeContext, + ) + .subscribe((res) => { + expect(res).toEqual({ name: 'test' }); + expect(mockApollo.query).toHaveBeenCalledWith({ + query: expect.anything(), + variables: { + name: 'test-name', + namespace: namespacedNodeContext.namespaceId, + }, + }); + done(); + }); + }); + + it('should handle read error', (done) => { + const error = new Error('fail'); + mockApollo.query.mockReturnValue(throwError(() => error)); + console.error = jest.fn(); + + service + .read( + 'test-name', + 'core_k8s_io', + 'TestKind', + ['name'], + namespacedNodeContext, + ) + .subscribe({ + error: (err) => { + expect(console.error).toHaveBeenCalledWith( + 'Error executing GraphQL query.', + error, + ); + done(); + }, + }); + }); + }); + + describe('list', () => { + it('should list namespaced resources', (done) => { + mockApollo.subscribe.mockReturnValue( + of({ data: { myList: [{ name: 'res1' }] } }), + ); + service + .list('myList', ['name'], namespacedNodeContext) + .subscribe((res) => { + expect(res).toEqual([{ name: 'res1' }]); + expect(mockApollo.subscribe).toHaveBeenCalledWith({ + query: expect.anything(), + variables: { namespace: namespacedNodeContext.namespaceId }, + }); + done(); + }); + }); + + it('should list cluster resources', (done) => { + mockApollo.subscribe.mockReturnValue( + of({ data: { myList: [{ name: 'res1' }] } }), + ); + service + .list('myList', ['name'], clusterScopeNodeContext) + .subscribe((res) => { + expect(res).toEqual([{ name: 'res1' }]); + expect(mockApollo.subscribe).toHaveBeenCalledWith({ + query: expect.anything(), + variables: {}, + }); + done(); + }); + }); + + it('should list resources with namespace', (done) => { + mockApollo.subscribe.mockReturnValue( + of({ data: { myList: [{ name: 'res1' }] } }), + ); + + service + .list('myList', ['name'], namespacedNodeContext) + .subscribe((res) => { + expect(res).toEqual([{ name: 'res1' }]); + expect(mockApollo.subscribe).toHaveBeenCalledWith({ + query: expect.anything(), + variables: { namespace: namespacedNodeContext.namespaceId }, + }); + done(); + }); + }); + + it('should list namespaced resources (raw query string)', (done) => { + const rawQuery = ` + subscription { + myList { + name + } + } + `; + mockApollo.subscribe.mockReturnValue( + of({ data: { myList: { myData: [{ name: 'res2' }] } } }), + ); + + service + .list('myList.myData', rawQuery, namespacedNodeContext) + .subscribe((res) => { + expect(res).toEqual([{ name: 'res2' }]); + expect(mockApollo.subscribe).toHaveBeenCalledWith({ + query: expect.anything(), + variables: { + namespace: { + type: 'String', + value: namespacedNodeContext.namespaceId, + }, + }, + }); + done(); + }); + }); + + it('should list cluster resources (raw query string)', (done) => { + const rawQuery = ` + subscription { + myList { + name + } + } + `; + mockApollo.subscribe.mockReturnValue( + of({ data: { myList: [{ name: 'res2' }] } }), + ); + + service + .list('myList', rawQuery, clusterScopeNodeContext) + .subscribe((res) => { + expect(res).toEqual([{ name: 'res2' }]); + expect(mockApollo.subscribe).toHaveBeenCalledWith({ + query: expect.anything(), + variables: {}, + }); + done(); + }); + }); + + it('should handle list error', (done) => { + const error = new Error('fail'); + mockApollo.subscribe.mockReturnValue(throwError(() => error)); + console.error = jest.fn(); + + service.list('myList', ['name'], namespacedNodeContext).subscribe({ + error: (err) => { + expect(console.error).toHaveBeenCalledWith( + 'Error executing GraphQL query.', + error, + ); + done(); + }, + }); + }); + }); + + describe('readOrganizations', () => { + it('should read organizations', (done) => { + mockApollo.query.mockReturnValue(of({ data: { orgList: [{ id: 1 }] } })); + service + .readOrganizations('orgList', ['id'], namespacedNodeContext) + .subscribe((res) => { + expect(res).toEqual([{ id: 1 }]); + done(); + }); + }); + + it('should handle read organizations error', (done) => { + const error = new Error('fail'); + mockApollo.query.mockReturnValue(throwError(() => error)); + console.error = jest.fn(); + + service + .readOrganizations('orgList', ['id'], namespacedNodeContext) + .subscribe({ + error: (err) => { + expect(console.error).toHaveBeenCalledWith( + 'Error executing GraphQL query.', + error, + ); + done(); + }, + }); + }); + }); + + describe('delete', () => { + it('should delete resource', (done) => { + mockApollo.mutate.mockReturnValue(of({})); + service + .delete(resource, resourceDefinition, namespacedNodeContext) + .subscribe((res) => { + expect(mockApollo.mutate).toHaveBeenCalled(); + done(); + }); + }); + + it('should delete namespaced resource', (done) => { + mockApollo.mutate.mockReturnValue(of({})); + + service + .delete(resource, resourceDefinition, namespacedNodeContext) + .subscribe(() => { + expect(mockApollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + name: 'test-name', + namespace: namespacedNodeContext.namespaceId, + }, + }); + done(); + }); + }); + + it('should delete cluster resource', (done) => { + mockApollo.mutate.mockReturnValue(of({})); + + service + .delete(resource, resourceDefinition, clusterScopeNodeContext) + .subscribe(() => { + expect(mockApollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + name: 'test-name', + }, + }); + done(); + }); + }); + + it('should handle delete error', (done) => { + const error = new Error('fail'); + mockApollo.mutate.mockReturnValue(throwError(() => error)); + console.error = jest.fn(); + + service + .delete(resource, resourceDefinition, clusterScopeNodeContext) + .subscribe({ + error: () => { + expect(console.error).toHaveBeenCalledWith( + 'Error executing GraphQL query.', + error, + ); + expect(mockLuigiCoreService.showAlert).toHaveBeenCalledWith({ + text: 'fail', + type: 'error', + }); + done(); + }, + }); + }); + }); + + describe('create', () => { + it('should create resource', (done) => { + mockApollo.mutate.mockReturnValue( + of({ data: { __typename: 'TestKind' } }), + ); + service + .create(resource, resourceDefinition, namespacedNodeContext) + .subscribe((res) => { + expect(mockApollo.mutate).toHaveBeenCalled(); + done(); + }); + }); + + it('should create namespaced resource ', (done) => { + mockApollo.mutate.mockReturnValue( + of({ data: { __typename: 'TestKind' } }), + ); + + service + .create(resource, resourceDefinition, namespacedNodeContext) + .subscribe(() => { + expect(mockApollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + fetchPolicy: 'no-cache', + variables: { + object: resource, + namespace: namespacedNodeContext.namespaceId, + }, + }); + done(); + }); + }); + + it('should create cluster resource ', (done) => { + mockApollo.mutate.mockReturnValue( + of({ data: { __typename: 'TestKind' } }), + ); + + service + .create(resource, resourceDefinition, clusterScopeNodeContext) + .subscribe(() => { + expect(mockApollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + fetchPolicy: 'no-cache', + variables: { + object: resource, + }, + }); + done(); + }); + }); + + it('should handle create error', (done) => { + const error = new Error('fail'); + mockApollo.mutate.mockReturnValue(throwError(() => error)); + console.error = jest.fn(); + + service + .create(resource, resourceDefinition, clusterScopeNodeContext) + .subscribe({ + error: () => { + expect(console.error).toHaveBeenCalledWith( + 'Error executing GraphQL query.', + error, + ); + expect(mockLuigiCoreService.showAlert).toHaveBeenCalledWith({ + text: 'fail', + type: 'error', + }); + done(); + }, + }); + }); + }); + + describe('readAccountInfo', () => { + it('should read account info', (done) => { + const ca = 'cert-data'; + const accountInfo = { spec: { clusterInfo: { ca } } }; + mockApollo.query.mockReturnValue( + of({ + data: { + core_platform_mesh_io: { + AccountInfo: accountInfo, + }, + }, + }), + ); + + service.readAccountInfo(namespacedNodeContext).subscribe((res) => { + expect(res).toBe(accountInfo); + expect(mockApolloFactory.apollo).toHaveBeenCalledWith( + namespacedNodeContext, + ); + done(); + }); + }); + + it('should handle read account info error', (done) => { + const error = new Error('fail'); + mockApollo.query.mockReturnValue(throwError(() => error)); + console.error = jest.fn(); + + service.readAccountInfo(namespacedNodeContext).subscribe({ + error: () => { + expect(console.error).toHaveBeenCalledWith( + 'Error executing GraphQL query.', + error, + ); + expect(mockLuigiCoreService.showAlert).toHaveBeenCalledWith({ + text: 'fail', + type: 'error', + }); + done(); + }, + }); + }); + }); +}); diff --git a/projects/lib/services/resource/resource.service.ts b/projects/lib/services/resource/resource.service.ts new file mode 100644 index 0000000..eb2b82c --- /dev/null +++ b/projects/lib/services/resource/resource.service.ts @@ -0,0 +1,320 @@ +import { Injectable, inject } from '@angular/core'; +import { TypedDocumentNode } from '@apollo/client/core'; +import { + AccountInfo, LuigiCoreService, Resource, + ResourceDefinition, +} from '@openmfp/portal-ui-lib'; +import { getValueByPath, replaceDotsAndHyphensWithUnderscores } from '@platform-mesh/portal-ui-lib/utils'; +import { gql } from 'apollo-angular'; +import * as gqlBuilder from 'gql-query-builder'; +import { Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { ApolloFactory } from './apollo-factory'; +import { ResourceNodeContext } from './resource-node-context'; + +interface ResourceResponseError extends Record { + message: string; +} + +interface ResourceResponse extends Record { + data: { + [key: string]: any; + }; + errors: { message: string }[]; +} + +@Injectable({ + providedIn: 'root', +}) +export class ResourceService { + private apolloFactory = inject(ApolloFactory); + private luigiCoreService = inject(LuigiCoreService); + + read( + resourceId: string, + operation: string, + kind: string, + fieldsOrRawQuery: any[] | string, + nodeContext: ResourceNodeContext, + readFromParentKcpPath: boolean = true, + ): Observable { + const isNamespacedResource = this.isNamespacedResource(nodeContext); + let query: string | TypedDocumentNode = this.resolveReadQuery( + fieldsOrRawQuery, + kind, + resourceId, + isNamespacedResource ? nodeContext.namespaceId : null, + operation, + ); + + try { + query = gql` + ${query} + `; + } catch (error) { + this.luigiCoreService.showAlert({ + text: `Could not read a resource: ${resourceId}. Wrong read query:

${query}`, + type: 'error', + }); + return of(null); + } + + return this.apolloFactory + .apollo(nodeContext, readFromParentKcpPath) + .query({ + query, + variables: { + name: resourceId, + ...(isNamespacedResource && { + namespace: nodeContext.namespaceId, + }), + }, + }) + .pipe( + map((res) => res.data?.[operation]?.[kind]), + catchError((error) => { + this.alertErrors(error); + console.error('Error executing GraphQL query.', error); + return error; + }), + ); + } + + private resolveReadQuery( + fieldsOrRawQuery: any[] | string, + kind: string, + resourceId: string, + namespace: string, + operation: string, + ) { + if (fieldsOrRawQuery instanceof Array) { + return ( + gqlBuilder + .query({ + operation: kind, + variables: { + name: { value: resourceId, type: 'String!' }, + ...(namespace && { + namespace: { value: namespace, type: 'String' }, + }), + }, + fields: fieldsOrRawQuery, + }) + .query.replace(kind, `${operation} { ${kind}`) + .trim() + '}' + ); + } else { + return fieldsOrRawQuery; + } + } + + list( + operation: string, + fieldsOrRawQuery: any[] | string, + nodeContext: ResourceNodeContext, + ): Observable { + const isNamespacedResource = this.isNamespacedResource(nodeContext); + const variables = { + ...(isNamespacedResource && { + namespace: { type: 'String', value: nodeContext.namespaceId }, + }), + }; + + let query: { variables: any; query: string }; + + if (fieldsOrRawQuery instanceof Array) { + query = gqlBuilder.subscription({ + operation, + fields: fieldsOrRawQuery, + variables: variables, + }); + } else { + query = { + variables: variables, + query: fieldsOrRawQuery, + }; + } + + return this.apolloFactory + .apollo(nodeContext) + .subscribe({ + query: gql` + ${query.query} + `, + variables: query.variables, + }) + .pipe( + map((res: any): Resource[] => + getValueByPath(res.data, operation), + ), + catchError((error) => { + this.alertErrors(error); + console.error('Error executing GraphQL query.', error); + return error; + }), + ); + } + + private alertErrors(res: ResourceResponseError) { + this.luigiCoreService.showAlert({ + text: res.message, + type: 'error', + }); + } + + readOrganizations( + operation: string, + fields: any[], + nodeContext: ResourceNodeContext, + ): Observable { + const query = gqlBuilder.query({ + operation: operation, + fields, + variables: {}, + }); + + return this.apolloFactory + .apollo(nodeContext) + .query({ + query: gql` + ${query.query} + `, + }) + .pipe( + map((res: any) => res.data?.[operation]), + catchError((error) => { + this.alertErrors(error); + console.error('Error executing GraphQL query.', error); + return error; + }), + ); + } + + delete( + resource: Resource, + resourceDefinition: ResourceDefinition, + nodeContext: ResourceNodeContext, + ) { + const group = replaceDotsAndHyphensWithUnderscores( + resourceDefinition.group, + ); + const isNamespacedResource = this.isNamespacedResource(nodeContext); + const kind = resourceDefinition.kind; + + const mutation = gqlBuilder.mutation({ + operation: group, + fields: [ + { + operation: `delete${kind}`, + variables: { + name: { type: 'String!', value: resource.metadata.name }, + ...(isNamespacedResource && { + namespace: { type: 'String', value: nodeContext.namespaceId }, + }), + }, + fields: [], + }, + ], + }); + + return this.apolloFactory + .apollo(nodeContext) + .mutate({ + mutation: gql` + ${mutation.query} + `, + variables: mutation.variables, + }) + .pipe( + catchError((error) => { + this.alertErrors(error); + console.error('Error executing GraphQL query.', error); + return error; + }), + ); + } + + create( + resource: Resource, + resourceDefinition: ResourceDefinition, + nodeContext: ResourceNodeContext, + ) { + const isNamespacedResource = this.isNamespacedResource(nodeContext); + const group = replaceDotsAndHyphensWithUnderscores( + resourceDefinition.group, + ); + const kind = resourceDefinition.kind; + const namespace = nodeContext.namespaceId; + + const mutation = gqlBuilder.mutation({ + operation: group, + fields: [ + { + operation: `create${kind}`, + variables: { + ...(isNamespacedResource && { + namespace: { type: 'String', value: namespace }, + }), + object: { type: `${kind}Input!`, value: resource }, + }, + fields: ['__typename'], + }, + ], + }); + + return this.apolloFactory + .apollo(nodeContext) + .mutate({ + mutation: gql` + ${mutation.query} + `, + fetchPolicy: 'no-cache', + variables: mutation.variables, + }) + .pipe( + catchError((error) => { + this.alertErrors(error); + console.error('Error executing GraphQL query.', error); + return error; + }), + ); + } + + readAccountInfo(nodeContext: ResourceNodeContext): Observable { + return this.apolloFactory + .apollo(nodeContext) + .query({ + query: gql` + { + core_platform_mesh_io { + AccountInfo(name: "account") { + metadata { + name + annotations + } + spec { + clusterInfo { + ca + } + } + } + } + } + `, + }) + .pipe( + map((res: any) => { + return res.data.core_platform_mesh_io.AccountInfo; + }), + catchError((error) => { + this.alertErrors(error); + console.error('Error executing GraphQL query.', error); + return error; + }), + ); + } + + private isNamespacedResource(nodeContext: ResourceNodeContext) { + return nodeContext?.resourceDefinition?.scope === 'Namespaced'; + } +} diff --git a/projects/lib/utils/ng-package.json b/projects/lib/utils/ng-package.json new file mode 100644 index 0000000..37e141d --- /dev/null +++ b/projects/lib/utils/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public-api.ts" + } +} diff --git a/projects/lib/utils/public-api.ts b/projects/lib/utils/public-api.ts new file mode 100644 index 0000000..04bca77 --- /dev/null +++ b/projects/lib/utils/public-api.ts @@ -0,0 +1 @@ +export * from './utils'; diff --git a/projects/lib/utils/utils/columns-to-gql-fields.spec.ts b/projects/lib/utils/utils/columns-to-gql-fields.spec.ts new file mode 100644 index 0000000..2cdfcff --- /dev/null +++ b/projects/lib/utils/utils/columns-to-gql-fields.spec.ts @@ -0,0 +1,84 @@ +import { FieldDefinition } from '@openmfp/portal-ui-lib'; +import { generateGraphQLFields } from './columns-to-gql-fields'; + +describe('columns-to-gql-fields', () => { + describe('generateGraphQLFields', () => { + it('should handle empty array input', () => { + const result = generateGraphQLFields([]); + expect(result).toEqual([]); + }); + + it('should handle single field with simple property', () => { + const fields: FieldDefinition[] = [{ property: 'name', label: 'Name' }]; + const result = generateGraphQLFields(fields); + expect(result).toEqual(['name']); + }); + + it('should handle multiple fields with simple properties', () => { + const fields: FieldDefinition[] = [ + { property: 'name', label: 'Name' }, + { property: 'age', label: 'Age' }, + ]; + const result = generateGraphQLFields(fields); + expect(result).toEqual(['name', 'age']); + }); + + it('should handle nested properties with dot notation', () => { + const fields: FieldDefinition[] = [ + { property: 'user.name', label: 'User Name' }, + ]; + const result = generateGraphQLFields(fields); + expect(result).toEqual([{ user: ['name'] }]); + }); + + it('should handle deeply nested properties', () => { + const fields: FieldDefinition[] = [ + { property: 'user.profile.address.city', label: 'City' }, + ]; + const result = generateGraphQLFields(fields); + expect(result).toEqual([ + { user: [{ profile: [{ address: ['city'] }] }] }, + ]); + }); + + it('should handle array of properties', () => { + const fields: FieldDefinition[] = [ + { property: ['name', 'age'], label: 'User Info' }, + ]; + const result = generateGraphQLFields(fields); + expect(result).toEqual(['name', 'age']); + }); + + it('should handle mixed array of simple and nested properties', () => { + const fields: FieldDefinition[] = [ + { property: ['name', 'user.profile.age'], label: 'Mixed Info' }, + ]; + const result = generateGraphQLFields(fields); + expect(result).toEqual(['name', { user: [{ profile: ['age'] }] }]); + }); + + it('should handle multiple fields with mixed properties', () => { + const fields: FieldDefinition[] = [ + { property: 'name', label: 'Name' }, + { property: 'user.profile.age', label: 'Age' }, + { property: ['email', 'phone'], label: 'Contact' }, + ]; + const result = generateGraphQLFields(fields); + expect(result).toEqual([ + 'name', + { user: [{ profile: ['age'] }] }, + 'email', + 'phone', + ]); + }); + + it('should handle empty or null property values', () => { + const fields: FieldDefinition[] = [ + { property: '', label: 'Empty' }, + { property: null as any, label: 'Null' }, + ]; + const result = generateGraphQLFields(fields); + expect(result).toEqual([]); + }); + }); +}); diff --git a/projects/lib/utils/utils/columns-to-gql-fields.ts b/projects/lib/utils/utils/columns-to-gql-fields.ts new file mode 100644 index 0000000..59bb789 --- /dev/null +++ b/projects/lib/utils/utils/columns-to-gql-fields.ts @@ -0,0 +1,34 @@ +import { FieldDefinition } from '@openmfp/portal-ui-lib'; + +export const generateGraphQLFields = (uiFields: FieldDefinition[]): any[] => { + const graphQLFields = []; + uiFields.map((field) => { + if (field.property instanceof Array) { + field.property.map((property) => generate(property, graphQLFields)); + } else { + generate(field.property, graphQLFields); + } + }); + return graphQLFields; +}; + +const generate = (root: string, fields: any = []) => { + if (!root) { + return []; + } + + const paths = root.split('.'); + + for (const part of paths) { + if (paths.length === 1) { + fields.push(part); + return fields; + } + + fields.push({ + [part]: [...generate(paths.splice(1).join('.'))], + }); + + return fields; + } +}; diff --git a/projects/lib/utils/utils/get-value-by-path.spec.ts b/projects/lib/utils/utils/get-value-by-path.spec.ts new file mode 100644 index 0000000..5887bd3 --- /dev/null +++ b/projects/lib/utils/utils/get-value-by-path.spec.ts @@ -0,0 +1,57 @@ +import { getValueByPath } from './get-value-by-path'; + +describe('getValueByPath', () => { + const obj = { + user: { + id: 1, + name: 'Alice', + address: { + city: 'Wonderland', + zip: 12345, + }, + }, + settings: { + theme: 'dark', + notifications: { + email: true, + sms: false, + }, + }, + }; + + it('should return a top-level property', () => { + expect(getValueByPath(obj, 'user')).toEqual(obj.user); + }); + + it('should return a nested property', () => { + expect(getValueByPath(obj, 'user.name')).toBe('Alice'); + expect(getValueByPath(obj, 'user.address.city')).toBe('Wonderland'); + }); + + it('should return undefined for a missing property', () => { + expect(getValueByPath(obj, 'user.age')).toBeUndefined(); + expect(getValueByPath(obj, 'settings.language')).toBeUndefined(); + }); + + it('should return undefined for a deeply missing property', () => { + expect(getValueByPath(obj, 'user.address.country.code')).toBeUndefined(); + }); + + it('should return undefined if the path is empty', () => { + expect(getValueByPath(obj, '')).toBeUndefined(); + }); + + it('should handle non-object intermediate values gracefully', () => { + expect(getValueByPath(obj, 'user.name.first')).toBeUndefined(); + expect(getValueByPath(obj, 'settings.theme.color')).toBeUndefined(); + }); + + it('should work with boolean, number, and null values', () => { + expect(getValueByPath(obj, 'settings.notifications.email')).toBe(true); + expect(getValueByPath(obj, 'user.address.zip')).toBe(12345); + + const testObj = { a: null }; + expect(getValueByPath(testObj, 'a')).toBeNull(); + expect(getValueByPath(testObj, 'a.b')).toBeUndefined(); + }); +}); diff --git a/projects/lib/utils/utils/get-value-by-path.ts b/projects/lib/utils/utils/get-value-by-path.ts new file mode 100644 index 0000000..463ed40 --- /dev/null +++ b/projects/lib/utils/utils/get-value-by-path.ts @@ -0,0 +1,12 @@ +import { Resource } from '@openmfp/portal-ui-lib'; +import { getResourceValueByJsonPath } from './resource-field-by-path'; + +export const getValueByPath = ( + obj: T, + path: string, +): R | undefined => { + return getResourceValueByJsonPath(obj as Resource, { + jsonPathExpression: path, + property: '', + }); +}; diff --git a/projects/lib/utils/utils/group-name-sanitizer.spec.ts b/projects/lib/utils/utils/group-name-sanitizer.spec.ts new file mode 100644 index 0000000..e69a703 --- /dev/null +++ b/projects/lib/utils/utils/group-name-sanitizer.spec.ts @@ -0,0 +1,89 @@ +import { replaceDotsAndHyphensWithUnderscores } from './group-name-sanitizer'; + +describe('replaceDotsAndHyphensWithUnderscores', () => { + it('should replace dots with underscores', () => { + expect(replaceDotsAndHyphensWithUnderscores('test.string')).toBe( + 'test_string', + ); + }); + + it('should replace hyphens with underscores', () => { + expect(replaceDotsAndHyphensWithUnderscores('test-string')).toBe( + 'test_string', + ); + }); + + it('should replace both dots and hyphens with underscores', () => { + expect(replaceDotsAndHyphensWithUnderscores('test.string-with.both')).toBe( + 'test_string_with_both', + ); + }); + + it('should handle multiple consecutive dots', () => { + expect(replaceDotsAndHyphensWithUnderscores('test...string')).toBe( + 'test___string', + ); + }); + + it('should handle multiple consecutive hyphens', () => { + expect(replaceDotsAndHyphensWithUnderscores('test---string')).toBe( + 'test___string', + ); + }); + + it('should handle mixed consecutive dots and hyphens', () => { + expect(replaceDotsAndHyphensWithUnderscores('test.-.string')).toBe( + 'test___string', + ); + }); + + it('should handle empty string', () => { + expect(replaceDotsAndHyphensWithUnderscores('')).toBe(''); + }); + + it('should handle string with no dots or hyphens', () => { + expect(replaceDotsAndHyphensWithUnderscores('teststring')).toBe( + 'teststring', + ); + }); + + it('should handle string with only dots', () => { + expect(replaceDotsAndHyphensWithUnderscores('...')).toBe('___'); + }); + + it('should handle string with only hyphens', () => { + expect(replaceDotsAndHyphensWithUnderscores('---')).toBe('___'); + }); + + it('should handle string starting with dot', () => { + expect(replaceDotsAndHyphensWithUnderscores('.test')).toBe('_test'); + }); + + it('should handle string starting with hyphen', () => { + expect(replaceDotsAndHyphensWithUnderscores('-test')).toBe('_test'); + }); + + it('should handle string ending with dot', () => { + expect(replaceDotsAndHyphensWithUnderscores('test.')).toBe('test_'); + }); + + it('should handle string ending with hyphen', () => { + expect(replaceDotsAndHyphensWithUnderscores('test-')).toBe('test_'); + }); + + it('should handle null input', () => { + expect(replaceDotsAndHyphensWithUnderscores(null as any)).toBe(null); + }); + + it('should handle undefined input', () => { + expect(replaceDotsAndHyphensWithUnderscores(undefined as any)).toBe( + undefined, + ); + }); + + it('should handle complex real-world example', () => { + expect(replaceDotsAndHyphensWithUnderscores('my-app.v1.0-beta.test')).toBe( + 'my_app_v1_0_beta_test', + ); + }); +}); diff --git a/projects/lib/utils/utils/group-name-sanitizer.ts b/projects/lib/utils/utils/group-name-sanitizer.ts new file mode 100644 index 0000000..c806d97 --- /dev/null +++ b/projects/lib/utils/utils/group-name-sanitizer.ts @@ -0,0 +1,12 @@ +/** + * Utility function to replace all occurrences of dots (.) and hyphens (-) with underscores (_) + * @param input - The input string to process + * @returns The processed string with dots and hyphens replaced by underscores + */ +export function replaceDotsAndHyphensWithUnderscores(input: string): string { + if (!input) { + return input; + } + + return input.replace(/[.-]/g, '_'); +} diff --git a/projects/lib/utils/utils/index.ts b/projects/lib/utils/utils/index.ts new file mode 100644 index 0000000..0c644e9 --- /dev/null +++ b/projects/lib/utils/utils/index.ts @@ -0,0 +1,4 @@ +export * from './columns-to-gql-fields'; +export * from './get-value-by-path'; +export * from './group-name-sanitizer'; +export * from './resource-field-by-path'; diff --git a/projects/lib/utils/utils/resource-field-by-path.spec.ts b/projects/lib/utils/utils/resource-field-by-path.spec.ts new file mode 100644 index 0000000..e0d8ceb --- /dev/null +++ b/projects/lib/utils/utils/resource-field-by-path.spec.ts @@ -0,0 +1,113 @@ +import { FieldDefinition, Resource } from '@openmfp/portal-ui-lib'; +import { getResourceValueByJsonPath } from './resource-field-by-path'; + +describe('getResourceValueByJsonPath', () => { + it('should return undefined when field is undefined', () => { + const resource = {} as Resource; + const result = getResourceValueByJsonPath( + resource, + undefined as unknown as FieldDefinition, + ); + expect(result).toBeUndefined(); + }); + + it('should return undefined when property is not defined', () => { + const resource = {} as Resource; + const field = {} as FieldDefinition; + const result = getResourceValueByJsonPath(resource, field); + expect(result).toBeUndefined(); + }); + + it('should return undefined when property is an array', () => { + const resource = {} as Resource; + const field = { property: ['prop1', 'prop2'] } as FieldDefinition; + + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); + const result = getResourceValueByJsonPath(resource, field); + + expect(result).toBeUndefined(); + expect(consoleSpy).toHaveBeenCalledWith( + 'Property defined as an array: ["prop1","prop2"], provide "jsonPathExpression" field to properly ready resource value', + ); + consoleSpy.mockRestore(); + }); + + it('should use jsonPathExpression if provided', () => { + const resource = { metadata: { name: 'test' } } as Resource; + const field = { + property: 'not-used', + jsonPathExpression: 'metadata.name', + } as FieldDefinition; + + const result = getResourceValueByJsonPath(resource, field); + expect(result).toBe('test'); + }); + + it('should use property if jsonPathExpression is not provided', () => { + const resource = { metadata: { name: 'test' } } as Resource; + const field = { property: 'metadata.name' } as FieldDefinition; + + const result = getResourceValueByJsonPath(resource, field); + expect(result).toBe('test'); + }); + + it('should return undefined when jsonpath query returns empty array', () => { + const resource = { metadata: { name: 'test' } } as Resource; + const field = { property: 'metadata.nonexistent' } as FieldDefinition; + + const result = getResourceValueByJsonPath(resource, field); + expect(result).toBeUndefined(); + }); + + it('should return the first value when jsonpath query returns multiple values', () => { + const resource = { + items: [{ name: 'item1' }, { name: 'item2' }], + } as unknown as Resource; + const field = { property: 'items[*].name' } as FieldDefinition; + + const result = getResourceValueByJsonPath(resource, field); + expect(result).toBe('item1'); + }); + + it('should handle null resource input', () => { + const field = { property: 'metadata.name' } as FieldDefinition; + + const result = getResourceValueByJsonPath( + null as unknown as Resource, + field, + ); + expect(result).toBeUndefined(); + }); + + it('should handle undefined resource input', () => { + const field = { property: 'metadata.name' } as FieldDefinition; + + const result = getResourceValueByJsonPath( + undefined as unknown as Resource, + field, + ); + expect(result).toBeUndefined(); + }); + + it('should handle complex nested paths', () => { + const resource = { + spec: { + template: { + spec: { + containers: [ + { name: 'container1', image: 'image1' }, + { name: 'container2', image: 'image2' }, + ], + }, + }, + }, + } as unknown as Resource; + + const field = { + property: 'spec.template.spec.containers[0].image', + } as FieldDefinition; + + const result = getResourceValueByJsonPath(resource, field); + expect(result).toBe('image1'); + }); +}); diff --git a/projects/lib/utils/utils/resource-field-by-path.ts b/projects/lib/utils/utils/resource-field-by-path.ts new file mode 100644 index 0000000..bea0920 --- /dev/null +++ b/projects/lib/utils/utils/resource-field-by-path.ts @@ -0,0 +1,22 @@ +import { FieldDefinition, Resource } from '@openmfp/portal-ui-lib'; +import jsonpath from 'jsonpath'; + +export const getResourceValueByJsonPath = ( + resource: Resource, + field: FieldDefinition, +) => { + const property = field?.jsonPathExpression || field?.property; + if (!property) { + return undefined; + } + + if (property instanceof Array) { + console.error( + `Property defined as an array: ${JSON.stringify(property)}, provide "jsonPathExpression" field to properly ready resource value`, + ); + return undefined; + } + + const value = jsonpath.query(resource || {}, `$.${property}`); + return value.length ? value[0] : undefined; +}; diff --git "a/projects/lib/_mocks_/ui5\342\200\221mock.ts" b/projects/wc/_mocks_/ui5-mock.ts similarity index 94% rename from "projects/lib/_mocks_/ui5\342\200\221mock.ts" rename to projects/wc/_mocks_/ui5-mock.ts index 6f26246..9a81681 100644 --- "a/projects/lib/_mocks_/ui5\342\200\221mock.ts" +++ b/projects/wc/_mocks_/ui5-mock.ts @@ -1,5 +1,4 @@ import { Component } from '@angular/core'; -import { ButtonComponent } from '@ui5/webcomponents-ngx'; @Component({ selector: 'ui5-component', template: '', standalone: true }) export class MockComponent {} @@ -30,3 +29,4 @@ jest.mock('@ui5/webcomponents-ngx', () => { ToolbarComponent: MockComponent, }; }); + diff --git a/projects/wc/jest.config.js b/projects/wc/jest.config.js new file mode 100644 index 0000000..7e91b20 --- /dev/null +++ b/projects/wc/jest.config.js @@ -0,0 +1,34 @@ +const path = require('path'); + +module.exports = { + displayName: 'wc', + roots: [__dirname], + testMatch: ['**/*.spec.ts'], + coverageDirectory: path.resolve(__dirname, '../../coverage/wc'), + collectCoverageFrom: [ + '!/projects/wc/**/*.spec.ts', + ], + coveragePathIgnorePatterns: [ + '/projects/lib/', + '/projects/wc/src/main.ts', + '/projects/wc/src/app/app.config.ts', + '/projects/wc/jest.config.js', + ], + setupFilesAfterEnv: [`${__dirname}/jest.setup.ts`], + modulePathIgnorePatterns: ['/projects/wc/_mocks_/'], + coverageThreshold: { + global: { + branches: 85, + functions: 80, + lines: 94, + statements: -8, + }, + }, + moduleNameMapper: { + '^lodash-es(.*)': 'lodash', + '^@platform-mesh/portal-ui-lib$': '/projects/lib/public-api.ts', + '^@platform-mesh/portal-ui-lib/services$': '/projects/lib/services/public-api.ts', + '^@platform-mesh/portal-ui-lib/utils$': '/projects/lib/utils/public-api.ts', + '^@platform-mesh/portal-ui-lib/(.*)': '/projects/lib/$1', + }, +}; diff --git a/projects/wc/jest.setup.ts b/projects/wc/jest.setup.ts new file mode 100644 index 0000000..80d4719 --- /dev/null +++ b/projects/wc/jest.setup.ts @@ -0,0 +1 @@ +jest.requireMock('./_mocks_/ui5-mock'); diff --git a/projects/wc/src/app/app.config.ts b/projects/wc/src/app/app.config.ts new file mode 100644 index 0000000..cc934ed --- /dev/null +++ b/projects/wc/src/app/app.config.ts @@ -0,0 +1,12 @@ +import { provideLuigiWebComponents } from './initializers/luigi-wc-initializer'; +import { provideHttpClient } from '@angular/common/http'; +import { ApplicationConfig } from '@angular/core'; +import '@ui5/webcomponents-fiori/illustrations/NoData.js'; +import '@ui5/webcomponents-icons/dist/delete.js'; +import '@ui5/webcomponents-icons/dist/download-from-cloud.js'; + +document.body.classList.add('ui5-content-density-compact'); + +export const appConfig: ApplicationConfig = { + providers: [provideHttpClient(), provideLuigiWebComponents()], +}; diff --git a/projects/wc/src/app/components/dynamic-select/dynamic-select.component.html b/projects/wc/src/app/components/dynamic-select/dynamic-select.component.html new file mode 100644 index 0000000..0cb3253 --- /dev/null +++ b/projects/wc/src/app/components/dynamic-select/dynamic-select.component.html @@ -0,0 +1,20 @@ +@let dynamicValues = dynamicValues$(); +@if(dynamicValues.length > 0) { + + @for (item of dynamicValues; track item.value) { + {{ item.key }} + } + +} diff --git a/projects/wc/src/app/components/dynamic-select/dynamic-select.component.scss b/projects/wc/src/app/components/dynamic-select/dynamic-select.component.scss new file mode 100644 index 0000000..be0991e --- /dev/null +++ b/projects/wc/src/app/components/dynamic-select/dynamic-select.component.scss @@ -0,0 +1,8 @@ +.input { + width: 100%; +} + +:host { + display: block; + width: 100%; +} \ No newline at end of file diff --git a/projects/wc/src/app/components/dynamic-select/dynamic-select.component.spec.ts b/projects/wc/src/app/components/dynamic-select/dynamic-select.component.spec.ts new file mode 100644 index 0000000..af43edd --- /dev/null +++ b/projects/wc/src/app/components/dynamic-select/dynamic-select.component.spec.ts @@ -0,0 +1,66 @@ +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ResourceService } from '@platform-mesh/portal-ui-lib/services'; +import { of } from 'rxjs'; +import { DynamicSelectComponent } from './dynamic-select.component'; + +const mockResourceService = { + list: jest.fn(), +}; + +describe('DynamicSelectComponent', () => { + let component: DynamicSelectComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + mockResourceService.list.mockReturnValue(of([])); + + await TestBed.configureTestingModule({ + imports: [DynamicSelectComponent], + providers: [{ provide: ResourceService, useValue: mockResourceService }], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(DynamicSelectComponent, { + set: { template: '' }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(DynamicSelectComponent); + component = fixture.componentInstance; + + const fieldDefinition: any = { + dynamicValuesDefinition: { + opeartion: 'getData', + gqlQuery: '{ someQuery }', + key: 'name', + value: 'id', + }, + }; + + const context: any = { id: 'ctx' }; + + component.field = (() => fieldDefinition) as any; + component.context = (() => context) as any; + }); + + it('should create component', () => { + expect(component).toBeTruthy(); + }); + + it('should load dynamicValues via ResourceService', () => { + const mockResponse = [ + { id: '1', name: 'First' }, + { id: '2', name: 'Second' }, + ]; + + mockResourceService.list.mockReturnValue(of(mockResponse)); + + fixture.detectChanges(); + + const values = component.dynamicValues$(); + expect(values).toEqual([ + { value: '1', key: 'First' }, + { value: '2', key: 'Second' }, + ]); + }); +}); diff --git a/projects/wc/src/app/components/dynamic-select/dynamic-select.component.ts b/projects/wc/src/app/components/dynamic-select/dynamic-select.component.ts new file mode 100644 index 0000000..91965b3 --- /dev/null +++ b/projects/wc/src/app/components/dynamic-select/dynamic-select.component.ts @@ -0,0 +1,79 @@ +import { + Component, + DestroyRef, + effect, + inject, + input, + output, + signal, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + FieldDefinition, +} from '@openmfp/portal-ui-lib'; +import { ResourceNodeContext, ResourceService } from '@platform-mesh/portal-ui-lib/services'; +import { getValueByPath } from '@platform-mesh/portal-ui-lib/utils'; +import { OptionComponent, SelectComponent } from '@ui5/webcomponents-ngx'; +import { Observable, map } from 'rxjs'; + +@Component({ + selector: 'dynamic-select', + imports: [SelectComponent, OptionComponent], + templateUrl: './dynamic-select.component.html', + styleUrl: './dynamic-select.component.scss', +}) +export class DynamicSelectComponent { + field = input.required(); + context = input.required(); + + value = input(); + required = input(false); + valueState = input< + 'None' | 'Positive' | 'Critical' | 'Negative' | 'Information' + >('None'); + + change = output(); + input = output(); + blur = output(); + + dynamicValues$ = signal<{ value: string; key: string }[]>([]); + + private resourceService = inject(ResourceService); + private destroyRef = inject(DestroyRef); + + constructor() { + effect(() => { + this.getDynamicValues(this.field(), this.context()) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((result) => { + this.dynamicValues$.set(result); + }); + }); + } + + private getDynamicValues( + fieldDefinition: FieldDefinition, + context: ResourceNodeContext, + ): Observable<{ value: string; key: string }[]> { + return this.resourceService + .list( + fieldDefinition.dynamicValuesDefinition.operation, + fieldDefinition.dynamicValuesDefinition.gqlQuery, + context, + ) + .pipe( + map((result) => + result.map((resource) => ({ + value: getValueByPath( + resource, + fieldDefinition.dynamicValuesDefinition.value, + ), + key: getValueByPath( + resource, + fieldDefinition.dynamicValuesDefinition.key, + ), + })), + ), + ); + } +} diff --git a/projects/wc/src/app/components/generic-ui/detail-view/detail-view.component.html b/projects/wc/src/app/components/generic-ui/detail-view/detail-view.component.html new file mode 100644 index 0000000..99ab9e9 --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/detail-view/detail-view.component.html @@ -0,0 +1,55 @@ + + + + +
+ {{ resource()?.spec?.displayName || resourceId }} +
+
+ + The {{ resourceDefinition?.singular }} for + {{ resource()?.spec?.displayName || resourceId }} + + + + + +
+ + +
+ @if (resourceDefinition.ui?.logoUrl) { + + } +
+ Workspace Path +

{{ workspacePath }}

+
+ + @for (field of resourceFields; track field.property) { +
+ {{ field.label }} +

+ +

+
+ } +
+
+ + +
diff --git a/projects/wc/src/app/components/generic-ui/detail-view/detail-view.component.scss b/projects/wc/src/app/components/generic-ui/detail-view/detail-view.component.scss new file mode 100644 index 0000000..3b358ef --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/detail-view/detail-view.component.scss @@ -0,0 +1,32 @@ +.resource-title { + display: flex; + align-items: center; + padding: 1rem 0; +} + +.resource-title-subheading { + display: block; + padding-bottom: 1rem; +} + +.resource-logo { + width: 5rem; + padding-bottom: 1rem; +} + +.resource-title-actions { + height: auto; +} + +.resource-info { + display: flex; + flex-wrap: wrap; + gap: 2rem; +} + +.resource-info-cell { + display: flex; + gap: 5px; + flex-direction: column; + max-width: 300px; +} diff --git a/projects/wc/src/app/components/generic-ui/detail-view/detail-view.component.spec.ts b/projects/wc/src/app/components/generic-ui/detail-view/detail-view.component.spec.ts new file mode 100644 index 0000000..78332c1 --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/detail-view/detail-view.component.spec.ts @@ -0,0 +1,119 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { GatewayService, ResourceService } from '@platform-mesh/portal-ui-lib/services'; +import { of } from 'rxjs'; +import { DetailViewComponent } from './detail-view.component'; + +describe('DetailViewComponent', () => { + let component: DetailViewComponent; + let fixture: ComponentFixture; + let mockResourceService: any; + let mockGatewayService: any; + let luigiClientLinkManagerNavigate = jest.fn(); + + beforeEach(() => { + mockResourceService = { + read: jest.fn().mockReturnValue(of({ name: 'test-resource' })), + readAccountInfo: jest.fn().mockReturnValue(of('mock-ca-data')), + }; + + mockGatewayService = { + resolveKcpPath: jest.fn().mockReturnValue('https://example.com'), + }; + + TestBed.configureTestingModule({ + providers: [ + { provide: ResourceService, useValue: mockResourceService }, + { provide: GatewayService, useValue: mockGatewayService }, + ], + }).overrideComponent(DetailViewComponent, { + set: { template: '
' }, + }); + + fixture = TestBed.createComponent(DetailViewComponent); + component = fixture.componentInstance; + + component.context = (() => ({ + resourceId: 'cluster-1', + token: 'abc123', + resourceDefinition: { + kind: 'Cluster', + group: 'core.k8s.io', + ui: { + detailView: { + fields: [], + }, + }, + }, + entity: { + metadata: { name: 'test-resource' }, + }, + parentNavigationContexts: ['project'], + })) as any; + + component.LuigiClient = (() => ({ + linkManager: () => ({ + fromContext: jest.fn().mockReturnThis(), + navigate: luigiClientLinkManagerNavigate, + withParams: jest.fn().mockReturnThis(), + }), + getNodeParams: jest.fn(), + })) as any; + + fixture.detectChanges(); + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should call read on init', () => { + expect(mockResourceService.read).toHaveBeenCalled(); + }); + + it('should navigate to parent', () => { + component.navigateToParent(); + expect(luigiClientLinkManagerNavigate).toHaveBeenCalledWith('/'); + }); + + it('should have namespaceId in context when provided', () => { + fixture = TestBed.createComponent(DetailViewComponent); + component = fixture.componentInstance; + + const testNamespace = 'test-namespace'; + component.context = (() => ({ + resourceId: 'cluster-1', + token: 'abc123', + namespaceId: testNamespace, + resourceDefinition: { + kind: 'Cluster', + group: 'core.k8s.io', + ui: { + detailView: { + fields: [], + }, + }, + }, + entity: { metadata: { name: 'test-resource' } }, + parentNavigationContexts: ['project'], + })) as any; + + fixture.detectChanges(); + + expect(component.context().namespaceId).toBe(testNamespace); + }); + + it('should download kubeconfig', async () => { + const mockAnchorElement = document.createElement('a'); + jest.spyOn(mockAnchorElement, 'click'); + jest.spyOn(document, 'createElement').mockReturnValue(mockAnchorElement); + + global.URL.createObjectURL = jest.fn().mockReturnValue('blob-url'); + + await component.downloadKubeConfig(); + + expect(document.createElement).toHaveBeenCalledWith('a'); + expect(mockAnchorElement.href).toEqual('http://localhost/blob-url'); + expect(mockAnchorElement.download).toBe('kubeconfig.yaml'); + expect(mockAnchorElement.click).toHaveBeenCalled(); + }); +}); diff --git a/projects/wc/src/app/components/generic-ui/detail-view/detail-view.component.ts b/projects/wc/src/app/components/generic-ui/detail-view/detail-view.component.ts new file mode 100644 index 0000000..276921b --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/detail-view/detail-view.component.ts @@ -0,0 +1,128 @@ +import { + ChangeDetectionStrategy, + Component, + ViewEncapsulation, + effect, + inject, + input, + signal, +} from '@angular/core'; +import { LuigiClient } from '@luigi-project/client/luigi-element'; +import { + FieldDefinition, + Resource, + ResourceDefinition, +} from '@openmfp/portal-ui-lib'; +import { GatewayService, ResourceNodeContext, ResourceService } from '@platform-mesh/portal-ui-lib/services'; +import { generateGraphQLFields, getResourceValueByJsonPath, replaceDotsAndHyphensWithUnderscores } from '@platform-mesh/portal-ui-lib/utils'; +import { + DynamicPageComponent, + DynamicPageHeaderComponent, + DynamicPageTitleComponent, + LabelComponent, + TextComponent, + TitleComponent, + ToolbarButtonComponent, + ToolbarComponent, +} from '@ui5/webcomponents-ngx'; +import { ValueCellComponent } from '../value-cell/value-cell.component'; +import { kubeConfigTemplate } from './kubeconfig-template'; + +const defaultFields: FieldDefinition[] = [ + { + label: 'Workspace Status', + jsonPathExpression: 'status.conditions[?(@.type=="Ready")].status', + property: ['status.conditions.status', 'status.conditions.type'], + }, +]; + +@Component({ + selector: 'detail-view', + encapsulation: ViewEncapsulation.ShadowDom, + standalone: true, + imports: [ + DynamicPageComponent, + DynamicPageTitleComponent, + TitleComponent, + TextComponent, + ToolbarComponent, + ToolbarButtonComponent, + DynamicPageHeaderComponent, + LabelComponent, + ValueCellComponent, + ], + templateUrl: './detail-view.component.html', + styleUrl: './detail-view.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DetailViewComponent { + private resourceService = inject(ResourceService); + private gatewayService = inject(GatewayService); + protected readonly getResourceValueByJsonPath = getResourceValueByJsonPath; + + LuigiClient = input(); + context = input(); + resource = signal(null); + + resourceDefinition: ResourceDefinition; + workspacePath: string; + resourceFields: FieldDefinition[]; + kcpCA: string = ''; + resourceId: string; + + constructor() { + effect(() => { + this.workspacePath = this.gatewayService.resolveKcpPath(this.context()); + this.resourceFields = + this.context().resourceDefinition.ui?.detailView?.fields || + defaultFields; + this.resourceDefinition = this.context().resourceDefinition; + this.resourceId = this.context().entity.metadata.name; + this.readResource(); + }); + } + + private readResource(): void { + const fields = generateGraphQLFields(this.resourceFields); + const queryOperation = replaceDotsAndHyphensWithUnderscores( + this.resourceDefinition.group, + ); + const kind = this.resourceDefinition.kind; + + this.resourceService + .read( + this.resourceId, + queryOperation, + kind, + fields, + this.context(), + kind.toLowerCase() === 'account', + ) + .subscribe({ + next: (result) => this.resource.set(result), + }); + } + + navigateToParent() { + this.LuigiClient() + .linkManager() + .fromContext(this.context().parentNavigationContexts.at(-1)) + .navigate('/'); + } + + async downloadKubeConfig() { + const kubeConfig = kubeConfigTemplate + .replaceAll('', this.context().accountId) + .replaceAll('', this.workspacePath) + .replaceAll('', this.context().kcpCA) + .replaceAll('', this.context().token); + + const blob = new Blob([kubeConfig], { type: 'application/plain' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = 'kubeconfig.yaml'; + a.click(); + } +} diff --git a/projects/wc/src/app/components/generic-ui/detail-view/kubeconfig-template.ts b/projects/wc/src/app/components/generic-ui/detail-view/kubeconfig-template.ts new file mode 100644 index 0000000..5668c86 --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/detail-view/kubeconfig-template.ts @@ -0,0 +1,30 @@ +export const kubeConfigTemplate = ` + apiVersion: v1 + kind: Config + clusters: + - name: + cluster: + certificate-authority-data: + server: "https://kcp.api.portal.cc-one.showroom.apeirora.eu/clusters/" + contexts: + - name: + context: + cluster: + user: + current-context: + users: + - name: + user: + exec: + apiVersion: client.authentication.k8s.io/v1beta1 + args: + - oidc-login + - get-token + - --oidc-issuer-url=https://portal.cc-one.showroom.apeirora.eu/keycloak/realms/openmfp + - --oidc-client-id=openmfp-public + - --oidc-extra-scope=email + - --oidc-extra-scope=groups + command: kubectl + env: null + interactiveMode: IfAvailable +`; diff --git a/projects/wc/src/app/components/generic-ui/list-view/create-resource-modal/create-resource-modal.component.html b/projects/wc/src/app/components/generic-ui/list-view/create-resource-modal/create-resource-modal.component.html new file mode 100644 index 0000000..b5139ea --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/list-view/create-resource-modal/create-resource-modal.component.html @@ -0,0 +1,68 @@ + +
+ @for (field of fields(); track field.property) { + @let fieldProperty = sanitizePropertyName(field.property); +
+ {{ + field.label + }} + @if (field.values?.length) { + + @for (value of [''].concat(field.values); track value) { + {{ value }} + } + + } @else if (field.dynamicValuesDefinition) { + + } @else { + + } +
+ } +
+ + + + + + +
diff --git a/projects/wc/src/app/components/generic-ui/list-view/create-resource-modal/create-resource-modal.component.scss b/projects/wc/src/app/components/generic-ui/list-view/create-resource-modal/create-resource-modal.component.scss new file mode 100644 index 0000000..fe8eadd --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/list-view/create-resource-modal/create-resource-modal.component.scss @@ -0,0 +1,12 @@ +.form > div { + display: flex; + flex-direction: column; + justify-content: space-evenly; + align-items: flex-start; + margin-bottom: 0.5rem; + width: 100%; +} + +.input { + width: 100%; +} diff --git a/projects/wc/src/app/components/generic-ui/list-view/create-resource-modal/create-resource-modal.component.spec.ts b/projects/wc/src/app/components/generic-ui/list-view/create-resource-modal/create-resource-modal.component.spec.ts new file mode 100644 index 0000000..f5059b9 --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/list-view/create-resource-modal/create-resource-modal.component.spec.ts @@ -0,0 +1,171 @@ +import { CreateResourceModalComponent } from './create-resource-modal.component'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FieldDefinition } from '@openmfp/portal-ui-lib'; + +describe('CreateResourceModalComponent', () => { + let component: CreateResourceModalComponent; + let fixture: ComponentFixture; + let mockDialog: any; + + const testFields: FieldDefinition[] = [ + { property: 'name.firstName', required: true, label: 'First Name' }, + { property: 'address.city', required: false, label: 'City' }, + ]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, CreateResourceModalComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(CreateResourceModalComponent, { + set: { template: '' }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(CreateResourceModalComponent); + component = fixture.componentInstance; + + component.fields = (() => testFields) as any; + + mockDialog = { + open: false, + }; + (component as any).dialog = () => mockDialog; + + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize form with controls from fields input', () => { + expect(component.form).toBeDefined(); + expect(component.form.controls['name_firstName']).toBeDefined(); + expect(component.form.controls['address_city']).toBeDefined(); + + const firstNameControl = component.form.controls['name_firstName']; + firstNameControl.setValue(''); + expect(firstNameControl.valid).toBeFalsy(); + + const cityControl = component.form.controls['address_city']; + cityControl.setValue(''); + expect(cityControl.valid).toBeTruthy(); + }); + + it('should open dialog when open method is called', () => { + component.open(); + expect(mockDialog.open).toBeTruthy(); + }); + + it('should close dialog and reset form when close method is called', () => { + spyOn(component.form, 'reset'); + + component.close(); + + expect(mockDialog.open).toBeFalsy(); + expect(component.form.reset).toHaveBeenCalled(); + }); + + it('should transform form data and emit resource when create method is called with valid form', () => { + component.form.controls['name_firstName'].setValue('John'); + component.form.controls['address_city'].setValue('New York'); + + spyOn(component.resource, 'emit'); + + component.create(); + + expect(component.resource.emit).toHaveBeenCalledWith({ + name: { firstName: 'John' }, + address: { city: 'New York' }, + }); + + expect(mockDialog.open).toBeFalsy(); + }); + + it('should not emit resource when form is invalid', () => { + component.form.controls['name_firstName'].setValue(''); + component.form.controls['address_city'].setValue('New York'); + + spyOn(component.resource, 'emit'); + + component.create(); + + expect(component.resource.emit).not.toHaveBeenCalled(); + }); + + it('should update form control value, mark as touched and dirty on setFormControlValue', () => { + const event = { target: { value: 'Test' } }; + + spyOn(component.form.controls['name_firstName'], 'setValue'); + spyOn(component.form.controls['name_firstName'], 'markAsTouched'); + spyOn(component.form.controls['name_firstName'], 'markAsDirty'); + + component.setFormControlValue(event, 'name_firstName'); + + expect( + component.form.controls['name_firstName'].setValue, + ).toHaveBeenCalledWith('Test'); + expect( + component.form.controls['name_firstName'].markAsTouched, + ).toHaveBeenCalled(); + expect( + component.form.controls['name_firstName'].markAsDirty, + ).toHaveBeenCalled(); + }); + + it('should return Negative value state for invalid and touched control', () => { + const control = component.form.controls['name_firstName']; + control.setValue(''); + control.markAsTouched(); + + expect(component.getValueState('name_firstName')).toBe('Negative'); + }); + + it('should return None value state for valid control or untouched control', () => { + const control = component.form.controls['name_firstName']; + control.setValue('John'); + control.markAsTouched(); + + expect(component.getValueState('name_firstName')).toBe('None'); + + control.setValue(''); + control.markAsUntouched(); + + expect(component.getValueState('name_firstName')).toBe('None'); + }); + + it('should mark control as touched on field blur', () => { + spyOn(component.form.controls['name_firstName'], 'markAsTouched'); + + component.onFieldBlur('name_firstName'); + + expect( + component.form.controls['name_firstName'].markAsTouched, + ).toHaveBeenCalled(); + }); + + describe('sanitizePropertyName', () => { + it('should replace dots with underscores in property name', () => { + const property = 'metadata.name.firstName'; + const result = (component as any).sanitizePropertyName(property); + expect(result).toBe('metadata_name_firstName'); + }); + + it('should handle property names without dots', () => { + const property = 'name'; + const result = (component as any).sanitizePropertyName(property); + expect(result).toBe('name'); + }); + + it('should throw error when property is an array', () => { + const property = ['name', 'firstName']; + expect(() => (component as any).sanitizePropertyName(property)).toThrow( + 'Wrong property type, array not supported', + ); + }); + }); +}); diff --git a/projects/wc/src/app/components/generic-ui/list-view/create-resource-modal/create-resource-modal.component.ts b/projects/wc/src/app/components/generic-ui/list-view/create-resource-modal/create-resource-modal.component.ts new file mode 100644 index 0000000..85256dc --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/list-view/create-resource-modal/create-resource-modal.component.ts @@ -0,0 +1,122 @@ +import { + Component, + OnInit, + ViewEncapsulation, + inject, + input, + output, + viewChild, +} from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { + FieldDefinition, + Resource, +} from '@openmfp/portal-ui-lib'; +import { ResourceNodeContext } from '@platform-mesh/portal-ui-lib/services'; +import { + DialogComponent, + InputComponent, + LabelComponent, + OptionComponent, + SelectComponent, + ToolbarButtonComponent, + ToolbarComponent, +} from '@ui5/webcomponents-ngx'; +import { set } from 'lodash'; +import { DynamicSelectComponent } from '../../../dynamic-select/dynamic-select.component'; + +@Component({ + selector: 'create-resource-modal', + standalone: true, + imports: [ + ReactiveFormsModule, + DialogComponent, + OptionComponent, + SelectComponent, + InputComponent, + LabelComponent, + ToolbarButtonComponent, + ToolbarComponent, + DynamicSelectComponent, + ], + templateUrl: './create-resource-modal.component.html', + styleUrl: './create-resource-modal.component.scss', + encapsulation: ViewEncapsulation.ShadowDom, +}) +export class CreateResourceModalComponent implements OnInit { + fields = input([]); + context = input(); + resource = output(); + dialog = viewChild('dialog'); + + fb = inject(FormBuilder); + form: FormGroup; + + ngOnInit(): void { + this.form = this.fb.group(this.createControls()); + } + + open() { + const dialog = this.dialog(); + if (dialog) { + dialog.open = true; + } + } + + close() { + const dialog = this.dialog(); + if (dialog) { + dialog.open = false; + this.form.reset(); + } + } + + create() { + if (this.form.valid) { + const result = {} as Resource; + for (const key in this.form.value) { + set(result, key.replaceAll('_', '.'), this.form.value[key]); + } + + this.resource.emit(result); + this.close(); + } + } + + private createControls() { + return this.fields().reduce((obj, fieldDefinition) => { + const validator = fieldDefinition.required ? Validators.required : null; + obj[this.sanitizePropertyName(fieldDefinition.property)] = + new FormControl('', validator); + return obj; + }, {}); + } + + setFormControlValue($event: any, formControlName: string) { + this.form.controls[formControlName].setValue($event.target.value); + this.form.controls[formControlName].markAsTouched(); + this.form.controls[formControlName].markAsDirty(); + } + + getValueState(formControlName: string) { + const control = this.form.controls[formControlName]; + return control.invalid && control.touched ? 'Negative' : 'None'; + } + + onFieldBlur(formControlName: string) { + this.form.controls[formControlName].markAsTouched(); + } + + sanitizePropertyName(property: string | string[]) { + if (property instanceof Array) { + throw new Error('Wrong property type, array not supported'); + } + return (property as string).replaceAll('.', '_'); + } +} diff --git a/projects/wc/src/app/components/generic-ui/list-view/list-view.component.html b/projects/wc/src/app/components/generic-ui/list-view/list-view.component.html new file mode 100644 index 0000000..a09e380 --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/list-view/list-view.component.html @@ -0,0 +1,76 @@ + + + + + + + This page displays the created + {{ resourceDefinition.plural }} in your environment + + + + @if (hasUiCreateViewFields()) { + + } + + + + + + @for (column of columns; track column.property) { + {{ column.label }} + } + Actions + + + + No Resources + There are currently no items to show. + + + @for (item of resources(); track item.metadata.name) { + + @for (column of columns; track column.label) { + + + + } + + + + + } + + + +@if (hasUiCreateViewFields()) { + +} diff --git a/projects/wc/src/app/components/generic-ui/list-view/list-view.component.scss b/projects/wc/src/app/components/generic-ui/list-view/list-view.component.scss new file mode 100644 index 0000000..8ab161c --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/list-view/list-view.component.scss @@ -0,0 +1,35 @@ +.title-logo { + display: flex; + align-items: center; + padding: 1rem 0; +} + +.logo { + width: 2rem; + padding: 0 1rem; +} + +.title-subheading { + display: block; + padding-bottom: 1rem; +} + +.title-actions { + height: auto; +} + +.actions-column { + width: fit-content !important; + min-width: fit-content !important; + white-space: nowrap; + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.actions-column ui5-icon { + min-width: 1rem; +} + +.delete-item { + padding: 0 1rem; +} diff --git a/projects/wc/src/app/components/generic-ui/list-view/list-view.component.spec.ts b/projects/wc/src/app/components/generic-ui/list-view/list-view.component.spec.ts new file mode 100644 index 0000000..e269fb5 --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/list-view/list-view.component.spec.ts @@ -0,0 +1,115 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { LuigiCoreService } from '@openmfp/portal-ui-lib'; +import { ResourceService } from '@platform-mesh/portal-ui-lib/services'; +import { of, throwError } from 'rxjs'; +import { ListViewComponent } from './list-view.component'; + +describe('ListViewComponent', () => { + let component: ListViewComponent; + let fixture: ComponentFixture; + let mockResourceService: any; + let mockLuigiCoreService: any; + + beforeEach(() => { + mockResourceService = { + list: jest.fn().mockReturnValue(of([{ metadata: { name: 'test' } }])), + delete: jest.fn().mockReturnValue(of({})), + create: jest.fn().mockReturnValue(of({ data: { name: 'test' } })), + }; + + mockLuigiCoreService = { + showAlert: jest.fn(), + }; + + TestBed.configureTestingModule({ + providers: [ + { provide: ResourceService, useValue: mockResourceService }, + { provide: LuigiCoreService, useValue: mockLuigiCoreService }, + ], + }).overrideComponent(ListViewComponent, { + set: { template: '' }, + }); + + fixture = TestBed.createComponent(ListViewComponent); + component = fixture.componentInstance; + + component.context = (() => ({ + resourceDefinition: { + plural: 'clusters', + kind: 'Cluster', + group: 'core.k8s.io', + ui: { + listView: { + fields: [], + }, + }, + }, + })) as any; + + component.LuigiClient = (() => ({ + linkManager: () => ({ + fromContext: jest.fn().mockReturnThis(), + navigate: jest.fn(), + withParams: jest.fn().mockReturnThis(), + }), + getNodeParams: jest.fn(), + })) as any; + + fixture.detectChanges(); + }); + + it('should create component', () => { + expect(component).toBeTruthy(); + }); + + it('should fetch resources on init', () => { + expect(mockResourceService.list).toHaveBeenCalled(); + expect(component.resources().length).toBeGreaterThan(0); + }); + + it('should delete a resource', () => { + const resource = { metadata: { name: 'test' } }; + const event = { stopPropagation: jest.fn() }; + + component.delete(event, resource as any); + expect(mockResourceService.delete).toHaveBeenCalled(); + }); + + it('should show alert on delete error', () => { + mockResourceService.delete.mockReturnValueOnce( + throwError(() => new Error()), + ); + const resource = { metadata: { name: 'test' } }; + const event = { stopPropagation: jest.fn() }; + + component.delete(event, resource as any); + expect(mockLuigiCoreService.showAlert).toHaveBeenCalled(); + }); + + it('should create a resource', () => { + const resource = { metadata: { name: 'test' } }; + + component.create(resource as any); + expect(mockResourceService.create).toHaveBeenCalled(); + }); + + it('should navigate to resource', () => { + const resource = { metadata: { name: 'res1' } }; + const navSpy = jest.fn(); + component.LuigiClient = (() => ({ + linkManager: () => ({ + navigate: navSpy, + }), + })) as any; + + component.navigateToResource(resource as any); + expect(navSpy).toHaveBeenCalledWith('res1'); + }); + + it('should check create view fields existence', () => { + component.resourceDefinition.ui.createView = { + fields: [{ property: 'any' }], + }; + expect(component.hasUiCreateViewFields()).toBe(true); + }); +}); diff --git a/projects/wc/src/app/components/generic-ui/list-view/list-view.component.ts b/projects/wc/src/app/components/generic-ui/list-view/list-view.component.ts new file mode 100644 index 0000000..29568b7 --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/list-view/list-view.component.ts @@ -0,0 +1,158 @@ +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + OnInit, + ViewEncapsulation, + effect, + inject, + input, + signal, + viewChild, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { LuigiClient } from '@luigi-project/client/luigi-element'; +import { + FieldDefinition, + LuigiCoreService, + Resource, + ResourceDefinition, +} from '@openmfp/portal-ui-lib'; +import { ResourceNodeContext, ResourceService } from '@platform-mesh/portal-ui-lib/services'; +import { generateGraphQLFields, getResourceValueByJsonPath, replaceDotsAndHyphensWithUnderscores } from '@platform-mesh/portal-ui-lib/utils'; +import { + DynamicPageComponent, + DynamicPageTitleComponent, + IconComponent, + IllustratedMessageComponent, + TableCellComponent, + TableComponent, + TableHeaderCellComponent, + TableHeaderRowComponent, + TableRowComponent, + TextComponent, + TitleComponent, + ToolbarButtonComponent, + ToolbarComponent, +} from '@ui5/webcomponents-ngx'; +import { ValueCellComponent } from '../value-cell/value-cell.component'; +import { CreateResourceModalComponent } from './create-resource-modal/create-resource-modal.component'; + +const defaultColumns: FieldDefinition[] = [ + { + label: 'Name', + property: 'metadata.name', + }, + { + label: 'Workspace Status', + jsonPathExpression: 'status.conditions[?(@.type=="Ready")].status', + property: ['status.conditions.status', 'status.conditions.type'], + }, +]; + +@Component({ + selector: 'list-view', + standalone: true, + templateUrl: './list-view.component.html', + styleUrls: ['./list-view.component.scss'], + encapsulation: ViewEncapsulation.ShadowDom, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CreateResourceModalComponent, + DynamicPageComponent, + DynamicPageTitleComponent, + IconComponent, + IllustratedMessageComponent, + TableComponent, + TableCellComponent, + TableHeaderCellComponent, + TableHeaderRowComponent, + TableRowComponent, + TextComponent, + TitleComponent, + ToolbarButtonComponent, + ToolbarComponent, + ValueCellComponent, + ], +}) +export class ListViewComponent implements OnInit { + private resourceService = inject(ResourceService); + private luigiCoreService = inject(LuigiCoreService); + private destroyRef = inject(DestroyRef); + LuigiClient = input(); + context = input(); + private createModal = viewChild('createModal'); + + resources = signal([]); + columns: FieldDefinition[]; + heading: string; + resourceDefinition: ResourceDefinition; + protected readonly getResourceValueByJsonPath = getResourceValueByJsonPath; + + constructor() { + effect(() => { + this.resourceDefinition = this.context().resourceDefinition; + this.columns = + this.context().resourceDefinition.ui?.listView?.fields || + defaultColumns; + this.heading = `${this.context().resourceDefinition.plural.charAt(0).toUpperCase()}${this.context().resourceDefinition.plural.slice(1)}`; + this.list(); + }); + } + + ngOnInit(): void {} + + list() { + const fields = generateGraphQLFields(this.columns); + const queryOperation = `${replaceDotsAndHyphensWithUnderscores(this.resourceDefinition.group)}_${this.resourceDefinition.plural}`; + + this.resourceService + .list(queryOperation, fields, this.context()) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (result) => { + this.resources.set(result); + }, + }); + } + + delete(event: any, resource: Resource) { + event.stopPropagation(); + + this.resourceService + .delete(resource, this.resourceDefinition, this.context()) + .subscribe({ + next: (result) => { + console.debug('Resource deleted.'); + }, + error: (error) => { + this.luigiCoreService.showAlert({ + text: `Failure! Could not delete resource: ${resource.metadata.name}.`, + type: 'error', + }); + }, + }); + } + + create(resource: Resource) { + this.resourceService + .create(resource, this.resourceDefinition, this.context()) + .subscribe({ + next: (result) => { + console.debug('Resource created', result); + }, + }); + } + + navigateToResource(resource: Resource) { + this.LuigiClient().linkManager().navigate(resource.metadata.name); + } + + openCreateResourceModal() { + this.createModal()?.open(); + } + + hasUiCreateViewFields() { + return !!this.resourceDefinition?.ui?.createView?.fields?.length; + } +} diff --git a/projects/wc/src/app/components/generic-ui/value-cell/value-cell.component.html b/projects/wc/src/app/components/generic-ui/value-cell/value-cell.component.html new file mode 100644 index 0000000..7f80fc3 --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/value-cell/value-cell.component.html @@ -0,0 +1,5 @@ +@if (isBoolLike()) { + +} @else { + {{ value() }} +} diff --git a/projects/wc/src/app/components/generic-ui/value-cell/value-cell.component.spec.ts b/projects/wc/src/app/components/generic-ui/value-cell/value-cell.component.spec.ts new file mode 100644 index 0000000..0af756a --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/value-cell/value-cell.component.spec.ts @@ -0,0 +1,74 @@ +import { ValueCellComponent } from './value-cell.component'; +import { + ICON_DESIGN_NEGATIVE, + ICON_DESIGN_POSITIVE, + ICON_NAME_NEGATIVE, + ICON_NAME_POSITIVE, +} from './value-cell.constants'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +jest.mock('@ui5/webcomponents-ngx', () => ({ IconComponent: class {} }), { + virtual: true, +}); + +describe('ValueCellComponent', () => { + let component: ValueCellComponent; + let fixture: ComponentFixture; + + const makeComponent = (value: unknown) => { + fixture = TestBed.createComponent(ValueCellComponent); + component = fixture.componentInstance; + + fixture.componentRef.setInput('value', value as any); + + fixture.detectChanges(); + + return { component, fixture }; + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ValueCellComponent], + }).overrideComponent(ValueCellComponent, { + set: { template: '
', imports: [] }, + }); + }); + + it('should create', () => { + const { component } = makeComponent('r1'); + expect(component).toBeTruthy(); + }); + + it('should accept non-boolean value and mark as not boolean-like', () => { + const { component } = makeComponent('cluster-a'); + + expect(component.isBoolLike()).toBe(false); + expect(component.value()).toBe('cluster-a'); + expect(component.iconDesign()).toBeUndefined(); + expect(component.iconName()).toBeUndefined(); + }); + + it("should accept boolean-like 'true' string and set positive icon and design", () => { + const { component } = makeComponent('true'); + + expect(component.isBoolLike()).toBe(true); + expect(component.iconDesign()).toBe(ICON_DESIGN_POSITIVE); + expect(component.iconName()).toBe(ICON_NAME_POSITIVE); + }); + + it("should accept boolean-like 'false' string and set negative icon and design", () => { + const { component } = makeComponent('false'); + + expect(component.isBoolLike()).toBe(true); + expect(component.iconDesign()).toBe(ICON_DESIGN_NEGATIVE); + expect(component.iconName()).toBe(ICON_NAME_NEGATIVE); + }); + + it('should accept boolean value true and set positive icon', () => { + const { component } = makeComponent(true); + + expect(component.isBoolLike()).toBe(true); + expect(component.iconDesign()).toBe(ICON_DESIGN_POSITIVE); + expect(component.iconName()).toBe(ICON_NAME_POSITIVE); + }); +}); diff --git a/projects/wc/src/app/components/generic-ui/value-cell/value-cell.component.ts b/projects/wc/src/app/components/generic-ui/value-cell/value-cell.component.ts new file mode 100644 index 0000000..4906692 --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/value-cell/value-cell.component.ts @@ -0,0 +1,56 @@ +import { + ICON_DESIGN_NEGATIVE, + ICON_DESIGN_POSITIVE, + ICON_NAME_NEGATIVE, + ICON_NAME_POSITIVE, +} from './value-cell.constants'; +import { + ChangeDetectionStrategy, + Component, + computed, + input, +} from '@angular/core'; +import { IconComponent } from '@ui5/webcomponents-ngx'; + +export type IconDesignType = + | typeof ICON_DESIGN_POSITIVE + | typeof ICON_DESIGN_NEGATIVE; + +@Component({ + selector: 'value-cell', + standalone: true, + imports: [IconComponent], + templateUrl: './value-cell.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ValueCellComponent { + value = input(); + isBoolLike = computed(() => this.boolValue() !== undefined); + iconDesign = computed(() => { + return this.boolValue() === undefined + ? undefined + : this.boolValue() + ? ICON_DESIGN_POSITIVE + : ICON_DESIGN_NEGATIVE; + }); + iconName = computed(() => { + return this.boolValue() === undefined + ? undefined + : this.boolValue() + ? ICON_NAME_POSITIVE + : ICON_NAME_NEGATIVE; + }); + + private boolValue = computed(() => this.normalizeBoolean(this.value())); + + private normalizeBoolean(value: unknown): boolean | undefined { + const normalizedValue = value?.toString()?.toLowerCase(); + if (normalizedValue === 'true') { + return true; + } + if (normalizedValue === 'false') { + return false; + } + return undefined; + } +} diff --git a/projects/wc/src/app/components/generic-ui/value-cell/value-cell.constants.ts b/projects/wc/src/app/components/generic-ui/value-cell/value-cell.constants.ts new file mode 100644 index 0000000..d4bce4d --- /dev/null +++ b/projects/wc/src/app/components/generic-ui/value-cell/value-cell.constants.ts @@ -0,0 +1,5 @@ +export const ICON_DESIGN_POSITIVE = 'Positive'; +export const ICON_DESIGN_NEGATIVE = 'Negative'; + +export const ICON_NAME_POSITIVE = 'accept'; +export const ICON_NAME_NEGATIVE = 'decline'; diff --git a/projects/lib/organization/components/organization-management/organization-management.component.html b/projects/wc/src/app/components/organization-management/organization-management.component.html similarity index 100% rename from projects/lib/organization/components/organization-management/organization-management.component.html rename to projects/wc/src/app/components/organization-management/organization-management.component.html diff --git a/projects/lib/organization/components/organization-management/organization-management.component.scss b/projects/wc/src/app/components/organization-management/organization-management.component.scss similarity index 100% rename from projects/lib/organization/components/organization-management/organization-management.component.scss rename to projects/wc/src/app/components/organization-management/organization-management.component.scss diff --git a/projects/lib/organization/components/organization-management/organization-management.component.spec.ts b/projects/wc/src/app/components/organization-management/organization-management.component.spec.ts similarity index 73% rename from projects/lib/organization/components/organization-management/organization-management.component.spec.ts rename to projects/wc/src/app/components/organization-management/organization-management.component.spec.ts index 54c1b8d..b5321d8 100644 --- a/projects/lib/organization/components/organization-management/organization-management.component.spec.ts +++ b/projects/wc/src/app/components/organization-management/organization-management.component.spec.ts @@ -6,12 +6,12 @@ import { import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { MutationResult } from '@apollo/client'; -import { LuigiContextService } from '@luigi-project/client-support-angular'; +import { LuigiClient } from '@luigi-project/client/luigi-element'; import { ClientEnvironment, EnvConfigService, - I18nService, - LuigiCoreService, LuigiGlobalContext, NodeContext, ResourceService + I18nService, LuigiGlobalContext, NodeContext, } from '@openmfp/portal-ui-lib'; +import { ResourceService } from '@platform-mesh/portal-ui-lib/services'; import { of, throwError } from 'rxjs'; import { OrganizationManagementComponent } from './organization-management.component'; @@ -20,8 +20,8 @@ describe('OrganizationManagementComponent', () => { let fixture: ComponentFixture; let resourceServiceMock: jest.Mocked; let i18nServiceMock: jest.Mocked; - let luigiCoreServiceMock: jest.Mocked; let envConfigServiceMock: jest.Mocked; + let luigiClientMock: jest.Mocked; beforeEach(async () => { resourceServiceMock = { @@ -34,33 +34,33 @@ describe('OrganizationManagementComponent', () => { getTranslation: jest.fn(), } as any; - luigiCoreServiceMock = { - getGlobalContext: jest.fn(), - showAlert: jest.fn(), - } as any; - envConfigServiceMock = { getEnvConfig: jest.fn(), } as any; + luigiClientMock = { + uxManager: jest.fn().mockReturnValue({ + showAlert: jest.fn(), + }), + } as any; + await TestBed.configureTestingModule({ imports: [OrganizationManagementComponent, FormsModule], providers: [ { provide: ResourceService, useValue: resourceServiceMock }, { provide: I18nService, useValue: i18nServiceMock }, - { provide: LuigiCoreService, useValue: luigiCoreServiceMock }, { provide: EnvConfigService, useValue: envConfigServiceMock }, - LuigiContextService, ], schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA], }) .overrideComponent(OrganizationManagementComponent, { - set: { template: '', imports: [] }, + set: { template: '' }, }) .compileComponents(); fixture = TestBed.createComponent(OrganizationManagementComponent); component = fixture.componentInstance; + component.LuigiClient = (() => luigiClientMock) as any; }); it('should create', () => { @@ -103,7 +103,8 @@ describe('OrganizationManagementComponent', () => { organization: 'org1', portalBaseUrl: 'https://test.com', }; - luigiCoreServiceMock.getGlobalContext.mockReturnValue(mockGlobalContext); + + component.context = (() => mockGlobalContext) as any; resourceServiceMock.readOrganizations.mockReturnValue( of(mockOrganizations as any), ); @@ -139,6 +140,7 @@ describe('OrganizationManagementComponent', () => { expect(component.organizations()).toEqual(['newOrg', 'existingOrg']); expect(component.organizationToSwitch).toBe('newOrg'); expect(component.newOrganization).toBe(''); + expect(luigiClientMock.uxManager().showAlert).toHaveBeenCalled(); }); it('should handle organization creation error', () => { @@ -149,7 +151,7 @@ describe('OrganizationManagementComponent', () => { component.onboardOrganization(); - expect(luigiCoreServiceMock.showAlert).toHaveBeenCalledWith({ + expect(component.LuigiClient().uxManager().showAlert).toHaveBeenCalledWith({ text: 'Failure! Could not create organization: newOrg.', type: 'error', }); @@ -181,43 +183,4 @@ describe('OrganizationManagementComponent', () => { expect(window.location.href).toBe('https://newOrg.test.com:8080'); }); - - it('should not switch and show alert for invalid organization name', async () => { - const mockEnvConfig: ClientEnvironment = { - idpName: 'test', - organization: 'test', - oauthServerUrl: 'https://test.com', - clientId: 'test', - baseDomain: 'test.com', - isLocal: false, - developmentInstance: false, - authData: { - expires_in: '3600', - access_token: 'test-access-token', - id_token: 'test-id-token', - }, - }; - envConfigServiceMock.getEnvConfig.mockResolvedValue(mockEnvConfig); - - const invalidNames = ['-abc', 'abc-', 'a.b', 'a b', '']; - - for (const name of invalidNames) { - component.organizationToSwitch = name as any; - Object.defineProperty(window, 'location', { - value: { protocol: 'https:', port: '' }, - writable: true, - }); - - await component.switchOrganization(); - - expect(luigiCoreServiceMock.showAlert).toHaveBeenCalledWith({ - text: - 'Organization name is not valid for subdomain usage, accrording to RFC 1034/1123.', - type: 'error', - }); - - expect((window.location as any).href).toBeUndefined(); - (luigiCoreServiceMock.showAlert as jest.Mock).mockClear(); - } - }); }); diff --git a/projects/lib/organization/components/organization-management/organization-management.component.ts b/projects/wc/src/app/components/organization-management/organization-management.component.ts similarity index 87% rename from projects/lib/organization/components/organization-management/organization-management.component.ts rename to projects/wc/src/app/components/organization-management/organization-management.component.ts index 88ec94f..6aebe5c 100644 --- a/projects/lib/organization/components/organization-management/organization-management.component.ts +++ b/projects/wc/src/app/components/organization-management/organization-management.component.ts @@ -5,21 +5,19 @@ import { ViewEncapsulation, effect, inject, - signal + input, + signal, } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; -import { LuigiContextService } from '@luigi-project/client-support-angular'; +import { LuigiClient } from '@luigi-project/client/luigi-element'; import { EnvConfigService, I18nService, - LuigiCoreService, Resource, - ResourceDefinition, - ResourceNodeContext, - ResourceService, - generateGraphQLFields + ResourceDefinition } from '@openmfp/portal-ui-lib'; +import { ResourceNodeContext, ResourceService } from '@platform-mesh/portal-ui-lib/services'; +import { generateGraphQLFields } from '@platform-mesh/portal-ui-lib/utils'; import { ButtonComponent, InputComponent, @@ -27,7 +25,7 @@ import { OptionComponent, SelectComponent, } from '@ui5/webcomponents-ngx'; -import { map } from 'rxjs'; + @Component({ selector: 'organization-management', @@ -48,11 +46,10 @@ import { map } from 'rxjs'; export class OrganizationManagementComponent implements OnInit { private i18nService = inject(I18nService); private resourceService = inject(ResourceService); - private luigiCoreService = inject(LuigiCoreService); private envConfigService = inject(EnvConfigService); - private contextService = inject(LuigiContextService); + context = input(); + LuigiClient = input(); - context = toSignal(this.contextService.contextObservable().pipe(map((context) => context.context as ResourceNodeContext))); texts: any = {}; organizations = signal([]); organizationToSwitch: string; @@ -93,7 +90,7 @@ export class OrganizationManagementComponent implements OnInit { .map((o) => o.metadata.name) .filter( (o) => - o !== this.luigiCoreService.getGlobalContext().organization, + o !== this.context()['organization'] ), ); }, @@ -124,13 +121,13 @@ export class OrganizationManagementComponent implements OnInit { ]); this.organizationToSwitch = this.newOrganization; this.newOrganization = ''; - this.luigiCoreService.showAlert({ + this.LuigiClient().uxManager().showAlert({ text: 'New organization has been created, select it from the list to switch to it.', type: 'info', }); }, error: (error) => { - this.luigiCoreService.showAlert({ + this.LuigiClient().uxManager().showAlert({ text: `Failure! Could not create organization: ${resource.metadata.name}.`, type: 'error', }); @@ -186,7 +183,7 @@ export class OrganizationManagementComponent implements OnInit { const sanitizedOrg = this.sanitizeSubdomainInput(this.organizationToSwitch); if (!sanitizedOrg) { - this.luigiCoreService.showAlert({ + this.LuigiClient().uxManager().showAlert({ text: 'Organization name is not valid for subdomain usage, accrording to RFC 1034/1123.', type: 'error', }); diff --git a/projects/wc/src/app/initializers/luigi-wc-initializer.spec.ts b/projects/wc/src/app/initializers/luigi-wc-initializer.spec.ts new file mode 100644 index 0000000..09a547d --- /dev/null +++ b/projects/wc/src/app/initializers/luigi-wc-initializer.spec.ts @@ -0,0 +1,41 @@ +import { ApplicationInitStatus } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; + +import { DetailViewComponent } from '../components/generic-ui/detail-view/detail-view.component'; +import { ListViewComponent } from '../components/generic-ui/list-view/list-view.component'; +import { provideLuigiWebComponents } from './luigi-wc-initializer'; + +import { OrganizationManagementComponent } from '../components/organization-management/organization-management.component'; +import * as wc from '../utils/wc'; + +describe('provideLuigiWebComponents', () => { + beforeEach(() => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [provideLuigiWebComponents()], + }); + }); + + it('registers Luigi web components on app init', async () => { + const spy = jest.spyOn(wc, 'registerLuigiWebComponents').mockReturnValue(); + + // Trigger Angular APP_INITIALIZERs + await TestBed.inject(ApplicationInitStatus).donePromise; + + expect(spy).toHaveBeenCalledTimes(1); + + const expectedMap = { + 'generic-list-view': ListViewComponent, + 'generic-detail-view': DetailViewComponent, + 'organization-management': OrganizationManagementComponent, + } as Record; + + // Validate first arg equals the components map + expect(spy.mock.calls[0][0]).toEqual(expectedMap); + + // Validate second arg looks like an Injector + const passedInjector = spy.mock.calls[0][1]; + expect(passedInjector).toBeTruthy(); + expect(typeof passedInjector.get).toBe('function'); + }); +}); diff --git a/projects/wc/src/app/initializers/luigi-wc-initializer.ts b/projects/wc/src/app/initializers/luigi-wc-initializer.ts new file mode 100644 index 0000000..78f6b1b --- /dev/null +++ b/projects/wc/src/app/initializers/luigi-wc-initializer.ts @@ -0,0 +1,20 @@ +import { Injector, inject, provideAppInitializer } from '@angular/core'; +import { DetailViewComponent } from '../components/generic-ui/detail-view/detail-view.component'; +import { ListViewComponent } from '../components/generic-ui/list-view/list-view.component'; +import { OrganizationManagementComponent } from '../components/organization-management/organization-management.component'; +import { registerLuigiWebComponents } from '../utils/wc'; + +export const provideLuigiWebComponents = () => + provideAppInitializer(() => { + const injector = inject(Injector); + registerLuigiWebComponents( + { + 'generic-list-view': ListViewComponent, + 'generic-detail-view': DetailViewComponent, + 'organization-management': OrganizationManagementComponent, + }, + injector, + ); + + return undefined + }); diff --git a/projects/wc/src/app/utils/wc.spec.ts b/projects/wc/src/app/utils/wc.spec.ts new file mode 100644 index 0000000..be68fc1 --- /dev/null +++ b/projects/wc/src/app/utils/wc.spec.ts @@ -0,0 +1,181 @@ +import * as wc from './wc'; +import { mock } from 'jest-mock-extended'; +import { Injector, Type } from '@angular/core'; + +jest.mock('@angular/elements', () => ({ + createCustomElement: jest.fn(), +})); + +import * as angularElements from '@angular/elements'; + +describe('Luigi WebComponents Utils', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('registerLuigiWebComponent', () => { + const component = mock>(); + const injector = mock(); + const element = mock>(); + const src = 'src-of-the-script'; + + const createCustomElementSpy = ( + angularElements.createCustomElement as jest.MockedFunction< + typeof angularElements.createCustomElement + > + ).mockReturnValue(element); + const _registerWebcomponent = jest.fn(); + // @ts-ignore + window.Luigi = { _registerWebcomponent }; + + const getSrcSpy = jest.spyOn(wc, 'getSrc').mockReturnValue(src); + + wc.registerLuigiWebComponent(component, injector); + + expect(createCustomElementSpy).toHaveBeenCalledWith(component, { + injector, + }); + expect(getSrcSpy).toHaveBeenCalled(); + expect(_registerWebcomponent).toHaveBeenCalledWith(src, element); + }); + + it('registerLuigiWebComponents', () => { + const component1 = mock>(); + const component2 = mock>(); + const components = { + component1, + component2, + }; + const injector = mock(); + + const getSrcSpy = jest + .spyOn(wc, 'getSrc') + .mockReturnValue('http://localhost:12345/main.js#component1'); + + const registerLuigiWebComponentSpy = jest + .spyOn(wc, 'registerLuigiWebComponent') + .mockReturnValue(void 0); + + wc.registerLuigiWebComponents(components, injector); + + expect(getSrcSpy).toHaveBeenCalled(); + expect(registerLuigiWebComponentSpy).toHaveBeenCalledWith( + component1, + injector + ); + }); + + it('registerLuigiWebComponents no hash', () => { + const component1 = mock>(); + const component2 = mock>(); + const components = { + component1, + component2, + }; + const injector = mock(); + + const getSrcSpy = jest + .spyOn(wc, 'getSrc') + .mockReturnValue('http://localhost:12345/main.js'); + + const registerLuigiWebComponentSpy = jest + .spyOn(wc, 'registerLuigiWebComponent') + .mockReturnValue(void 0); + + wc.registerLuigiWebComponents(components, injector); + + expect(getSrcSpy).toHaveBeenCalled(); + expect(registerLuigiWebComponentSpy).not.toHaveBeenCalled(); + }); + + it('registerLuigiWebComponents no corresponding component', () => { + const component1 = mock>(); + const component2 = mock>(); + const components = { + component1, + component2, + }; + const injector = mock(); + + const getSrcSpy = jest + .spyOn(wc, 'getSrc') + .mockReturnValue('http://localhost:12345/main.js#component7'); + + const registerLuigiWebComponentSpy = jest + .spyOn(wc, 'registerLuigiWebComponent') + .mockReturnValue(void 0); + + wc.registerLuigiWebComponents(components, injector); + + expect(getSrcSpy).toHaveBeenCalled(); + expect(registerLuigiWebComponentSpy).not.toHaveBeenCalled(); + }); + + describe('getSrc', () => { + let originalCurrentScript: any; + + beforeEach(() => { + originalCurrentScript = document.currentScript; + }); + + afterEach(() => { + Object.defineProperty(document, 'currentScript', { + value: originalCurrentScript, + writable: true, + }); + }); + + it('should throw error when src attribute does not exist', () => { + Object.defineProperty(document, 'currentScript', { + value: { + getAttribute: () => null, + }, + writable: true, + }); + + expect(() => wc.getSrc()).toThrow('Not defined src of currentScript.'); + }); + + it('should throw error when currentScript is null', () => { + Object.defineProperty(document, 'currentScript', { + value: null, + writable: true, + }); + + expect(() => wc.getSrc()).toThrow('Not defined src of currentScript.'); + }); + + it('should throw error when currentScript is undefined', () => { + Object.defineProperty(document, 'currentScript', { + value: undefined, + writable: true, + }); + + expect(() => wc.getSrc()).toThrow('Not defined src of currentScript.'); + }); + + it('should throw error when getAttribute returns empty string', () => { + Object.defineProperty(document, 'currentScript', { + value: { + getAttribute: () => '', + }, + writable: true, + }); + + expect(() => wc.getSrc()).toThrow('Not defined src of currentScript.'); + }); + + it('should get src', () => { + const src = 'http://localhost:12345/main.js#component1'; + + Object.defineProperty(document, 'currentScript', { + value: { + getAttribute: () => src, + }, + writable: true, + }); + + expect(wc.getSrc()).toEqual(src); + }); + }); +}); diff --git a/projects/wc/src/app/utils/wc.ts b/projects/wc/src/app/utils/wc.ts new file mode 100644 index 0000000..881e66b --- /dev/null +++ b/projects/wc/src/app/utils/wc.ts @@ -0,0 +1,37 @@ +import { Injector, Type } from '@angular/core'; +import { createCustomElement } from '@angular/elements'; + +export const registerLuigiWebComponent = ( + component: Type, + injector: Injector, +) => { + const el = createCustomElement(component, { injector }); + (window as any).Luigi._registerWebcomponent(getSrc(), el); +}; + +/** + * When there are multiple web components in the same Angular project, use this method to register them. + * In the content-configuration.json, set the hash of the urlSuffix to the key of this map. + * + * @param components + * @param injector + */ +export const registerLuigiWebComponents = ( + components: Record>, + injector: Injector, +) => { + const hash = getSrc().split('#')[1]; + if (!hash || !components[hash]) { + return; + } + return registerLuigiWebComponent(components[hash], injector); +}; + +export const getSrc = () => { + const src = document.currentScript?.getAttribute('src'); + console.log('currentScript', document.currentScript, src); + if (!src) { + throw new Error('Not defined src of currentScript.'); + } + return src; +}; diff --git a/projects/wc/src/main.ts b/projects/wc/src/main.ts new file mode 100644 index 0000000..34201da --- /dev/null +++ b/projects/wc/src/main.ts @@ -0,0 +1,5 @@ +import '@angular/localize/init'; +import { appConfig } from './app/app.config'; +import { createApplication } from '@angular/platform-browser'; + +createApplication(appConfig).catch((err) => console.error(err)); diff --git a/projects/wc/tsconfig.app.json b/projects/wc/tsconfig.app.json new file mode 100644 index 0000000..4c177cf --- /dev/null +++ b/projects/wc/tsconfig.app.json @@ -0,0 +1,4 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json" +} diff --git a/projects/wc/tsconfig.app.prod.json b/projects/wc/tsconfig.app.prod.json new file mode 100644 index 0000000..4c177cf --- /dev/null +++ b/projects/wc/tsconfig.app.prod.json @@ -0,0 +1,4 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json" +} diff --git a/projects/wc/tsconfig.json b/projects/wc/tsconfig.json new file mode 100644 index 0000000..4ed7cdf --- /dev/null +++ b/projects/wc/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/app", + "types": ["@types/node"] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/projects/wc/tsconfig.spec.json b/projects/wc/tsconfig.spec.json new file mode 100644 index 0000000..faa16be --- /dev/null +++ b/projects/wc/tsconfig.spec.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.spec.json" +} diff --git a/tsconfig.json b/tsconfig.json index 3fd0818..3386c68 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compileOnSave": false, "compilerOptions": { + "composite": true, "moduleResolution": "node", "target": "ES2022", "lib": ["ES2022", "dom"], @@ -14,6 +15,8 @@ "@platform-mesh/portal-ui-lib": [ "./projects/lib" ], + "@platform-mesh/portal-ui-lib/services": ["./projects/lib/services/public-api.ts"], + "@platform-mesh/portal-ui-lib/utils": ["./projects/lib/utils/public-api.ts"], }, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, @@ -29,5 +32,5 @@ "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true - } + }, }